diff options
253 files changed, 4253 insertions, 2299 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 0ca97898e936..dd919cacc534 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -93,6 +93,7 @@ aconfig_declarations_group { "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 @@ java_aconfig_library { 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 533f9f66434b..3f2316f005bd 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -102,6 +102,11 @@ combined_apis { "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 b3a674fbd70e..d1aa23c8ea5f 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -466,6 +466,32 @@ java_library { } 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 { }, } +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 @@ java_api_library { 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 @@ java_library { 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 @@ java_library { "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 966bf13adfe4..5c5b220d20e9 100644 --- a/cmds/uiautomator/library/Android.bp +++ b/cmds/uiautomator/library/Android.bp @@ -28,9 +28,9 @@ droidstubs { "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 @@ droiddoc { ":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 5d134c6fed7b..542543d11318 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -32613,6 +32613,13 @@ package android.os { 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 @@ package android.os { 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 df45862b4d3f..8447a7feb54e 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -387,6 +387,12 @@ package android.os { 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 7a8e82960e92..b2a49e11c8df 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3835,6 +3835,7 @@ package android.content { 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 caf699280e08..72a68f85b6b7 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1797,18 +1797,20 @@ package android.hardware.input { 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 dbf9afdd52ff..ed6b85125e66 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 @@ public class ApplicationPackageManager extends PackageManager { 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 @@ public class ApplicationPackageManager extends PackageManager { } 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 0c786cb20fdc..0e761fce9346 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -19,6 +19,7 @@ package android.app; 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 @@ public class PropertyInvalidatedCache<Query, Result> { */ /** + * 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 @@ public class PropertyInvalidatedCache<Query, Result> { } } - 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 class PropertyInvalidatedCache<Query, Result> { 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 class PropertyInvalidatedCache<Query, Result> { 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 c27141a1acbf..0d981ea5a679 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -26,6 +26,7 @@ import android.annotation.MainThread; 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 @@ public abstract class AppFunctionService extends Service { @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 7948cec545c3..db663f8ed4c4 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 @@ import com.android.internal.compat.IPlatformCompat; */ 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 9891e8930936..9b06adf4e894 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -236,4 +236,12 @@ flag { 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 12c5d0756fc9..91f7a8bae163 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4324,6 +4324,7 @@ public abstract class Context { SECURITY_STATE_SERVICE, //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE, CONTACT_KEYS_SERVICE, + RANGING_SERVICE, }) @Retention(RetentionPolicy.SOURCE) @@ -6402,6 +6403,17 @@ public abstract class Context { /** * 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 ffadd1e4083c..2cdae21bde92 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 9612a53be96e..7185719abdd5 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -1445,7 +1445,7 @@ public final class DisplayManagerGlobal { * 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 8592dedbb2bb..177ee6f1540a 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_BOUNCE_KEYS_FL 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 @@ public class InputSettings { * * <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 @@ public class InputSettings { } /** - * 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 @@ public class InputSettings { * * <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 @@ public class InputSettings { */ @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 @@ public class InputSettings { * * <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 @@ public class InputSettings { */ @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 @@ public class InputSettings { * * <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 @@ public class InputSettings { @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 @@ public class InputSettings { } /** - * 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 @@ public class InputSettings { * {@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 @@ public class InputSettings { @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 6f11d3ae661c..af93c964a8ba 100644 --- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java +++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java @@ -35,7 +35,6 @@ import android.os.PersistableBundle; 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 @@ public final class VcnGatewayConnectionConfig { @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 a97563724e50..e1d1b3c65c99 100644 --- a/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java +++ b/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java @@ -24,7 +24,6 @@ import android.os.Parcelable; 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 @@ public final class VcnUnderlyingNetworkSpecifier extends NetworkSpecifier implem @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 f3efd89d4bc3..8b267bf28c7e 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -141,6 +141,7 @@ public class BatteryManager { /** * 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 class BatteryManager { 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 80f39bfbdc21..d0828c384664 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 @@ public class Handler { } /** + * 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 @@ public class Handler { } /** + * 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 @@ public class Handler { * 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 026013c34e30..e4c12b6ff834 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -1144,9 +1144,10 @@ public final class PowerManager { } 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 738d12978aed..f670601bd0d4 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -216,3 +216,11 @@ flag { 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 7e51cb020196..e98397d104d6 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1796,7 +1796,8 @@ public final class PermissionManager { } /** @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 0ada9934482c..b8a8be159d12 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9158,15 +9158,27 @@ public final class Settings { 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 c2ad508c2b44..ca887646b3aa 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 076721f629ed..f69a333ff81f 100644 --- a/core/java/android/text/TextFlags.java +++ b/core/java/android/text/TextFlags.java @@ -19,11 +19,10 @@ package android.text; 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 @@ public final class TextFlags { * 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 @@ public final class TextFlags { * 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 3846972a12e8..c83285a5c889 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -84,16 +84,6 @@ flag { } 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 3cfde870de18..8bb4c526b20d 100644 --- a/core/java/android/window/TransitionFilter.java +++ b/core/java/android/window/TransitionFilter.java @@ -187,6 +187,7 @@ public final class TransitionFilter implements Parcelable { /** 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 @@ public final class TransitionFilter implements Parcelable { // 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 @@ public final class TransitionFilter implements Parcelable { dest.writeStrongBinder(mLaunchCookie); int customAnimRaw = mCustomAnimation == null ? 0 : (mCustomAnimation ? 2 : 1); dest.writeInt(customAnimRaw); + dest.writeStrongBinder(mTaskFragmentToken); } @NonNull @@ -357,6 +366,9 @@ public final class TransitionFilter implements Parcelable { 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 ec79f94a6dd3..14505f527195 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -49,6 +49,7 @@ import android.content.ComponentName; 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 @@ public final class TransitionInfo implements Parcelable { 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 @@ public final class TransitionInfo implements Parcelable { mSnapshotLuma = in.readFloat(); mActivityComponent = in.readTypedObject(ComponentName.CREATOR); mAnimationOptions = in.readTypedObject(AnimationOptions.CREATOR); + mTaskFragmentToken = in.readStrongBinder(); } private Change localRemoteCopy() { @@ -737,6 +740,7 @@ public final class TransitionInfo implements Parcelable { out.mSnapshotLuma = mSnapshotLuma; out.mActivityComponent = mActivityComponent; out.mAnimationOptions = mAnimationOptions; + out.mTaskFragmentToken = mTaskFragmentToken; return out; } @@ -854,6 +858,14 @@ public final class TransitionInfo implements Parcelable { 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 @@ public final class TransitionInfo implements Parcelable { 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 @@ public final class TransitionInfo implements Parcelable { dest.writeFloat(mSnapshotLuma); dest.writeTypedObject(mActivityComponent, flags); dest.writeTypedObject(mAnimationOptions, flags); + dest.writeStrongBinder(mTaskFragmentToken); } @NonNull @@ -1110,6 +1132,9 @@ public final class TransitionInfo implements Parcelable { 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 13648de5b28e..9ae3fc1fa3f0 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -122,3 +122,10 @@ flag { 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 e440dc9053fd..fbc058cc0330 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -210,6 +210,8 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto 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 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto "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 7031d694f09c..d65aaae7deaa 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]); - } + 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. + */ + boolean isLoggingToLogcat(@NonNull String group); /** * 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); - } + void enableProtoLogToLogcat(@NonNull String... groups); /** * Disable logging target groups to logcat. * @param groups we want to disable from being logged to logcat. */ - public void disableProtoLogToLogcat(String... groups) { - toggleProtoLogToLogcat(false, groups); - } - - /** - * Check if a group is logging to logcat - * @param group The group we want to check for - * @return True iff we are logging this group to logcat. - */ - public boolean isLoggingToLogcat(@NonNull String group) { - final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group); - - if (isLoggingToLogcat == null) { - throw new RuntimeException( - "Trying to get logcat logging status of non-registered group " + group); - } - - return isLoggingToLogcat; - } - - private void registerViewerConfigFile( - @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) { - final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0); - mConfigFileCounts.put(viewerConfigFile, count + 1); - mClientConfigFiles.put(client, viewerConfigFile); - } - - private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups, - @NonNull boolean[] logcatStatuses) throws RemoteException { - if (groups.length != logcatStatuses.length) { - throw new RuntimeException( - "Expected groups and logcatStatuses to have the same length, " - + "but groups has length " + groups.length - + " and logcatStatuses has length " + logcatStatuses.length); - } - - for (int i = 0; i < groups.length; i++) { - String group = groups[i]; - boolean logcatStatus = logcatStatuses[i]; - - mRegisteredGroups.add(group); - - mGroupToClients.putIfAbsent(group, new HashSet<>()); - mGroupToClients.get(group).add(client); - - if (!mLogGroupToLogcatStatus.containsKey(group)) { - mLogGroupToLogcatStatus.put(group, logcatStatus); - } - - boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group); - if (requestedLogToLogcat != logcatStatus) { - client.toggleLogcat(requestedLogToLogcat, new String[] { group }); - } - } - } - - private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) { - final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>(); - - for (String group : groups) { - final var clients = mGroupToClients.get(group); - - if (clients == null) { - // No clients associated to this group - Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group - + " with no registered clients."); - continue; - } - - for (IProtoLogClient client : clients) { - clientToGroups.putIfAbsent(client, new HashSet<>()); - clientToGroups.get(client).add(group); - } - } - - for (IProtoLogClient client : clientToGroups.keySet()) { - try { - client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0])); - } catch (RemoteException e) { - throw new RuntimeException( - "Failed to toggle logcat status for groups on client", e); - } - } - - for (String group : groups) { - mLogGroupToLogcatStatus.put(group, enabled); - } - } - - private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { - mRunningInstances.add(instanceIdx); - } - - private void onTracingInstanceFlush() { - for (String fileName : mConfigFileCounts.keySet()) { - 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); - } + 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 000000000000..e382ac1513e0 --- /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 b73cacb77539..bdb33c4b151c 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.LinearLayout; 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 @@ public class ListMenuItemView extends LinearLayout 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 c43a8c6ee7ee..8e2536a423b8 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; import android.widget.PopupWindow.OnDismissListener; import android.widget.TextView; -import com.android.text.flags.Flags; - import java.util.Objects; /** @@ -122,8 +120,7 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On 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 cb7c226bfc64..606e829c41fa 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -401,6 +401,7 @@ message SecureSettingsProto { 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 @@ message SecureSettingsProto { // 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 86a7e8882bc4..000000000000 --- 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 4b7adf92cc03..000000000000 --- 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 6d31578ac020..e07471cd64bc 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -128,6 +128,22 @@ public final class Bitmap implements Parcelable { 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 @@ public final class Bitmap implements Parcelable { mWidth = width; mHeight = height; mRequestPremultiplied = requestPremultiplied; - mNinePatchChunk = ninePatchChunk; mNinePatchInsets = ninePatchInsets; if (density >= 0) { @@ -159,17 +174,9 @@ public final class Bitmap implements Parcelable { } 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 dd86a1a0edbb..83619efefd3b 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 @@ public class DesktopModeStatus { /** 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 @@ public class DesktopModeStatus { 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 @@ public class DesktopModeStatus { 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 c545d73734f0..af4a0c55f28d 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 @@ public class BubbleController implements ConfigurationChangeListener, 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 @@ public class BubbleController implements ConfigurationChangeListener, @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 1c9c195cf718..1367b7e24bc7 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 @@ public class BubbleBarLayerView extends FrameLayout 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 b47adb43c2a6..584f2721772a 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.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; +import com.android.wm.shell.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.CaptionWindowDecorViewModel; 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 @@ import java.util.Optional; includes = { WMShellBaseModule.class, PipModule.class, - ShellBackAnimationModule.class, + ShellBackAnimationModule.class }) public abstract class WMShellModule { @@ -249,6 +248,7 @@ public abstract class WMShellModule { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { if (DesktopModeStatus.canEnterDesktopMode(context)) { @@ -274,6 +274,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, + windowDecorCaptionHandleRepository, desktopActivityOrientationHandler, windowDecorViewHostSupplier); } @@ -384,19 +385,8 @@ public abstract class WMShellModule { @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 @@ public abstract class WMShellModule { @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 853284a58904..b8ebbcdbfb9d 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 @@ class DesktopTasksController( 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 @@ class DesktopTasksController( 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 @@ class DesktopTasksController( 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 000000000000..7ae537088832 --- /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 5a905cfd317f..e8eb10c984af 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } 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 02c818ffa906..7ea0bd68f732 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; 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { this( @@ -259,10 +263,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, + new AppHeaderViewHolder.Factory(), rootTaskDisplayAreaOrganizer, new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter, + windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory()); } @@ -291,10 +297,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; mTransactionFactory = transactionFactory; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mGenericLinksParser = genericLinksParser; mInputManager = mContext.getSystemService(InputManager.class); @@ -325,6 +334,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { : 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 16036bee75b3..10e4a39fc4ef 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_ 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.DisplayLayout; 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private ExclusionRegionListener mExclusionRegionListener; + private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final MaximizeMenuFactory mMaximizeMenuFactory; private final HandleMenuFactory mHandleMenuFactory; @@ -181,6 +185,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, @@ -234,7 +244,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mGenericLinksParser = genericLinksParser; mAssistContentRequester = assistContentRequester; @@ -251,6 +263,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandleMenuFactory = handleMenuFactory; mMultiInstanceHelper = multiInstanceHelper; mWindowManagerWrapper = windowManagerWrapper; + mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; } /** @@ -383,6 +396,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin position.set(determineHandlePosition()); } Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData"); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } mWindowDecorViewHolder.bindData(mTaskInfo, position, mResult.mCaptionWidth, @@ -407,6 +426,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isFocused) { closeHandleMenu(); + closeManageWindowsMenu(); closeMaximizeMenu(); } updateDragResizeListener(oldDecorationSurface); @@ -507,6 +527,67 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mAppIconBitmap, mAppName, mSplitScreenController, - DesktopModeStatus.canEnterDesktopMode(mContext), + canEnterDesktopMode(mContext), supportsMultiInstance, shouldShowManageWindowsButton, getBrowserLink(), @@ -1027,6 +1108,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Unit.INSTANCE; } ); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } mMinimumInstancesFound = false; } @@ -1067,7 +1151,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } void closeManageWindowsMenu() { - mManageWindowsMenu.close(); + if (mManageWindowsMenu != null) { + mManageWindowsMenu.close(); + } + mManageWindowsMenu = null; } private void updateGenericLink() { @@ -1089,11 +1176,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin public void close() { closeDragResizeListener(); closeHandleMenu(); + closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); disposeStatusBarInputLayer(); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } + super.close(); } @@ -1367,10 +1463,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 6f3f41191485..6eb5cca9ad1a 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.PointF; 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 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T @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 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T 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 033d69583725..4a8cabca98cf 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.content.res.Configuration 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 @@ import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppeara * 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 @@ internal class AppHeaderViewHolder( 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 @@ internal class AppHeaderViewHolder( 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 2341b099699f..5ea55b367703 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 @@ import android.view.View * 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 5156e47cfd13..139e6790b744 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.Context 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 @@ class DefaultWindowDecorViewHost( override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() - viewHostAdapter.release(t) + viewHost?.release() + t.remove(rootSurface) } private fun updateViewHost( @@ -83,15 +102,45 @@ class DefaultWindowDecorViewHost( configuration: Configuration, onDrawTransaction: SurfaceControl.Transaction? ) { - viewHostAdapter.prepareViewHost(configuration) + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost") + if (wwm == null) { + wwm = WindowlessWindowManager(configuration, rootSurface, null) + } + requireWindowlessWindowManager().setConfiguration(configuration) + if (viewHost == null) { + viewHost = surfaceControlViewHostFactory.invoke( + context, + display, + requireWindowlessWindowManager(), + "DefaultWindowDecorViewHost#updateViewHost" + ) + } onDrawTransaction?.let { - viewHostAdapter.applyTransactionOnDraw(it) + 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() } - viewHostAdapter.updateView(view, attrs) + 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 b04188fa82a8..000000000000 --- 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 64536d1a7897..000000000000 --- 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 a54c9ba67cf8..000000000000 --- 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 0df9bfa2ee78..000000000000 --- 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 e610ebd6bfab..8f20841e76b3 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 @@ class DesktopTasksControllerTest : ShellTestCase() { } @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 @@ private fun WindowContainerTransaction?.anyDensityConfigChange( } ?: 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 000000000000..e3caf2ede99d --- /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 fec9e3ebd1ef..aea14b900647 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 @@ public class ShellTransitionTests extends ShellTestCase { } @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 b8939e6ff623..49ae182fef34 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 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 class TransitionInfoBuilder { } 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 @@ public class TransitionInfoBuilder { /** 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 85bc7cc287e6..ee2a41c322c9 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.DesktopActivityOrientationChangeHandler 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.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, + mockAppHeaderViewHolderFactory, mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter), + mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory ) @@ -1211,7 +1217,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 dff42dae16a2..a1867f3698fc 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 @@ package com.android.wm.shell.windowdecor; 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.anyLong; 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.pm.PackageManager; 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.AttachedSurfaceControl; 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.DisplayController; 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private HandleMenuFactory mMockHandleMenuFactory; @Mock private MultiInstanceHelper mMockMultiInstanceHelper; + @Mock + private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository; @Captor private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; @Captor @@ -245,6 +260,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { != 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 ab41d9c80177..1273ee823159 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.Point 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.view.WindowManager.TRANSIT_CHANGE 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { 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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_movesTask_doesNotShowResizeVeil() { + fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, STARTING_BOUNDS.left.toFloat(), @@ -221,7 +223,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { }) } - @Test - fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() { + fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag STARTING_BOUNDS.left.toFloat(), @@ -321,7 +322,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_draggedTaskNotReorderedToTop() { + fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = false taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag @@ -370,7 +371,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testIsResizingOrAnimatingResizeSet() { + fun testIsResizingOrAnimatingResizeSet() = runOnUiThread { Assert.assertFalse(taskPositioner.isResizingOrAnimating) taskPositioner.onDragPositioningStart( @@ -443,7 +444,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @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 1b0b7d95e657..1b2ce9e4df36 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 @@ 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 @@ -27,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { ) // 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { // 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { 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 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { 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 a7e4213ad01d..000000000000 --- 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 de2444e34ca9..000000000000 --- 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 d6c80a7fffc1..000000000000 --- 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 000000000000..09e2f423c3ba --- /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 000000000000..504e3290b0ae --- /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 000000000000..d802177e249b --- /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 000000000000..d802177e249b --- /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 000000000000..d802177e249b --- /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 000000000000..d802177e249b --- /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 000000000000..d802177e249b --- /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 000000000000..b1dd4676a35e --- /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 000000000000..65959dfdf561 --- /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 000000000000..fa6d2ff12313 --- /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 000000000000..60c25fae58d1 --- /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 000000000000..b1b05f79f33f --- /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 185436160349..84bd45dfc012 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -264,6 +264,7 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy , mPixelStorageType(PixelStorageType::Heap) { mPixelStorage.heap.address = address; mPixelStorage.heap.size = size; + traceBitmapCreate(); } Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) @@ -272,6 +273,7 @@ Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) , 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 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info 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 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes 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 @@ void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { 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 dd344e2f5517..3d55d859ed5f 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 @@ private: } 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 d7bf20130b71..e13e136550ca 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -73,6 +73,7 @@ static void simplifyPaint(int color, Paint* paint) { } 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 63424892ec72..306643d8477e 100644 --- a/location/java/android/location/LocationManager.java +++ b/location/java/android/location/LocationManager.java @@ -451,7 +451,7 @@ public class LocationManager { 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 cddc337d95b8..c3cb4927ac10 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -94,6 +94,16 @@ flag { } 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 bc8a7afd94e9..9603c0a9f7c7 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -60,8 +60,13 @@ package android.nfc { 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 e2ec95215d1a..246efc7ca557 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -73,7 +73,7 @@ interface INfcAdapter 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 f47879385070..de85f1eeabe3 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -559,6 +559,18 @@ public final class NfcAdapter { @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 @@ public final class NfcAdapter { * 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 @@ public final class NfcAdapter { 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 d51b704a7e7f..45038d41e237 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -70,6 +70,58 @@ public final class NfcOemExtension { 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 @@ public final class NfcOemExtension { 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 bd84b58aa0f4..a30c0c3c6d4c 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 717ec02d9dd6..91882fde1537 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 022ddedd1062..265864e1b3fd 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.saveable.rememberSaveable 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 @@ private fun AlertDialogPresenter.SettingsAlertDialog( 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 @@ private fun AlertDialogPresenter.Button(button: AlertDialogButton) { 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 030522d73b26..58a83fa72532 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 @@ fun SettingsAlertDialogWithIcon( 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 @@ fun SettingsAlertDialogWithIcon( 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) + dismissButton = + dismissButton?.let { { OutlinedButton(onClick = { it.onClick() }) { Text(it.text) } } }, + title = + title?.let { + { + CenterRow { + Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + } } - } - }, - 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 bef0bca1c5a4..9f2210d852a9 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 @@ fun SettingsAlertDialogContent( 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 @@ fun SettingsAlertDialogContent( 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 @@ internal fun SettingsAlertDialogContent( 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 @@ internal fun SettingsAlertDialogContent( ) { 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 @@ internal fun SettingsAlertDialogContent( 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 @@ internal fun SettingsAlertDialogContent( ProvideContentColorTextStyle( contentColor = MaterialTheme.colorScheme.primary, textStyle = MaterialTheme.typography.labelLarge, - content = buttons + content = buttons, ) } } @@ -188,7 +157,7 @@ internal fun SettingsAlertDialogContent( 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 @@ internal fun AlertDialogFlowRow( // 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 @@ internal fun AlertDialogFlowRow( 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 @@ internal fun AlertDialogFlowRow( 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 @@ private val ButtonsCrossAxisSpacing = 12.dp /** * 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 val ButtonsCrossAxisSpacing = 12.dp 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 6f2567b9c5dc..a3f9e515a0bc 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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { "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 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { 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 01bb6f013d16..7ee7180811dc 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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa 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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa @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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa mTitle = title; mToggleInfos = ImmutableList.copyOf(toggleInfos); mState = state; + mIsActive = isActive; mIsAllowedChangingState = allowChangingState; mExtras = extras; } @@ -67,9 +70,11 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa 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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa dest.writeString(mTitle); dest.writeTypedList(mToggleInfos, flags); dest.writeInt(mState); + dest.writeBoolean(mIsActive); dest.writeBoolean(mIsAllowedChangingState); dest.writeBundle(mExtras); } @@ -108,6 +114,7 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa 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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa } /** + * 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 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa @NonNull public MultiTogglePreference build() { return new MultiTogglePreference( - mTitle, mToggleInfos.build(), mState, mAllowChangingState, mExtras); + mTitle, mToggleInfos.build(), mState, mIsActive, mAllowChangingState, mExtras); } } @@ -202,6 +222,16 @@ public class MultiTogglePreference extends DeviceSettingPreference implements Pa } /** + * 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 29664f63d3b2..851b614f5279 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 @@ class DeviceSettingRepositoryImpl( 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 7eae5b2a1f5f..3d8ff86c9377 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.flatMapConcat 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 @@ class DeviceSettingServiceConnection( } 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 @@ class DeviceSettingServiceConnection( IDeviceSettingsProviderService.Stub::asInterface, ) .stateIn( - coroutineScope, + coroutineScope.plus(backgroundCoroutineContext), SharingStarted.WhileSubscribed(), ServiceConnectionStatus.Connecting, ) @@ -263,21 +264,30 @@ class DeviceSettingServiceConnection( 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 766cd438a811..9dd2dbb41295 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 @@ public class InputMediaDevice extends MediaDevice { 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 874e03012ae2..0c50166fff8b 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 @@ public final class InputRouteManager { 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 @@ public final class InputRouteManager { 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 @@ public final class InputRouteManager { 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 251cd3615897..9a9a96012984 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 @@ public class ConversationIconFactory extends BaseIconFactory { * 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 62fcb5ec3011..1c7b5bceafe7 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 @@ public final class MultiTogglePreferenceTest { .addToggleInfo(TOGGLE_INFO_1) .addToggleInfo(TOGGLE_INFO_2) .setState(123) + .setIsActive(true) .setAllowChangingState(true) .setExtras(buildBundle("key1", "value1")) .build(); @@ -130,6 +131,7 @@ public final class MultiTogglePreferenceTest { 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 81b56343ceed..0cb6bc1b1261 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 @@ class DeviceSettingRepositoryTest { `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 @@ class DeviceSettingRepositoryTest { 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 2501ae6769b6..8a18d0714e0a 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.spy; 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.RobolectricTestRunner; 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 @@ public class InputRouteManagerTest { } @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 40a8199a0690..d7109398b956 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 @@ public class SecureSettings { 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 3b9c68389632..fa16a44f4592 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 @@ public class SecureSettingsValidators { 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 855ebe3799b8..c49ffb49a1da 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1063,6 +1063,13 @@ flag { } 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 @@ flag { 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 70529cc762e0..ee65fbd810ae 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.platform.test.annotations.EnableFlags 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.communalInteractor 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.dockManager 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { bgScope = applicationCoroutineScope, mainDispatcher = testDispatcher, centralSurfacesOpt = centralSurfacesOptional, + uiEventLogger = uiEventLoggerFake, ) .apply { start() } @@ -119,7 +122,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Communal) @@ -140,7 +143,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Communal) @@ -161,7 +164,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.GONE, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Blank) @@ -181,7 +184,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Communal) } @@ -220,7 +223,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OCCLUDED, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Blank) } @@ -240,7 +243,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OCCLUDED, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Communal) } @@ -258,7 +261,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Blank) } @@ -276,7 +279,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.OFF, - testScope = this + testScope = this, ) assertThat(scene).isEqualTo(CommunalScenes.Communal) @@ -298,7 +301,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.OFF, to = KeyguardState.GLANCEABLE_HUB, - testScope = this + testScope = this, ) advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY) @@ -327,7 +330,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, - testScope = this + testScope = this, ) updateDocked(true) @@ -349,7 +352,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, - testScope = this + testScope = this, ) updateDocked(true) @@ -361,7 +364,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { 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 361e768a5b51..8f9e23824809 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.collectLastValue 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.Test 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 @@ import org.mockito.MockitoAnnotations @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { // 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 @@ class KeyboardRepositoryTest : SysuiTestCase() { 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 @@ class KeyboardRepositoryTest : SysuiTestCase() { 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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { @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 @@ class KeyboardRepositoryTest : SysuiTestCase() { 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 d594f3a2f932..62d06254e541 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 @@ class MediaControlInteractorTest : SysuiTestCase() { MediaData( userId = USER_ID, instanceId = InstanceId.fakeInstanceId(2), - artist = ARTIST + artist = ARTIST, ) mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData) @@ -145,10 +145,17 @@ class MediaControlInteractorTest : SysuiTestCase() { 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 @@ class MediaControlInteractorTest : SysuiTestCase() { mediaData.appUid, surface = SURFACE, cardinality = 2, - rank = 1 + rank = 1, ) verify(activityStarter) .postStartActivityDismissingKeyguard(eq(clickIntent), eq(activityController)) @@ -232,7 +239,7 @@ class MediaControlInteractorTest : SysuiTestCase() { eq(true), eq(dialogTransitionController), eq(null), - eq(null) + eq(null), ) } @@ -248,7 +255,7 @@ class MediaControlInteractorTest : SysuiTestCase() { .createBroadcastDialogWithController( eq(APP_NAME), eq(PACKAGE_NAME), - eq(dialogTransitionController) + eq(dialogTransitionController), ) } @@ -279,7 +286,7 @@ class MediaControlInteractorTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - anyBoolean() + anyBoolean(), ) verify(listener).onMediaDataRemoved(eq(KEY), eq(true)) } @@ -307,7 +314,7 @@ class MediaControlInteractorTest : SysuiTestCase() { 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 9558e5d23a25..01220285e10c 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.MediaSession 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 @@ class MediaControlViewModelTest : SysuiTestCase() { 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 @@ class MediaControlViewModelTest : SysuiTestCase() { 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 @@ class MediaControlViewModelTest : SysuiTestCase() { 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 @@ class MediaControlViewModelTest : SysuiTestCase() { 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 index a770ee199ba6..a770ee199ba6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java +++ b/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 index 85bc634c28b1..85bc634c28b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java +++ b/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 index eae6cdbe4d2c..eae6cdbe4d2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt +++ b/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 index f3cea3e8bb96..f3cea3e8bb96 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt +++ b/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 index e58c8f281fc1..e58c8f281fc1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java +++ b/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 index 2905a7329d21..2905a7329d21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/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 index 3621ab975daf..3621ab975daf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java +++ b/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 index 403a883e1760..403a883e1760 100644 --- 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 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 index 79c4a96e84bb..79c4a96e84bb 100644 --- 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 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 index 7941a68fa91e..7941a68fa91e 100644 --- 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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt index 450aadd70171..450aadd70171 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt +++ b/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 index 9ef6b9c13315..9ef6b9c13315 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt +++ b/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 index c9a5d0620c65..c9a5d0620c65 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt +++ b/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 index 0c88da750885..0c88da750885 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt +++ b/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 index 8f4078b88fc0..8f4078b88fc0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt +++ b/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 index 3673a25b68b2..3673a25b68b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/NotificationHelperTest.java +++ b/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 index dae34524c203..dae34524c203 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java +++ b/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 index 3e6d6746a997..3e6d6746a997 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleProviderTestable.java +++ b/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 index ae7fba939944..ae7fba939944 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java +++ b/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 index e701dc63325f..e701dc63325f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java +++ b/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 index b3ded15f582f..b3ded15f582f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java +++ b/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 index 0d6652cca0e8..0d6652cca0e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt +++ b/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 index 9ac04cf98375..9ac04cf98375 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt +++ b/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 index 5250d56edc0b..5250d56edc0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java +++ b/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 index 3ae7a1672821..3ae7a1672821 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt +++ b/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 index 466a09be4ff3..466a09be4ff3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt +++ b/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 index db8612ad4182..db8612ad4182 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt +++ b/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 index 52ad931185e2..52ad931185e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt +++ b/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 index cc3e27c71564..cc3e27c71564 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt +++ b/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 index 07ec38e6ae6c..07ec38e6ae6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt +++ b/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 index 225adab04ff0..225adab04ff0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/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 index 02c5b5ad214c..02c5b5ad214c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/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 index 56e25fcd580c..56e25fcd580c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt +++ b/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 index ecdabbf9853e..ecdabbf9853e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt +++ b/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 index 369bb228494c..369bb228494c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/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 index 3d6ba94556b7..3d6ba94556b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt +++ b/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 index a0ccec11fec3..a0ccec11fec3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt +++ b/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 index be388a17ab5d..be388a17ab5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt +++ b/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 index 29e2a8a8824f..29e2a8a8824f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TouchAnimatorTest.java +++ b/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 index 0c2b59fed078..0c2b59fed078 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt +++ b/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 index 9f2b1ea9e37e..9f2b1ea9e37e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java +++ b/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 index cbcd8104ce35..cbcd8104ce35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java +++ b/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 index 09a6c2c7f1f7..09a6c2c7f1f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java +++ b/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 index bd03acb6e7ec..bd03acb6e7ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt +++ b/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 index eb013c5975b9..eb013c5975b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileColorPickerTest.java +++ b/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 index 03483c98856e..03483c98856e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java +++ b/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 index c5a2370adcda..c5a2370adcda 100644 --- 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 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 index f90e1e931099..f90e1e931099 100644 --- 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 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 index 090a85b0d3f9..090a85b0d3f9 100644 --- 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 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 index 12c566ca9e2d..12c566ca9e2d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt +++ b/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 index 2580ac2c8da7..2580ac2c8da7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java +++ b/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 index e112bb05ab2e..e112bb05ab2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt +++ b/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 index 7a99aefc98fe..7a99aefc98fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt +++ b/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 index d6be31450fc0..d6be31450fc0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt +++ b/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 index 093cdf22a64b..093cdf22a64b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt +++ b/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 index 9f12b189d76a..9f12b189d76a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/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 index 028beb599644..028beb599644 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java +++ b/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 index 1343527e631b..1343527e631b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java +++ b/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 index 73ae4ee5aa0d..73ae4ee5aa0d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt +++ b/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 index f90463e7f589..f90463e7f589 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt +++ b/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 index c854920cbf1f..c854920cbf1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt +++ b/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 index 0cf96047fcc0..0cf96047fcc0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java +++ b/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 index 0a1455fe12cc..0a1455fe12cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt +++ b/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 index dbdf3a499f8b..dbdf3a499f8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt +++ b/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 index 442a94887157..442a94887157 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java +++ b/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 index f1c589512895..f1c589512895 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt +++ b/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 index d6fa124f3f91..d6fa124f3f91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java +++ b/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 index f8f82f2c2ed8..f8f82f2c2ed8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java +++ b/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 index 1aff45bf581d..1aff45bf581d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java +++ b/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 index 41930636cfa3..41930636cfa3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java +++ b/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 index 0d12483bad0a..0d12483bad0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/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 index 8324a7303cff..8324a7303cff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt +++ b/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 index c764c548ed92..c764c548ed92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/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 index c918ed82604c..c918ed82604c 100644 --- 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 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 index b88861756889..b88861756889 100644 --- 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 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 index 57484c21c767..57484c21c767 100644 --- 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 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 index e7bde681fe6f..e7bde681fe6f 100644 --- 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 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 index ad6c64b32ddc..ad6c64b32ddc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/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 index d8618fa71aab..d8618fa71aab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt +++ b/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 index 57cfe1b9e902..57cfe1b9e902 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt +++ b/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 index 4ab3c7b203c4..4ab3c7b203c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt +++ b/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 index 8d84c3e7392e..8d84c3e7392e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/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 index ba7a65dd8e65..ba7a65dd8e65 100644 --- 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 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 index b53652067755..b53652067755 100644 --- 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 diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index ee6223fd5292..08a7c395e57f 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 @@ package com.android.systemui.communal 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 @@ constructor( @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 @@ constructor( CommunalScenes.Blank, "dream started after timeout", ) + uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT) } } } @@ -212,6 +216,7 @@ constructor( 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 8f913ff01337..78a8a42e2743 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.accessibility.CaptioningManager; 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 @@ public class FrameworkServicesModule { @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 1a0525d97d30..ba23eb341b89 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 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, 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 class KeyguardViewMediator implements CoreStartable, Dumpable, 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 class KeyguardViewMediator implements CoreStartable, Dumpable, } 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 deb0b2d8f848..b5f6b418e322 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 @@ object KeyguardRootViewBinder { -> 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 @@ object KeyguardRootViewBinder { .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 @@ object KeyguardRootViewBinder { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.UNLOCK, - authInteractionProperties, + authInteractionProperties ) } else { vibratorHelper.performHapticFeedback( @@ -399,7 +398,7 @@ object KeyguardRootViewBinder { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.FAILURE, - authInteractionProperties, + authInteractionProperties ) } else { vibratorHelper.performHapticFeedback( @@ -426,7 +425,7 @@ object KeyguardRootViewBinder { blueprintViewModel, clockViewModel, childViews, - burnInParams, + burnInParams ) ) @@ -465,7 +464,11 @@ object KeyguardRootViewBinder { */ 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 @@ object KeyguardRootViewBinder { 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 @@ object KeyguardRootViewBinder { viewModel.onNotificationContainerBoundsChanged( notificationListPlaceholder.top.toFloat(), notificationListPlaceholder.bottom.toFloat(), - animate = shouldAnimate, + animate = shouldAnimate ) } @@ -528,7 +531,7 @@ object KeyguardRootViewBinder { 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 4cf3c4e7f6d0..c6efcfad8da7 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 androidx.constraintlayout.widget.ConstraintLayout 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 @@ constructor( } } - fun refreshBlueprint(type: Type = Type.NoTransition) = - keyguardBlueprintInteractor.refreshBlueprint(type) - fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) { runningTransitions.mutate() @@ -96,7 +95,7 @@ constructor( Log.w( TAG, "runTransition: skipping ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config", + "currentPriority=$currentPriority; config=$config" ) } apply() @@ -107,7 +106,7 @@ constructor( 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 130868dc3c1c..1f339dddd4d1 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.bluetooth.BroadcastDialogController 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 @@ constructor( private val lockscreenUserManager: NotificationLockscreenUserManager, private val mediaOutputDialogManager: MediaOutputDialogManager, private val broadcastDialogController: BroadcastDialogController, + private val mediaLogger: MediaLogger, ) { val mediaControl: Flow<MediaControlModel?> = @@ -73,7 +75,7 @@ constructor( instanceId: InstanceId, delayMs: Long, eventId: Int, - location: Int + location: Int, ): Boolean { logSmartspaceUserEvent(eventId, location) val dismissed = @@ -81,7 +83,7 @@ constructor( 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 @@ constructor( 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 @@ constructor( } } - 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 @@ constructor( fun startMediaOutputDialog( expandable: Expandable, packageName: String, - token: MediaSession.Token? = null + token: MediaSession.Token? = null, ) { mediaOutputDialogManager.createAndShowWithController( packageName, @@ -180,7 +195,7 @@ constructor( broadcastDialogController.createBroadcastDialogWithController( broadcastApp, packageName, - expandable.dialogTransitionController() + expandable.dialogTransitionController(), ) } @@ -188,10 +203,14 @@ constructor( 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 7d20e170d8bc..88c47ba4d243 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 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { bool1 = active str2 = reason }, - { "add media $str1, active: $bool1, reason: $str2" } + { "add media $str1, active: $bool1, reason: $str2" }, ) } @@ -48,7 +48,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { str1 = instanceId.toString() str2 = reason }, - { "removing media $str1, reason: $str2" } + { "removing media $str1, reason: $str2" }, ) } @@ -61,7 +61,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { bool1 = isActive str2 = reason }, - { "add recommendation $str1, active $bool1, reason: $str2" } + { "add recommendation $str1, active $bool1, reason: $str2" }, ) } @@ -74,7 +74,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { bool1 = immediately str2 = reason }, - { "removing recommendation $str1, immediate=$bool1, reason: $str2" } + { "removing recommendation $str1, immediate=$bool1, reason: $str2" }, ) } @@ -83,7 +83,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { TAG, LogLevel.DEBUG, { str1 = instanceId.toString() }, - { "adding media card $str1 to carousel" } + { "adding media card $str1 to carousel" }, ) } @@ -92,7 +92,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { TAG, LogLevel.DEBUG, { str1 = instanceId.toString() }, - { "removing media card $str1 from carousel" } + { "removing media card $str1 from carousel" }, ) } @@ -101,7 +101,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { TAG, LogLevel.DEBUG, { str1 = key }, - { "adding recommendation card $str1 to carousel" } + { "adding recommendation card $str1 to carousel" }, ) } @@ -110,7 +110,7 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { TAG, LogLevel.DEBUG, { str1 = key }, - { "removing recommendation card $str1 from carousel" } + { "removing recommendation card $str1 from carousel" }, ) } @@ -119,7 +119,24 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { 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 6373feda9c9b..4055818dcb0b 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 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { viewHolder, viewModel.outputSwitcher, viewController, - falsingManager + falsingManager, ) bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager) bindActionButtons(viewHolder, viewModel, viewController, falsingManager) @@ -157,7 +160,7 @@ object MediaControlViewBinder { viewController, backgroundDispatcher, mainDispatcher, - isSongUpdated + isSongUpdated, ) if (viewModel.playTurbulenceNoise) { @@ -259,12 +262,12 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { viewController.collapsedLayout, visible, buttonModel.notVisibleValue, - buttonModel.showInCollapsed + buttonModel.showInCollapsed, ) } } @@ -350,7 +353,7 @@ object MediaControlViewBinder { createTouchRippleAnimation( button, viewController.colorSchemeTransition, - multiRippleView + multiRippleView, ) ) @@ -382,12 +385,12 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { // 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 @@ object MediaControlViewBinder { viewModel.backgroundCover!!, viewModel.colorScheme, width, - height + height, ) } else { ColorDrawable(Color.TRANSPARENT) @@ -493,7 +496,7 @@ object MediaControlViewBinder { transitionDrawable: TransitionDrawable, layer: Int, targetWidth: Int, - targetHeight: Int + targetHeight: Int, ) { val drawable = transitionDrawable.getDrawable(layer) ?: return val width = drawable.intrinsicWidth @@ -509,7 +512,7 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { private fun createTouchRippleAnimation( button: ImageButton, colorSchemeTransition: ColorSchemeTransition, - multiRippleView: MultiRippleView + multiRippleView: MultiRippleView, ): RippleAnimation { val maxSize = (multiRippleView.width * 2).toFloat() return RippleAnimation( @@ -562,7 +565,7 @@ object MediaControlViewBinder { baseRingFadeParams = null, sparkleRingFadeParams = null, centerFillFadeParams = null, - shouldDistort = false + shouldDistort = false, ) ) } @@ -596,7 +599,7 @@ object MediaControlViewBinder { 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 @@ object MediaControlViewBinder { 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 82099e61009f..3f22d549698c 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 @@ data class MediaActionViewModel( 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 64820e0d0ced..104d155ab74e 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 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( instanceId, MEDIA_PLAYER_ANIMATION_DELAY, SMARTSPACE_CARD_DISMISS_EVENT, - location + location, ) } @@ -99,7 +111,7 @@ class MediaControlViewModel( applicationContext, backgroundDispatcher, model.artwork, - TAG + TAG, ) val scheme = wallpaperColors?.let { ColorScheme(it, true, Style.CONTENT) } @@ -107,7 +119,7 @@ class MediaControlViewModel( applicationContext, model.packageName, TAG, - Style.CONTENT + Style.CONTENT, ) ?: return null @@ -131,7 +143,7 @@ class MediaControlViewModel( R.string.controls_media_playing_item_description, model.songName, model.artistName, - model.appName + model.appName, ) } }, @@ -157,7 +169,7 @@ class MediaControlViewModel( expandable, clickIntent, SMARTSPACE_CARD_CLICK_EVENT, - location + location, ) } }, @@ -177,7 +189,7 @@ class MediaControlViewModel( } } }, - onLocationChanged = { location = it } + onLocationChanged = { location = it }, ) } @@ -191,7 +203,7 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( ?: interactor.startMediaOutputDialog( expandable, model.packageName, - model.token + model.token, ) } - } + }, ) } @@ -270,7 +282,7 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( model, mediaButton.getActionById(buttonId), buttonId, - isScrubbingTimeEnabled + isScrubbingTimeEnabled, ) } } @@ -319,7 +331,7 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( private fun toNotifActionViewModel( model: MediaControlModel, mediaAction: MediaAction, - index: Int + index: Int, ): MediaActionViewModel { return MediaActionViewModel( icon = mediaAction.icon, @@ -375,7 +387,7 @@ class MediaControlViewModel( 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 @@ class MediaControlViewModel( 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 9df9bccdf522..2a47a5af790a 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 @@ data class MediaOutputSwitcherViewModel( 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 96e7fc79c8eb..f4b0d6e2c990 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 @@ data class MediaPlayerViewModel( 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 2cbc75755cfd..f7b73534d35c 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 @@ public class MediaSwitchingController @VisibleForTesting boolean mNeedRefresh = false; private MediaController mMediaController; - private InputRouteManager mInputRouteManager; + @VisibleForTesting InputRouteManager mInputRouteManager; @VisibleForTesting Callback mCallback; @VisibleForTesting @@ -927,7 +927,18 @@ public class MediaSwitchingController } 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 4251b81226b3..8351597f35de 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.annotation.RequiresPermission; 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 @@ public class MediaProjectionPermissionActivity extends Activity { 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 @@ public class MediaProjectionPermissionActivity extends Activity { 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 @@ public class MediaProjectionPermissionActivity extends Activity { } 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 @@ public class MediaProjectionPermissionActivity extends Activity { 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 526c64c15696..55943a527a8c 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 @@ abstract class StatusBarModule { @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 1a0327cdd809..1ee7cf3490f4 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_SEASCAPE; 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.app.viewcapture.ViewCaptureAwareWindowManager; 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 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController 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 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController 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 1c7debcdf39d..ebfbac7be916 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]. + * Binds a [StatusBarWindowViewInflater]. * - * Only [StatusBarWindowController] should inject the view. + * Only [StatusBarWindowControllerImpl] should inject it. */ - @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" - ) - } - } + @Binds + @SysUISingleton + @InternalWindowViewInflater + abstract fun providesStatusBarWindowViewInflater( + inflaterImpl: StatusBarWindowViewInflaterImpl + ): StatusBarWindowViewInflater /** - * 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. + * 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. [StatusBarWindowView] can only be injected - * if it's annotated with [InternalWindowView], and only classes inside this [statusbar.window] - * package can access the annotation. + * 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 000000000000..f030a4ac0d0e --- /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 bbfa32b623ef..32a4f12777ac 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 @@ public class ToastUI implements 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 7385b828695b..e76401528ff6 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.provider.Settings.Secure.NOTIFICATION_BUBBLES; 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 @@ public class BubblesManager { @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 d3e20c6e39b7..53f0800a7d13 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.media.flags.Flags; 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.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Collections; import java.util.List; @SmallTest @@ -115,6 +117,10 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { 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 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { 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 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { 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 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { // 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 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { AudioDeviceInfo.TYPE_BUILTIN_MIC, MAX_VOLUME, CURRENT_VOLUME, - IS_VOLUME_FIXED); + VOLUME_FIXED_TRUE); final MediaDevice mediaDevice4 = InputMediaDevice.create( mContext, @@ -563,7 +571,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { 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 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { 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 3e7980da87e3..0d398348bda2 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.NOTIFICAT 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 @@ public class BubblesTest extends SysuiTestCase { } @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 ee36cadd8480..de4bbecaaf0e 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 @@ class FakeInputManager { if (devices.containsKey(deviceId)) { return } - addPhysicalKeyboard(deviceId, enabled) + addPhysicalKeyboard(deviceId, enabled = enabled) } fun registerInputDeviceListener(listener: InputDeviceListener) { @@ -92,9 +92,15 @@ class FakeInputManager { 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 @@ class FakeInputManager { 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 6e650a3c5391..77afa7989e83 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.bluetooth.mockBroadcastDialogController 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 @@ val Kosmos.mediaControlInteractor by 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 e490b7502894..9ea660fd3704 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.kosmos.Kosmos 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 @@ val Kosmos.mediaControlInteractorFactory by 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 8c6f50e5c1bd..e29b6e403f2a 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 @@ public class MetadataSyncAdapter { 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 @@ public class MetadataSyncAdapter { } 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 @@ public class MetadataSyncAdapter { } /** + * 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 b109472a2a1e..2fa0e0d0d946 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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 6657c1c1ba89..59dea099c2a1 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -160,6 +160,7 @@ public final class BatteryService extends SystemService { 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 @@ public final class BatteryService extends SystemService { || 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 @@ public final class BatteryService extends SystemService { mLastInvalidCharger = mInvalidCharger; mLastBatteryCycleCount = mHealthInfo.batteryCycleCount; mLastChargingState = mHealthInfo.chargingState; + mLastBatteryCapacityLevel = mHealthInfo.batteryCapacityLevel; } } @@ -862,6 +865,7 @@ public final class BatteryService extends SystemService { 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 @@ public final class BatteryService extends SystemService { 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 @@ public final class BatteryService extends SystemService { 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 1c13ad5b3ceb..f32031dec43c 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_AUTOMOTIVE; 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.app.DisableCarModeActivity; 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 class UiModeManagerService extends SystemService { } final int user = UserHandle.getCallingUserId(); - enforceValidCallingUser(user); + enforceCurrentUserIfVisibleBackgroundEnabled(user); final long ident = Binder.clearCallingIdentity(); try { @@ -914,7 +913,7 @@ final class UiModeManagerService extends SystemService { @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { setAttentionModeThemeOverlay_enforcePermission(); - enforceValidCallingUser(UserHandle.getCallingUserId()); + enforceCurrentUserIfVisibleBackgroundEnabled(UserHandle.getCallingUserId()); synchronized (mLock) { if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) { @@ -1005,7 +1004,7 @@ final class UiModeManagerService extends SystemService { 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 @@ final class UiModeManagerService extends SystemService { return; } final int user = UserHandle.getCallingUserId(); - enforceValidCallingUser(user); + enforceCurrentUserIfVisibleBackgroundEnabled(user); final long ident = Binder.clearCallingIdentity(); try { @@ -1094,7 +1093,7 @@ final class UiModeManagerService extends SystemService { return; } final int user = UserHandle.getCallingUserId(); - enforceValidCallingUser(user); + enforceCurrentUserIfVisibleBackgroundEnabled(user); final long ident = Binder.clearCallingIdentity(); try { @@ -1116,7 +1115,7 @@ final class UiModeManagerService extends SystemService { assertLegit(callingPackage); assertSingleProjectionType(projectionType); enforceProjectionTypePermissions(projectionType); - enforceValidCallingUser(getCallingUserId()); + enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId()); synchronized (mLock) { if (mProjectionHolders == null) { @@ -1162,7 +1161,7 @@ final class UiModeManagerService extends SystemService { assertLegit(callingPackage); assertSingleProjectionType(projectionType); enforceProjectionTypePermissions(projectionType); - enforceValidCallingUser(getCallingUserId()); + enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId()); return releaseProjectionUnchecked(projectionType, callingPackage); } @@ -1204,7 +1203,7 @@ final class UiModeManagerService extends SystemService { return; } - enforceValidCallingUser(getCallingUserId()); + enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId()); synchronized (mLock) { if (mProjectionListeners == null) { @@ -1253,32 +1252,6 @@ final class UiModeManagerService extends SystemService { } }; - // 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 54a741060bbe..871c32086a7f 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 @@ public class ActivityManagerService extends IActivityManager.Stub /** * 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 @@ public class ActivityManagerService extends IActivityManager.Stub @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 6bb56c9edcab..e885c14afddb 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 @@ final class CoreSettingsObserver extends ContentObserver { 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 b186eaacab74..262c76e4a4d7 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.Arrays; 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 @@ class UserController implements Handler.Callback { 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 @@ class UserController implements Handler.Callback { // 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 @@ class UserController implements Handler.Callback { 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 @@ class UserController implements Handler.Callback { 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,54 +2616,32 @@ class UserController implements Handler.Callback { @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)); - } - } else { - sendUserSwitchCompleteMessage.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"); + 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 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 await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) { + if (condition) { + conditionalStep.accept(nextStep); + } else { + nextStep.run(); + } } private void moveUserToForeground(UserState uss, int newUserId) { @@ -4100,45 +4088,29 @@ class UserController implements Handler.Callback { 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 @@ class UserController implements Handler.Callback { 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 835fb72e524a..d70bd8b17ddf 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 @@ class InputSettingsObserver extends ContentObserver { (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 @@ class InputSettingsObserver extends ContentObserver { } 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 @@ class InputSettingsObserver extends ContentObserver { 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 1e7c97f9e551..5dd461dda061 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 @@ interface NativeInputManagerService { 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 @@ interface NativeInputManagerService { 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 01c108bd5067..494ea7714ff9 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.CurrentTimeMillisLong; 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 @@ abstract class NetworkTimeHelper { 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 df45a6ec985c..177eefb2ef2a 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 @@ public class SystemEmergencyHelper extends EmergencyHelper { 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 @@ public class SystemEmergencyHelper extends EmergencyHelper { 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 621c090d37b8..48d24f2e14dd 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 @@ public final class MediaProjectionManagerService extends SystemService /** * 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 @@ public final class MediaProjectionManagerService extends SystemService == 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 c75622cb9bbc..21a6df203015 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 68026ea9094a..e3d71e4998be 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.AnyThread; 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 @@ import java.util.function.Consumer; 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 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } }; - 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)); + } + }; + + // 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()); - rollbackManager.commitRollback(rollback.getRollbackId(), - Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender()); + 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 3d5b2732e948..6009b4a70e3e 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 class ActivityTaskManagerService extends IActivityTaskManager.Stub { 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 8f5612c61e1c..84072e26761a 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 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } 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 655a6fb52e55..0a47522f7df6 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 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { 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 30d6f0a46bae..32fe303b9e90 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 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // 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 155e73c53819..d2493c513b5f 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 nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, } 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 @@ static const JNINativeMethod gInputManagerMethods[] = { {"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 ab459df1cdf6..3b334ec69231 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.BinderInternal; 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 @@ public final class SystemServer implements Dumpable { 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 @@ public final class SystemServer implements Dumpable { 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 @@ public final class SystemServer implements Dumpable { 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 a25621a8975f..390eb937fe25 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.any; 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.Looper; 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { // 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { // 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { // 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { 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 @@ public class UserControllerTest { @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 689b241f0faa..abc9ce3fdc36 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.verifyZeroInteractions; 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.os.test.TestLooper; 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 @@ import java.util.concurrent.TimeUnit; /** * 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 @@ public class MediaProjectionManagerServiceTest { } }; - 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 @@ public class MediaProjectionManagerServiceTest { @Mock private KeyguardManager mKeyguardManager; @Mock + AppOpsManager mAppOpsManager; + @Mock private IMediaProjectionWatcherCallback mWatcherCallback; @Mock private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; @@ -185,10 +193,9 @@ public class MediaProjectionManagerServiceTest { 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 @@ public class MediaProjectionManagerServiceTest { 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 96ddf8079e17..b8f9767b5512 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 957b5e04fef6..ae0c6e551246 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 @@ public class RootWindowContainerTests extends WindowTestsBase { 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 @@ public class RootWindowContainerTests extends WindowTestsBase { } 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 66a20ae28f39..50e3a0e4a79d 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl @@ -34,4 +34,12 @@ oneway interface ISatelliteModemStateCallback { * @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 90dae3be058c..4eefaaca71f4 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -19,6 +19,7 @@ package android.telephony.satellite; 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 @@ public final class SatelliteManager { 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 423a7859dd6b..13af4694389b 100644 --- a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java @@ -45,4 +45,13 @@ public interface SatelliteModemStateCallback { */ @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 2a82d5f9bd7c..351ec4635977 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 @@ class InputManagerServiceTests { 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 index bbeb18dfbecd..bbeb18dfbecd 100644 --- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java +++ b/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 index d78f78b1cb0e..d78f78b1cb0e 100644 --- a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java +++ b/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 e841d9ea0880..cfb2645dee0d 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 android.util.proto.ProtoInputStream; 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.runner.RunWith; 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 @@ public class PerfettoProtoLogImplTest { 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 @@ public class PerfettoProtoLogImplTest { viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(), TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); } + + waitDataSourceIsAvailable(); } @Before @@ -862,6 +870,54 @@ public class PerfettoProtoLogImplTest { .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 e1bdd777dc5f..a3d03a8278ed 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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 @@ public class ProtoLogConfigurationServiceTest { @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 f68ae2c7e249..fc4a909e761f 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 @@ public class SharedConnectivityManager { } } } + + Executor getExecutor() { + return mExecutor; + } } private ISharedConnectivityService mService; @@ -188,7 +192,7 @@ public class SharedConnectivityManager { 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 @@ public class SharedConnectivityManager { 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); + } + } + ); } /** |