diff options
335 files changed, 10268 insertions, 2613 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 45e33ce4b6e9..97d28d1a6506 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -401,6 +401,7 @@ java_aconfig_library { min_sdk_version: "30", apex_available: [ "//apex_available:platform", + "com.android.tethering", "com.android.wifi", ], defaults: ["framework-minus-apex-aconfig-java-defaults"], @@ -1215,6 +1216,7 @@ java_aconfig_library { // DevicePolicy aconfig_declarations { name: "device_policy_aconfig_flags", + exportable: true, package: "android.app.admin.flags", container: "system", srcs: [ @@ -1233,6 +1235,7 @@ java_aconfig_library { aconfig_declarations: "device_policy_aconfig_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], min_sdk_version: "30", + mode: "exported", apex_available: [ "//apex_available:platform", "com.android.permission", diff --git a/core/api/current.txt b/core/api/current.txt index cfbf9e55c7dc..ca4b2fae2f99 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -98,6 +98,7 @@ package android { field public static final String DUMP = "android.permission.DUMP"; field public static final String ENFORCE_UPDATE_OWNERSHIP = "android.permission.ENFORCE_UPDATE_OWNERSHIP"; field public static final String EXECUTE_APP_ACTION = "android.permission.EXECUTE_APP_ACTION"; + field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS = "android.permission.EXECUTE_APP_FUNCTIONS"; field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR"; field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST"; field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE"; @@ -8892,8 +8893,8 @@ package android.app.appfunctions { } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); - method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); + method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e1d8fb11efb3..f0f0fc98881e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -150,7 +150,6 @@ package android { field @FlaggedApi("com.android.window.flags.untrusted_embedding_any_app_permission") public static final String EMBED_ANY_APP_IN_UNTRUSTED_MODE = "android.permission.EMBED_ANY_APP_IN_UNTRUSTED_MODE"; field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES"; field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED"; - field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS = "android.permission.EXECUTE_APP_FUNCTIONS"; field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS_TRUSTED = "android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED"; field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS"; field public static final String FORCE_BACK = "android.permission.FORCE_BACK"; @@ -523,7 +522,6 @@ package android { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence_module") public static final int config_defaultOnDeviceIntelligenceDeviceConfigNamespace; field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence_module") public static final int config_defaultOnDeviceIntelligenceService; field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence_module") public static final int config_defaultOnDeviceSandboxedInferenceService; - field @FlaggedApi("android.permission.flags.cross_user_role_platform_api_enabled") public static final int config_defaultReservedForTestingProfileGroupExclusivity; field @FlaggedApi("android.permission.flags.retail_demo_role_enabled") public static final int config_defaultRetailDemo = 17039432; // 0x1040048 field public static final int config_defaultSms = 17039396; // 0x1040024 field @FlaggedApi("android.permission.flags.wallet_role_enabled") public static final int config_defaultWallet = 17039433; // 0x1040049 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 03ef669c0675..fee8cdb1ce51 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -5782,8 +5782,7 @@ public class Activity extends ContextThemeWrapper } if (!getAttributionSource().getRenouncedPermissions().isEmpty()) { - final int permissionCount = permissions.length; - for (int i = 0; i < permissionCount; i++) { + for (int i = 0; i < permissions.length; i++) { if (getAttributionSource().getRenouncedPermissions().contains(permissions[i])) { throw new IllegalArgumentException("Cannot request renounced permission: " + permissions[i]); @@ -5791,13 +5790,55 @@ public class Activity extends ContextThemeWrapper } } - PackageManager packageManager = getDeviceId() == deviceId ? getPackageManager() - : createDeviceContext(deviceId).getPackageManager(); + final Context context = getDeviceId() == deviceId ? this : createDeviceContext(deviceId); + if (Flags.permissionRequestShortCircuitEnabled()) { + int[] permissionsState = getPermissionRequestStates(context, permissions); + boolean hasRequestablePermission = false; + for (int i = 0; i < permissionsState.length; i++) { + if (permissionsState[i] == Context.PERMISSION_REQUEST_STATE_REQUESTABLE) { + hasRequestablePermission = true; + break; + } + } + // If none of the permissions is requestable, finish the request here. + if (!hasRequestablePermission) { + mHasCurrentPermissionsRequest = true; + Log.v(TAG, "No requestable permission in the request."); + int[] results = new int[permissionsState.length]; + for (int i = 0; i < permissionsState.length; i++) { + if (permissionsState[i] == Context.PERMISSION_REQUEST_STATE_GRANTED) { + results[i] = PackageManager.PERMISSION_GRANTED; + } else { + results[i] = PackageManager.PERMISSION_DENIED; + } + } + // Currently permission request result is passed to the client app asynchronously + // in onRequestPermissionsResult, lets keep async behavior here as well. + mHandler.post(() -> { + mHasCurrentPermissionsRequest = false; + onRequestPermissionsResult(requestCode, permissions, results, deviceId); + }); + return; + } + } + + final PackageManager packageManager = context.getPackageManager(); final Intent intent = packageManager.buildRequestPermissionsIntent(permissions); startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null); mHasCurrentPermissionsRequest = true; } + @NonNull + private int[] getPermissionRequestStates(@NonNull Context deviceContext, + @NonNull String[] permissions) { + final int size = permissions.length; + int[] results = new int[size]; + for (int i = 0; i < size; i++) { + results[i] = deviceContext.getPermissionRequestState(permissions[i]); + } + return results; + } + /** * Callback for the result from requesting permissions. This method * is invoked for every call on {@link #requestPermissions} diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index ec173338f262..717a2acb4b4a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7098,12 +7098,9 @@ public final class ActivityThread extends ClientTransactionHandler System.runFinalization(); System.gc(); } - if (dhd.dumpBitmaps != null) { - Bitmap.dumpAll(dhd.dumpBitmaps); - } try (ParcelFileDescriptor fd = dhd.fd) { if (dhd.managed) { - Debug.dumpHprofData(dhd.path, fd.getFileDescriptor()); + Debug.dumpHprofData(dhd.path, fd.getFileDescriptor(), dhd.dumpBitmaps); } else if (dhd.mallocInfo) { Debug.dumpNativeMallocInfo(fd.getFileDescriptor()); } else { @@ -7128,9 +7125,6 @@ public final class ActivityThread extends ClientTransactionHandler if (dhd.finishCallback != null) { dhd.finishCallback.sendResult(null); } - if (dhd.dumpBitmaps != null) { - Bitmap.dumpAll(null); // clear dump - } } final void handleDispatchPackageBroadcast(int cmd, String[] packages) { diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index dcbdc2348fbc..d8aa8b3df622 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2366,7 +2366,11 @@ class ContextImpl extends Context { Log.v(TAG, "Treating renounced permission " + permission + " as denied"); return PERMISSION_DENIED; } + int deviceId = resolveDeviceIdForPermissionCheck(permission); + return PermissionManager.checkPermission(permission, pid, uid, deviceId); + } + private int resolveDeviceIdForPermissionCheck(String permission) { // When checking a device-aware permission on a remote device, if the permission is CAMERA // or RECORD_AUDIO we need to check remote device's corresponding capability. If the remote // device doesn't have capability fall back to checking permission on the default device. @@ -2387,9 +2391,9 @@ class ContextImpl extends Context { VirtualDevice virtualDevice = virtualDeviceManager.getVirtualDevice(deviceId); if (virtualDevice != null) { if ((Objects.equals(permission, Manifest.permission.RECORD_AUDIO) - && !virtualDevice.hasCustomAudioInputSupport()) + && !virtualDevice.hasCustomAudioInputSupport()) || (Objects.equals(permission, Manifest.permission.CAMERA) - && !virtualDevice.hasCustomCameraSupport())) { + && !virtualDevice.hasCustomCameraSupport())) { deviceId = Context.DEVICE_ID_DEFAULT; } } else { @@ -2400,8 +2404,7 @@ class ContextImpl extends Context { } } } - - return PermissionManager.checkPermission(permission, pid, uid, deviceId); + return deviceId; } /** @hide */ @@ -2503,6 +2506,16 @@ class ContextImpl extends Context { message); } + /** @hide */ + @Override + public int getPermissionRequestState(String permission) { + Objects.requireNonNull(permission, "Permission name can't be null"); + int deviceId = resolveDeviceIdForPermissionCheck(permission); + PermissionManager permissionManager = getSystemService(PermissionManager.class); + return permissionManager.getPermissionRequestState(getOpPackageName(), permission, + deviceId); + } + @Override public void grantUriPermission(String toPackage, Uri uri, int modeFlags) { try { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index aa2ada5372af..17638ee76dba 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3323,6 +3323,18 @@ public class Notification implements Parcelable } /** + * Make sure this String is safe to put into a bundle. + * @hide + */ + public static String safeString(String str) { + if (str == null) return str; + if (str.length() > MAX_CHARSEQUENCE_LENGTH) { + str = str.substring(0, MAX_CHARSEQUENCE_LENGTH); + } + return str; + } + + /** * Make sure this CharSequence is safe to put into a bundle, which basically * means it had better not be some custom Parcelable implementation. * @hide @@ -5051,7 +5063,7 @@ public class Notification implements Parcelable @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) @NonNull public Builder setShortCriticalText(@Nullable String shortCriticalText) { - mN.extras.putString(EXTRA_SHORT_CRITICAL_TEXT, shortCriticalText); + mN.extras.putString(EXTRA_SHORT_CRITICAL_TEXT, safeString(shortCriticalText)); return this; } diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index c72c4c8feb71..5567c085d205 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -1356,7 +1356,7 @@ public class PropertyInvalidatedCache<Query, Result> { @Nullable QueryHandler<Query, Result> computer) { mPropertyName = createPropertyName(args.mModule, args.mApi); mCacheName = cacheName; - mCacheNullResults = args.mCacheNulls && Flags.picCacheNulls(); + mCacheNullResults = args.mCacheNulls; mNonce = getNonceHandler(mPropertyName); mMaxEntries = args.mMaxEntries; mCache = new CacheMap<>(args.mIsolateUids, args.mTestMode); diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java index 7d21cbf955d9..258ce06bffc3 100644 --- a/core/java/android/app/compat/ChangeIdStateCache.java +++ b/core/java/android/app/compat/ChangeIdStateCache.java @@ -16,8 +16,6 @@ package android.app.compat; -import static android.app.PropertyInvalidatedCache.createSystemCacheKey; - import android.annotation.NonNull; import android.app.PropertyInvalidatedCache; import android.content.Context; @@ -34,7 +32,10 @@ import com.android.internal.compat.IPlatformCompat; @android.ravenwood.annotation.RavenwoodKeepWholeClass public final class ChangeIdStateCache extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> { - private static final String CACHE_KEY = createSystemCacheKey("is_compat_change_enabled"); + + private static final String CACHE_MODULE = PropertyInvalidatedCache.MODULE_SYSTEM; + private static final String CACHE_API = "is_compat_change_enabled"; + private static final int MAX_ENTRIES = 2048; private static boolean sDisabled = getDefaultDisabled(); private volatile IPlatformCompat mPlatformCompat; @@ -51,7 +52,12 @@ public final class ChangeIdStateCache /** @hide */ public ChangeIdStateCache() { - super(MAX_ENTRIES, CACHE_KEY); + super(new PropertyInvalidatedCache.Args(CACHE_MODULE) + .maxEntries(MAX_ENTRIES) + .isolateUids(false) + .cacheNulls(false) + .api(CACHE_API), + CACHE_API, null); } /** @@ -72,7 +78,7 @@ public final class ChangeIdStateCache */ public static void invalidate() { if (!sDisabled) { - PropertyInvalidatedCache.invalidateCache(CACHE_KEY); + PropertyInvalidatedCache.invalidateCache(CACHE_MODULE, CACHE_API); } } diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index aee1cd9b4760..a5b58f968c27 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -16,8 +16,10 @@ package android.app.supervision; +import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; +import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.os.RemoteException; @@ -32,9 +34,7 @@ public class SupervisionManager { private final Context mContext; private final ISupervisionManager mService; - /** - * @hide - */ + /** @hide */ @UnsupportedAppUsage public SupervisionManager(Context context, ISupervisionManager service) { mContext = context; @@ -48,8 +48,23 @@ public class SupervisionManager { */ @UserHandleAware public boolean isSupervisionEnabled() { + return isSupervisionEnabledForUser(mContext.getUserId()); + } + + /** + * Returns whether the device is supervised. + * + * <p>The caller must be from the same user as the target or hold the {@link + * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. + * + * @hide + */ + @RequiresPermission( + value = android.Manifest.permission.INTERACT_ACROSS_USERS, + conditional = true) + public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { try { - return mService.isSupervisionEnabledForUser(mContext.getUserId()); + return mService.isSupervisionEnabledForUser(userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 6ec6a62ff639..7abf5600d659 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -788,6 +788,40 @@ public abstract class Context { public static final int RECEIVER_NOT_EXPORTED = 0x4; /** + * The permission is granted. + * + * @hide + */ + public static final int PERMISSION_REQUEST_STATE_GRANTED = 0; + + /** + * The permission isn't granted, but apps can request the permission. When the app request + * the permission, user will be prompted with permission dialog to grant or deny the request. + * + * @hide + */ + public static final int PERMISSION_REQUEST_STATE_REQUESTABLE = 1; + + /** + * The permission is denied, and shouldn't be requested by apps. Permission request + * will be automatically denied by the system, preventing the permission dialog from being + * displayed to the user. + * + * @hide + */ + public static final int PERMISSION_REQUEST_STATE_UNREQUESTABLE = 2; + + + /** @hide */ + @IntDef(prefix = { "PERMISSION_REQUEST_STATE_" }, value = { + PERMISSION_REQUEST_STATE_GRANTED, + PERMISSION_REQUEST_STATE_REQUESTABLE, + PERMISSION_REQUEST_STATE_UNREQUESTABLE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PermissionRequestState {} + + /** * Returns an AssetManager instance for the application's package. * <p> * <strong>Note:</strong> Implementations of this method should return @@ -6989,6 +7023,31 @@ public abstract class Context { @NonNull @PermissionName String permission, @Nullable String message); /** + * Returns the permission request state for a given runtime permission. This method provides a + * streamlined mechanism for applications to determine whether a permission can be + * requested (i.e. whether the user will be prompted with a permission dialog). + * + * <p>Traditionally, determining if a permission has been permanently denied (unrequestable) + * required applications to initiate a permission request and subsequently analyze the result + * of {@link android.app.Activity#shouldShowRequestPermissionRationale} in conjunction with the + * grant result within the {@link android.app.Activity#onRequestPermissionsResult} callback. + * + * @param permission The name of the permission. + * + * @return The current request state of the specified permission, represented by one of the + * following constants: {@link PermissionRequestState#PERMISSION_REQUEST_STATE_GRANTED}, + * {@link PermissionRequestState#PERMISSION_REQUEST_STATE_REQUESTABLE}, or + * {@link PermissionRequestState#PERMISSION_REQUEST_STATE_UNREQUESTABLE}. + * + * @hide + */ + @CheckResult + @PermissionRequestState + public int getPermissionRequestState(@NonNull String permission) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * Grant permission to access a specific Uri to another package, regardless * of whether that package has general permission to access the Uri's * content provider. This can be used to grant specific, temporary diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 413eb9886392..a146807b9fcf 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -1012,6 +1012,12 @@ public class ContextWrapper extends Context { mBase.enforceCallingOrSelfPermission(permission, message); } + /** @hide */ + @Override + public int getPermissionRequestState(String permission) { + return mBase.getPermissionRequestState(permission); + } + @Override public void grantUriPermission(String toPackage, Uri uri, int modeFlags) { mBase.grantUriPermission(toPackage, uri, modeFlags); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 4b579e7db9f8..219b20428d7a 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2322,10 +2322,10 @@ public class PackageParser { } else if (tagName.equals(TAG_ADOPT_PERMISSIONS)) { sa = res.obtainAttributes(parser, - com.android.internal.R.styleable.AndroidManifestOriginalPackage); + com.android.internal.R.styleable.AndroidManifestAdoptPermissions); String name = sa.getNonConfigurationString( - com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0); + com.android.internal.R.styleable.AndroidManifestAdoptPermissions_name, 0); sa.recycle(); diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index b76b2271fe57..44f80c819e83 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -39,6 +39,7 @@ interface IContextHubEndpoint { * @throws IllegalArgumentException If the HubEndpointInfo is not valid. * @throws IllegalStateException If there are too many opened sessions. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") int openSession(in HubEndpointInfo destination, in @nullable String serviceDescriptor); /** @@ -49,6 +50,7 @@ interface IContextHubEndpoint { * * @throws IllegalStateException If the session wasn't opened. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") void closeSession(int sessionId, int reason); /** @@ -60,11 +62,13 @@ interface IContextHubEndpoint { * * @throws IllegalStateException If the session wasn't opened. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") void openSessionRequestComplete(int sessionId); /** * Unregister this endpoint from the HAL, invalidate the EndpointInfo previously assigned. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") void unregister(); /** @@ -76,6 +80,7 @@ interface IContextHubEndpoint { * @param transactionCallback Nullable. If the hub message requires a reply, the transactionCallback * will be set to non-null. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessage(int sessionId, in HubMessage message, in @nullable IContextHubTransactionCallback transactionCallback); @@ -87,5 +92,6 @@ interface IContextHubEndpoint { * @param messageSeqNumber The message sequence number, this should match a previously received HubMessage. * @param errorCode The message delivery status detail. */ + @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode); } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index aaa78aa0916a..313bad50e88e 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -196,4 +196,11 @@ flag { namespace: "input" description: "Allows the user to disable pointer acceleration for mouse and touchpads." bug: "349006858" -}
\ No newline at end of file +} + +flag { + name: "mouse_scrolling_acceleration" + namespace: "input" + description: "Allows the user to disable input scrolling acceleration for mouse." + bug: "383555305" +} diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index d9888ad6cd8d..1e0cc94612dd 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -754,6 +754,7 @@ public final class ContextHubManager { * @param executor the executor to invoke callbacks for this client * @return the callback interface */ + @FlaggedApi(Flags.FLAG_OFFLOAD_API) private IContextHubEndpointDiscoveryCallback createDiscoveryCallback( IHubEndpointDiscoveryCallback callback, Executor executor, @@ -767,21 +768,9 @@ public final class ContextHubManager { } executor.execute( () -> { - // TODO(b/380293951): Refactor List<HubDiscoveryInfo> discoveryList = - new ArrayList<>(hubEndpointInfoList.length); - for (HubEndpointInfo info : hubEndpointInfoList) { - if (serviceDescriptor != null) { - for (HubServiceInfo sInfo : info.getServiceInfoCollection()) { - if (sInfo.getServiceDescriptor() - .equals(serviceDescriptor)) { - discoveryList.add(new HubDiscoveryInfo(info, sInfo)); - } - } - } else { - discoveryList.add(new HubDiscoveryInfo(info)); - } - } + getMatchingEndpointDiscoveryList( + hubEndpointInfoList, serviceDescriptor); if (discoveryList.isEmpty()) { Log.w(TAG, "onEndpointsStarted: no matching service descriptor"); } else { @@ -799,19 +788,8 @@ public final class ContextHubManager { executor.execute( () -> { List<HubDiscoveryInfo> discoveryList = - new ArrayList<>(hubEndpointInfoList.length); - for (HubEndpointInfo info : hubEndpointInfoList) { - if (serviceDescriptor != null) { - for (HubServiceInfo sInfo : info.getServiceInfoCollection()) { - if (sInfo.getServiceDescriptor() - .equals(serviceDescriptor)) { - discoveryList.add(new HubDiscoveryInfo(info, sInfo)); - } - } - } else { - discoveryList.add(new HubDiscoveryInfo(info)); - } - } + getMatchingEndpointDiscoveryList( + hubEndpointInfoList, serviceDescriptor); if (discoveryList.isEmpty()) { Log.w(TAG, "onEndpointsStopped: no matching service descriptor"); } else { @@ -823,6 +801,34 @@ public final class ContextHubManager { } /** + * Generates a list of matching endpoint discovery info, given the list and an (optional) + * service descriptor. If service descriptor is null, all endpoints are added to the filtered + * output list. + * + * @param hubEndpointInfoList The hub endpoints to filter. + * @param serviceDescriptor The optional service descriptor to match, null if adding all + * endpoints. + * @return The list of filtered HubDiscoveryInfo which matches the serviceDescriptor. + */ + @FlaggedApi(Flags.FLAG_OFFLOAD_API) + private List<HubDiscoveryInfo> getMatchingEndpointDiscoveryList( + HubEndpointInfo[] hubEndpointInfoList, @Nullable String serviceDescriptor) { + List<HubDiscoveryInfo> discoveryList = new ArrayList<>(hubEndpointInfoList.length); + for (HubEndpointInfo info : hubEndpointInfoList) { + if (serviceDescriptor != null) { + for (HubServiceInfo sInfo : info.getServiceInfoCollection()) { + if (sInfo.getServiceDescriptor().equals(serviceDescriptor)) { + discoveryList.add(new HubDiscoveryInfo(info, sInfo)); + } + } + } else { + discoveryList.add(new HubDiscoveryInfo(info)); + } + } + return discoveryList; + } + + /** * Equivalent to {@link #registerEndpointDiscoveryCallback(long, IHubEndpointDiscoveryCallback, * Executor)} with the default executor in the main thread. */ diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index ef1e6c9405f3..2bc6ab5a18e9 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.app.AppGlobals; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.graphics.Bitmap; import android.util.Log; import com.android.internal.util.FastPrintWriter; @@ -2139,6 +2140,47 @@ public final class Debug } /** + * Like dumpHprofData(String), but takes an argument of bitmapFormat, + * which can be png, jpg, webp, or null (no bitmaps in heapdump). + * + * @hide + */ + public static void dumpHprofData(String fileName, String bitmapFormat) + throws IOException { + try { + if (bitmapFormat != null) { + Bitmap.dumpAll(bitmapFormat); + } + VMDebug.dumpHprofData(fileName); + } finally { + if (bitmapFormat != null) { + Bitmap.dumpAll(null); // clear dump data + } + } + } + + /** + * Like dumpHprofData(String, FileDescriptor), but takes an argument + * of bitmapFormat, which can be png, jpg, webp, or null (no bitmaps + * in heapdump). + * + * @hide + */ + public static void dumpHprofData(String fileName, FileDescriptor fd, + String bitmapFormat) throws IOException { + try { + if (bitmapFormat != null) { + Bitmap.dumpAll(bitmapFormat); + } + VMDebug.dumpHprofData(fileName, fd); + } finally { + if (bitmapFormat != null) { + Bitmap.dumpAll(null); // clear dump data + } + } + } + + /** * Collect "hprof" and send it to DDMS. This may cause a GC. * * @throws UnsupportedOperationException if the VM was built without diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index b9f2cfcd8ca8..132805da7c94 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -4197,12 +4197,21 @@ public class UserManager { android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) private boolean hasUserRestrictionForUser(@NonNull @UserRestrictionKey String restrictionKey, - @UserIdInt int userId) { - try { - return mService.hasUserRestriction(restrictionKey, userId); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } + @NonNull @UserIdInt int userId) { + return getUserRestrictionFromQuery(new Pair(restrictionKey, userId)); + } + + /** @hide */ + @CachedProperty() + private boolean getUserRestrictionFromQuery(@NonNull Pair<String, Integer> restrictionPerUser) { + return UserManagerCache.getUserRestrictionFromQuery( + (Pair<String, Integer> q) -> mService.hasUserRestriction(q.first, q.second), + restrictionPerUser); + } + + /** @hide */ + public static final void invalidateUserRestriction() { + UserManagerCache.invalidateUserRestrictionFromQuery(); } /** @@ -6477,6 +6486,7 @@ public class UserManager { UserManagerCache.invalidateProfileParent(); } invalidateEnabledProfileIds(); + invalidateUserRestriction(); } /** diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 55011e52724c..b37581260bb1 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -108,6 +108,8 @@ interface IPermissionManager { int checkUidPermission(int uid, String permissionName, int deviceId); Map<String, PermissionState> getAllPermissionStates(String packageName, String persistentDeviceId, int userId); + + int getPermissionRequestState(String packageName, String permissionName, int deviceId); } /** diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 2473de4ff6d7..bdf8d23438df 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1742,6 +1742,16 @@ public final class PermissionManager { } } + private static int getPermissionRequestStateUncached(String packageName, String permission, + int deviceId) { + try { + return AppGlobals.getPermissionManager().getPermissionRequestState( + packageName, permission, deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Identifies a permission query. * @@ -1795,6 +1805,46 @@ public final class PermissionManager { } } + private static final class PermissionRequestStateQuery { + final String mPackageName; + final String mPermission; + final int mDeviceId; + + PermissionRequestStateQuery(@NonNull String packageName, @NonNull String permission, + int deviceId) { + mPackageName = packageName; + mPermission = permission; + mDeviceId = deviceId; + } + + @Override + public String toString() { + return TextUtils.formatSimple("PermissionRequestStateQuery(package=\"%s\"," + + " permission=\"%s\", " + "deviceId=%d)", + mPackageName, mPermission, mDeviceId); + } + + @Override + public int hashCode() { + return Objects.hash(mPackageName, mPermission, mDeviceId); + } + + @Override + public boolean equals(@Nullable Object rval) { + if (rval == null) { + return false; + } + PermissionRequestStateQuery other; + try { + other = (PermissionRequestStateQuery) rval; + } catch (ClassCastException ex) { + return false; + } + return mDeviceId == other.mDeviceId && Objects.equals(mPackageName, other.mPackageName) + && Objects.equals(mPermission, other.mPermission); + } + } + // The legacy system property "package_info" had two purposes: to invalidate PIC caches and to // signal that package information, and therefore permissions, might have changed. // AudioSystem is the only client of the signaling behavior. The "separate permissions @@ -1842,10 +1892,30 @@ public final class PermissionManager { }; /** @hide */ + private static final PropertyInvalidatedCache<PermissionRequestStateQuery, Integer> + sPermissionRequestStateCache = + new PropertyInvalidatedCache<>( + 512, CACHE_KEY_PACKAGE_INFO_CACHE, "getPermissionRequestState") { + @Override + public Integer recompute(PermissionRequestStateQuery query) { + return getPermissionRequestStateUncached(query.mPackageName, query.mPermission, + query.mDeviceId); + } + }; + + /** @hide */ public static int checkPermission(@Nullable String permission, int pid, int uid, int deviceId) { return sPermissionCache.query(new PermissionQuery(permission, pid, uid, deviceId)); } + /** @hide */ + @Context.PermissionRequestState + public int getPermissionRequestState(@NonNull String packageName, @NonNull String permission, + int deviceId) { + return sPermissionRequestStateCache.query( + new PermissionRequestStateQuery(packageName, permission, deviceId)); + } + /** * Gets the permission states for requested package and persistent device. * <p> diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index af96ccfee787..07b9f5242b42 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -467,15 +467,6 @@ flag { } flag { - name: "cross_user_role_platform_api_enabled" - is_exported: true - is_fixed_read_only: true - namespace: "permissions" - description: "Enable cross-user roles platform API" - bug: "367732307" -} - -flag { name: "rate_limit_batched_note_op_async_callbacks_enabled" is_fixed_read_only: true is_exported: true diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c14854023b39..cf0e90fb43ce 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13710,6 +13710,14 @@ public final class Settings { "render_shadows_in_compositor"; /** + * Policy to be used for the display shade when connected to an external display. + * @hide + */ + @Readable + public static final String DEVELOPMENT_SHADE_DISPLAY_AWARENESS = + "shade_display_awareness"; + + /** * Path to the WindowManager display settings file. If unset, the default file path will * be used. * diff --git a/core/java/android/service/autofill/augmented/FillWindow.java b/core/java/android/service/autofill/augmented/FillWindow.java index d42ec7c71830..0ce040d7f862 100644 --- a/core/java/android/service/autofill/augmented/FillWindow.java +++ b/core/java/android/service/autofill/augmented/FillWindow.java @@ -17,7 +17,6 @@ package android.service.autofill.augmented; import static android.service.autofill.augmented.AugmentedAutofillService.sDebug; import static android.service.autofill.augmented.AugmentedAutofillService.sVerbose; -import static android.service.autofill.Flags.addAccessibilityTitleForAugmentedAutofillDropdown; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; @@ -37,7 +36,6 @@ import android.view.WindowManager; import android.view.autofill.IAutofillWindowPresenter; import com.android.internal.annotations.GuardedBy; -import com.android.internal.R; import dalvik.system.CloseGuard; @@ -210,12 +208,6 @@ public final class FillWindow implements AutoCloseable { if (mWm != null && mFillView != null) { try { p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; - if (addAccessibilityTitleForAugmentedAutofillDropdown()) { - p.accessibilityTitle = - mFillView - .getContext() - .getString(R.string.autofill_picker_accessibility_title); - } if (!mShowing) { mWm.addView(mFillView, p); mShowing = true; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 609f1ef06612..1d27574eca8c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4435,7 +4435,8 @@ public final class ViewRootImpl implements ViewParent, // merged with a sync group or BLASTBufferQueue before making it to this point // But better a one or two frame flicker than steady-state broken from dropping // whatever is in this transaction - mPendingTransaction.apply(); + // apply immediately with bbq apply token + mergeWithNextTransaction(mPendingTransaction, 0); mHasPendingTransactions = false; } mSyncBuffer = false; @@ -5501,7 +5502,8 @@ public final class ViewRootImpl implements ViewParent, Log.d(mTag, "Pending transaction will not be applied in sync with a draw due to " + logReason); } - pendingTransaction.apply(); + // apply immediately with bbq apply token + mergeWithNextTransaction(pendingTransaction, 0); } } /** diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 6caa20e29c17..1707e61b28e4 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -175,13 +175,6 @@ flag { } flag { - name: "enable_a11y_metrics" - namespace: "lse_desktop_experience" - description: "Whether to enable log collection for a11y actions in desktop windowing mode" - bug: "341319597" -} - -flag { name: "enable_caption_compat_inset_force_consumption" namespace: "lse_desktop_experience" description: "Enables force-consumption of caption bar insets for immersive apps in freeform" diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index c160b42f8b6b..5c08dc6be1a0 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -3133,9 +3133,9 @@ public class ParsingPackageUtils { private static ParseResult<ParsingPackage> parseAdoptPermissions(ParseInput input, ParsingPackage pkg, Resources res, XmlResourceParser parser) { - TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestOriginalPackage); + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestAdoptPermissions); try { - String name = nonConfigString(0, R.styleable.AndroidManifestOriginalPackage_name, sa); + String name = nonConfigString(0, R.styleable.AndroidManifestAdoptPermissions_name, sa); if (name != null) { pkg.addAdoptPermission(name); } diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java index 169a9e8585b0..31d9770f6ac4 100644 --- a/core/java/com/android/internal/widget/MessagingGroup.java +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -448,6 +448,17 @@ public class MessagingGroup extends NotificationOptimizedLinearLayout implements mSenderView.setVisibility(hidden ? GONE : VISIBLE); } + private void updateIconVisibility() { + if (Flags.notificationsRedesignTemplates() && !mIsInConversation) { + // We don't show any icon (other than the app icon) in the collapsed form. For + // conversations, keeping this container helps with aligning the message to the icon + // when collapsed, but the old messaging style already has this alignment built into + // the template like all other layouts. Conversations are special because we use the + // same base layout for both the collapsed and expanded views. + mMessagingIconContainer.setVisibility(mSingleLine ? GONE : VISIBLE); + } + } + @Override public boolean hasDifferentHeightWhenFirst() { return mCanHideSenderIfFirst && !mSingleLine && !TextUtils.isEmpty(mSenderName); @@ -703,6 +714,7 @@ public class MessagingGroup extends NotificationOptimizedLinearLayout implements updateMaxDisplayedLines(); updateClipRect(); updateSenderVisibility(); + updateIconVisibility(); } } @@ -716,13 +728,16 @@ public class MessagingGroup extends NotificationOptimizedLinearLayout implements * @param isInConversation is this in a conversation */ public void setIsInConversation(boolean isInConversation) { - if (Flags.notificationsRedesignTemplates()) { - // No alignment adjustments are necessary in the redesign, as the size of the icons - // in both conversations and old messaging notifications are the same. - return; - } if (mIsInConversation != isInConversation) { mIsInConversation = isInConversation; + + if (Flags.notificationsRedesignTemplates()) { + updateIconVisibility(); + // No other alignment adjustments are necessary in the redesign, as the size of the + // icons in both conversations and old messaging notifications are the same. + return; + } + MarginLayoutParams layoutParams = (MarginLayoutParams) mMessagingIconContainer.getLayoutParams(); layoutParams.width = mIsInConversation diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index 70dd10f2c371..5fa8125ced10 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -722,7 +722,7 @@ public interface AndroidPackage { * The names of packages to adopt ownership of permissions from, parsed under {@link * ParsingPackageUtils#TAG_ADOPT_PERMISSIONS}. * - * @see R.styleable#AndroidManifestOriginalPackage_name + * @see R.styleable#AndroidManifestAdoptPermissions_name * @hide */ @NonNull diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp index 91849c1514cc..15132db2a569 100644 --- a/core/jni/android_view_SurfaceControlActivePictureListener.cpp +++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp @@ -106,12 +106,11 @@ struct SurfaceControlActivePictureListener : public gui::BnActivePictureListener } status_t startListening() { - // TODO(b/337330263): Make SF multiple-listener capable - return SurfaceComposerClient::setActivePictureListener(this); + return SurfaceComposerClient::addActivePictureListener(this); } status_t stopListening() { - return SurfaceComposerClient::setActivePictureListener(nullptr); + return SurfaceComposerClient::removeActivePictureListener(this); } protected: diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 82cad8b3a477..4a948dd91fb0 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8307,20 +8307,21 @@ android:featureFlag="android.app.appfunctions.flags.enable_app_function_manager" android:protectionLevel="signature" /> - <!-- @SystemApi Allows a trusted application to perform actions on behalf of users inside of + <!-- Allows a trusted application to perform actions on behalf of users inside of applications with privacy guarantees from the system. <p>This permission is currently only granted to system packages in the {@link android.app.role.SYSTEM_UI_INTELLIGENCE} role which complies with privacy requirements outlined in the Android CDD section "9.8.6 Content Capture". <p>Apps are not able to opt-out from caller having this permission. <p>Protection level: internal|role + @SystemApi @hide - @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") --> + @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) --> <permission android:name="android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED" android:featureFlag="android.app.appfunctions.flags.enable_app_function_manager" android:protectionLevel="internal|role" /> - <!-- @SystemApi Allows an application to perform actions on behalf of users inside of + <!-- Allows an application to perform actions on behalf of users inside of applications. <p>This permission is currently only granted to preinstalled / system apps having the {@link android.app.role.ASSISTANT} role. @@ -8328,8 +8329,7 @@ limiting to only callers with {@link android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} instead. <p>Protection level: internal|role - @hide - @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") --> + @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) --> <permission android:name="android.permission.EXECUTE_APP_FUNCTIONS" android:featureFlag="android.app.appfunctions.flags.enable_app_function_manager" android:protectionLevel="internal|role" /> diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index 63872aff8dd0..fc727e1c72f5 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -20,7 +20,6 @@ android:id="@+id/notification_header" android:layout_width="match_parent" android:layout_height="@dimen/notification_2025_header_height" - android:layout_marginBottom="@dimen/notification_header_margin_bottom" android:clipChildren="false" android:gravity="center_vertical" android:orientation="horizontal" diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index a06d184fd147..8c6fd1dfc47e 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -2892,6 +2892,17 @@ <attr name="name" /> </declare-styleable> + <!-- Private tag to declare the package name that the permissions of this package + is based on. Only used for packages installed in the system image. If + given, the permissions from the other package will be propagated into the + new package. + + <p>This appears as a child tag of the root + {@link #AndroidManifest manifest} tag. --> + <declare-styleable name="AndroidManifestAdoptPermissions" parent="AndroidManifest"> + <attr name="name" /> + </declare-styleable> + <!-- The <code>processes</code> tag specifies the processes the application will run code in and optionally characteristics of those processes. This tag is optional; if not specified, components will simply run in the processes they specify. If supplied, diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 89184bcc3721..3d023c3e1d11 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2466,9 +2466,6 @@ <string name="config_systemCallStreaming" translatable="false"></string> <!-- The name of the package that will hold the default retail demo role. --> <string name="config_defaultRetailDemo" translatable="false"></string> - <!-- The name of the package that will hold the default reserved for testing profile group - exclusivity role. --> - <string name="config_defaultReservedForTestingProfileGroupExclusivity" translatable="false">android.app.rolemultiuser.cts.app</string> <!-- The component name of the wear service class that will be started by the system server. --> <string name="config_wearServiceComponent" translatable="false"></string> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 20ae29659783..666f1cf39fe3 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -506,4 +506,10 @@ <!-- Whether to allow TN scanning during satellite session. --> <bool name="config_satellite_allow_tn_scanning_during_satellite_session">true</bool> <java-symbol type="bool" name="config_satellite_allow_tn_scanning_during_satellite_session" /> + + <!-- List of integer tag Ids representing VZW satellite coverage. --> + <integer-array name="config_verizon_satellite_enabled_tagids"> + </integer-array> + <java-symbol type="array" name="config_verizon_satellite_enabled_tagids" /> + </resources> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 8259ce3c0aee..e82992b91783 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -151,9 +151,8 @@ <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER) @hide @SystemApi --> <public name="config_systemDependencyInstaller" /> - <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_CROSS_USER_ROLE_PLATFORM_API_ENABLED) - @hide @SystemApi --> - <public name="config_defaultReservedForTestingProfileGroupExclusivity" /> + <!-- @hide @SystemApi --> + <public name="removed_config_defaultReservedForTestingProfileGroupExclusivity" /> <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED) @hide @SystemApi --> <public name="config_systemVendorIntelligence" /> diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index bd273377984d..e9dfdd826572 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -16,7 +16,6 @@ package android.app; -import static android.app.Flags.FLAG_PIC_CACHE_NULLS; import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static android.app.PropertyInvalidatedCache.NONCE_UNSET; import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH; @@ -711,7 +710,6 @@ public class PropertyInvalidatedCacheTests { } } - @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS) @Test public void testCachingNulls() { TestCache cache = new TestCache(new Args(MODULE_TEST) diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java index bb8356f7aebe..fc04e6438ac6 100644 --- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -16,7 +16,6 @@ package android.os; -import static android.app.Flags.FLAG_PIC_CACHE_NULLS; import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static org.junit.Assert.assertEquals; @@ -511,7 +510,6 @@ public class IpcDataCacheTest { IpcDataCache.setTestMode(true); } - @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS) @Test public void testCachingNulls() { IpcDataCache.Config c = diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 2c542ec31a20..2398e7134b34 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -679,4 +679,8 @@ applications that come with the platform <permission name="android.permission.BATTERY_STATS"/> <permission name="android.permission.ENTER_TRADE_IN_MODE"/> </privapp-permissions> + + <privapp-permissions package="com.android.wm.shell"> + <permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> + </privapp-permissions> </permissions> diff --git a/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml b/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml new file mode 100644 index 000000000000..5a39c83b1e92 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="44dp" + android:layout_height="40dp" + android:importantForAccessibility="noHideDescendants"> + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:progressDrawable="@drawable/circular_progress" + android:layout_width="32dp" + android:layout_height="32dp" + android:indeterminate="false" + android:layout_marginHorizontal="6dp" + android:layout_marginVertical="4dp" + android:visibility="invisible"/> +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml index b734d2d81455..059e9e1a7895 100644 --- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml @@ -17,21 +17,14 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <FrameLayout + <ViewStub + android:id="@+id/stub_progress_bar_container" + android:inflatedId="@+id/inflatedProgressBarContainer" + android:layout="@layout/desktop_header_maximize_menu_button_progress_indicator_layout" android:layout_width="44dp" android:layout_height="40dp" - android:importantForAccessibility="noHideDescendants"> - <ProgressBar - android:id="@+id/progress_bar" - style="?android:attr/progressBarStyleHorizontal" - android:progressDrawable="@drawable/circular_progress" - android:layout_width="32dp" - android:layout_height="32dp" - android:indeterminate="false" - android:layout_marginHorizontal="6dp" - android:layout_marginVertical="4dp" - android:visibility="invisible"/> - </FrameLayout> + android:importantForAccessibility="noHideDescendants" + /> <ImageButton android:id="@+id/maximize_window" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt index 68c42d6a2648..06a55d3dbbd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt @@ -24,7 +24,6 @@ import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState @@ -60,13 +59,15 @@ fun isBrowserApp(context: Context, packageName: String, userId: Int): Boolean { * Returns intent if there is a browser application available to handle the uri. Otherwise, returns * null. */ -fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? { +fun getBrowserIntent(uri: Uri, packageManager: PackageManager, userId: Int): Intent? { val intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER) .setData(uri) .addFlags(FLAG_ACTIVITY_NEW_TASK) - // If there is no browser application available to handle intent, return null - val component = intent.resolveActivity(packageManager) ?: return null - intent.setComponent(component) + // If there is a browser application available to handle the intent, return the intent. + // Otherwise, return null. + val resolveInfo = packageManager.resolveActivityAsUser(intent, /* flags= */ 0, userId) + ?: return null + intent.setComponent(resolveInfo.componentInfo.componentName) return intent } @@ -74,14 +75,17 @@ fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? { * Returns intent if there is a non-browser application available to handle the uri. Otherwise, * returns null. */ -fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? { - val intent = Intent(ACTION_VIEW, uri).apply { - flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER +fun getAppIntent(uri: Uri, packageManager: PackageManager, userId: Int): Intent? { + val intent = Intent(ACTION_VIEW, uri).addFlags(FLAG_ACTIVITY_NEW_TASK) + val resolveInfo = packageManager.resolveActivityAsUser(intent, /* flags= */ 0, userId) + ?: return null + // If there is a non-browser application available to handle the intent, return the intent. + // Otherwise, return null. + if (resolveInfo.activityInfo != null && !resolveInfo.handleAllWebDataURI) { + intent.setComponent(resolveInfo.componentInfo.componentName) + return intent } - // If there is no application available to handle intent, return null - val component = intent.resolveActivity(packageManager) ?: return null - intent.setComponent(component) - return intent + return null } /** 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 bec73a1500a7..9aba3aaa3268 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 @@ -2072,10 +2072,7 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void suppressionChanged(Bubble bubble, boolean isSuppressed) { - if (mLayerView != null) { - // TODO (b/273316505) handle suppression changes, although might not need to - // to do anything on the layerview side for this... - } + // Nothing to do for our views, handled by launcher / in the bubble bar. } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 10054a1727fc..94a6e5862b6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -115,6 +115,7 @@ public abstract class Pip2Module { PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, + PhonePipMenuController pipMenuController, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -123,7 +124,8 @@ public abstract class Pip2Module { context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, pipTouchHandler, pipAppOpsListener, mainExecutor)); + pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController, + mainExecutor)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 50187d552b09..d404634b0db0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -239,7 +239,6 @@ class DesktopMixedTransitionHandler( pending.minimizingTask?.let { minimizingTask -> findTaskChange(info, minimizingTask) } val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask) if (launchChange == null) { - check(minimizeChange == null) check(immersiveExitChange == null) logV("No launch Change, returning") return false 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 9b7c3a4d929f..609ac0aac381 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 @@ -592,10 +592,15 @@ class DesktopTasksController( val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false // If task is going to PiP, start a PiP transition instead of a minimize transition if (isMinimizingToPip) { - val requestInfo = TransitionRequestInfo( - TRANSIT_PIP, /* triggerTask= */ null, taskInfo, /* remoteTransition= */ null, - /* displayChange= */ null, /* flags= */ 0 - ) + val requestInfo = + TransitionRequestInfo( + TRANSIT_PIP, + /* triggerTask= */ null, + taskInfo, + /* remoteTransition= */ null, + /* displayChange= */ null, + /* flags= */ 0, + ) val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) pendingPipTransitionAndTask = @@ -759,15 +764,12 @@ class DesktopTasksController( displayId: Int = DEFAULT_DISPLAY, ): IBinder { val taskIdToMinimize = - if (launchingTaskId != null) { - addAndGetMinimizeChanges(displayId, wct, newTaskId = launchingTaskId) - } else { - logW("Starting desktop task launch without checking the task-limit") - // TODO(b/378920066): This currently does not respect the desktop window limit. - // It's possible that |launchingTaskId| is null when launching using an intent, and - // the task-limit should be respected then too. - null - } + addAndGetMinimizeChanges( + displayId, + wct, + newTaskId = launchingTaskId, + launchingNewIntent = launchingTaskId == null, + ) val exitImmersiveResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, @@ -1409,7 +1411,7 @@ class DesktopTasksController( override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, - finishT: Transaction? + finishT: Transaction?, ) { pendingPipTransitionAndTask?.let { (pipTransition, taskId) -> if (transition == pipTransition) { @@ -1985,10 +1987,14 @@ class DesktopTasksController( private fun addAndGetMinimizeChanges( displayId: Int, wct: WindowContainerTransaction, - newTaskId: Int, + newTaskId: Int?, + launchingNewIntent: Boolean = false, ): Int? { if (!desktopTasksLimiter.isPresent) return null - return desktopTasksLimiter.get().addAndGetMinimizeTaskChanges(displayId, wct, newTaskId) + require(newTaskId == null || !launchingNewIntent) + return desktopTasksLimiter + .get() + .addAndGetMinimizeTaskChanges(displayId, wct, newTaskId, launchingNewIntent) } private fun addPendingMinimizeTransition(transition: IBinder, taskIdToMinimize: Int) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 635078e68a00..45faba6e341f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -214,12 +214,17 @@ class DesktopTasksLimiter( fun addAndGetMinimizeTaskChanges( displayId: Int, wct: WindowContainerTransaction, - newFrontTaskId: Int, + newFrontTaskId: Int?, + launchingNewIntent: Boolean = false, ): Int? { logV("addAndGetMinimizeTaskChanges, newFrontTask=%d", newFrontTaskId) val taskRepository = desktopUserRepositories.current val taskIdToMinimize = - getTaskIdToMinimize(taskRepository.getExpandedTasksOrdered(displayId), newFrontTaskId) + getTaskIdToMinimize( + taskRepository.getExpandedTasksOrdered(displayId), + newFrontTaskId, + launchingNewIntent, + ) // If it's a running task, reorder it to back. taskIdToMinimize ?.let { shellTaskOrganizer.getRunningTaskInfo(it) } @@ -242,15 +247,24 @@ class DesktopTasksLimiter( * Returns the minimized task from the list of visible tasks ordered from front to back with the * new task placed in front of other tasks. */ - fun getTaskIdToMinimize(visibleOrderedTasks: List<Int>, newTaskIdInFront: Int? = null): Int? { + fun getTaskIdToMinimize( + visibleOrderedTasks: List<Int>, + newTaskIdInFront: Int? = null, + launchingNewIntent: Boolean = false, + ): Int? { return getTaskIdToMinimize( - createOrderedTaskListWithGivenTaskInFront(visibleOrderedTasks, newTaskIdInFront) + createOrderedTaskListWithGivenTaskInFront(visibleOrderedTasks, newTaskIdInFront), + launchingNewIntent, ) } /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */ - private fun getTaskIdToMinimize(visibleOrderedTasks: List<Int>): Int? { - if (visibleOrderedTasks.size <= maxTasksLimit) { + private fun getTaskIdToMinimize( + visibleOrderedTasks: List<Int>, + launchingNewIntent: Boolean, + ): Int? { + val newTasksOpening = if (launchingNewIntent) 1 else 0 + if (visibleOrderedTasks.size + newTasksOpening <= maxTasksLimit) { logV("No need to minimize; tasks below limit") // No need to minimize anything return null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 3cc477602cf0..9c3e815b389d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -781,6 +781,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb // cancel any running animator, as it is using stale display layout information animator.cancel(); } + mMenuController.hideMenu(); onDisplayChangedUncheck(layout, saveRestoreSnapFraction); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index a849b9de59ce..8c6d5f5c6660 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -97,6 +97,7 @@ public class PipController implements ConfigurationChangeListener, private final PipTransitionState mPipTransitionState; private final PipTouchHandler mPipTouchHandler; private final PipAppOpsListener mPipAppOpsListener; + private final PhonePipMenuController mPipMenuController; private final ShellExecutor mMainExecutor; private final PipImpl mImpl; private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); @@ -141,6 +142,7 @@ public class PipController implements ConfigurationChangeListener, PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, + PhonePipMenuController pipMenuController, ShellExecutor mainExecutor) { mContext = context; mShellCommandHandler = shellCommandHandler; @@ -157,6 +159,7 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.addPipTransitionStateChangedListener(this); mPipTouchHandler = pipTouchHandler; mPipAppOpsListener = pipAppOpsListener; + mPipMenuController = pipMenuController; mMainExecutor = mainExecutor; mImpl = new PipImpl(); @@ -183,6 +186,7 @@ public class PipController implements ConfigurationChangeListener, PipTransitionState pipTransitionState, PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, + PhonePipMenuController pipMenuController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -192,7 +196,8 @@ public class PipController implements ConfigurationChangeListener, return new PipController(context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, pipTouchHandler, pipAppOpsListener, mainExecutor); + pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController, + mainExecutor); } public PipImpl getPipImpl() { @@ -329,6 +334,7 @@ public class PipController implements ConfigurationChangeListener, } mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio()); + mPipMenuController.hideMenu(); if (mPipTransitionState.isInFixedRotation()) { // Do not change the bounds when in fixed rotation, but do update the movement bounds 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 e48c887c625f..d0c21c9ec7c0 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 @@ -20,6 +20,7 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_A import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT; @@ -145,6 +146,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex; @@ -2766,6 +2768,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final @WindowManager.TransitionType int type = request.getType(); final boolean isOpening = isOpeningType(type); final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; + final boolean inDesktopMode = DesktopModeStatus.canEnterDesktopMode(mContext) + && triggerTask.getWindowingMode() == WINDOWING_MODE_FREEFORM; final StageTaskListener stage = getStageOfTask(triggerTask); if (isOpening && inFullscreen) { @@ -2820,6 +2824,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.setDismissTransition(transition, stageType, EXIT_REASON_FULLSCREEN_REQUEST); } + } else if (isOpening && inDesktopMode) { + // If the app being opened is in Desktop mode, set it to full screen and dismiss + // split screen stage. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out); + out.setWindowingMode(triggerTask.token, WINDOWING_MODE_UNDEFINED) + .setBounds(triggerTask.token, null); } else if (isOpening && inFullscreen) { final int activityType = triggerTask.getActivityType(); if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 0b919668f7fe..792f5cad3418 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -468,7 +468,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT case MotionEvent.ACTION_DOWN: { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( - 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); + 0 /* ctrlType */, e.getDisplayId(), e.getRawX(0), e.getRawY(0)); mIsDragging = false; return false; } @@ -481,6 +481,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT if (decoration.isHandlingDragResize()) break; final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( + e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mIsDragging = true; return true; @@ -492,6 +493,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( + e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(newTaskBounds, mWindowDecorByTaskId.get(mTaskId).calculateValidDragArea()); 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 5a05861c3a88..7928e5ed4188 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 @@ -1140,7 +1140,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (dragAllowed) { mDragPointerId = e.getPointerId(0); final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart( - 0 /* ctrlType */, e.getRawX(0), + 0 /* ctrlType */, e.getDisplayId(), e.getRawX(0), e.getRawY(0)); updateDragStatus(e.getActionMasked()); mOnDragStartInitialBounds.set(initialBounds); @@ -1161,6 +1161,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove( + e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mDesktopTasksController.onDragPositioningMove(taskInfo, decoration.mTaskSurface, @@ -1191,6 +1192,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, (int) (e.getRawX(dragPointerIdx) - e.getX(dragPointerIdx)), (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx))); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( + e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); // Tasks bounds haven't actually been updated (only its leash), so pass to // DesktopTasksController to allow secondary transformations (i.e. snap resizing 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 febf5669d12d..0d1960ad6e29 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 @@ -617,14 +617,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } if (browserLink == null) return null; - return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager()); + return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager(), + mUserContext.getUserId()); } @Nullable private Intent getAppLink() { return mWebUri == null ? null - : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager()); + : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager(), + mUserContext.getUserId()); } private boolean isBrowserApp() { @@ -779,12 +781,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final Point position = new Point(mResult.mCaptionX, 0); if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) == SPLIT_POSITION_BOTTOM_OR_RIGHT - && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape() ) { - // If this is the right split task, add left stage's width. - final Rect leftStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); - position.x += leftStageBounds.width(); + if (mSplitScreenController.isLeftRightSplit()) { + // If this is the right split task, add left stage's width. + final Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + position.x += leftStageBounds.width(); + } else { + final Rect bottomStageBounds = new Rect(); + mSplitScreenController.getRefStageBounds(new Rect(), bottomStageBounds); + position.y += bottomStageBounds.top; + } } return position; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java index 421ffd929fb2..3eebdb048f0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java @@ -41,25 +41,30 @@ public interface DragPositioningCallback { * * @param ctrlType {@link CtrlType} indicating the direction of resizing, use * {@code 0} to indicate it's a move + * @param displayId the ID of the display where the drag starts * @param x x coordinate in window decoration coordinate system where the drag starts * @param y y coordinate in window decoration coordinate system where the drag starts * @return the starting task bounds */ - Rect onDragPositioningStart(@CtrlType int ctrlType, float x, float y); + Rect onDragPositioningStart(@CtrlType int ctrlType, int displayId, float x, float y); /** * Called when the pointer moves during a drag-resize or drag-move. + * + * @param displayId the ID of the display where the pointer is currently located * @param x x coordinate in window decoration coordinate system of the new pointer location * @param y y coordinate in window decoration coordinate system of the new pointer location * @return the updated task bounds */ - Rect onDragPositioningMove(float x, float y); + Rect onDragPositioningMove(int displayId, float x, float y); /** * Called when a drag-resize or drag-move stops. + * + * @param displayId the ID of the display where the pointer is located when drag stops * @param x x coordinate in window decoration coordinate system where the drag resize stops * @param y y coordinate in window decoration coordinate system where the drag resize stops * @return the final bounds for the dragged task */ - Rect onDragPositioningEnd(float x, float y); + Rect onDragPositioningEnd(int displayId, float x, float y); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index a6d503d0d991..7d1471f44674 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -454,7 +454,7 @@ class DragResizeInputListener implements AutoCloseable { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, - rawX, rawY); + e.getDisplayId(), rawX, rawY); mLastMotionEventOnDown = e; mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT) @@ -489,7 +489,8 @@ class DragResizeInputListener implements AutoCloseable { } final float rawX = e.getRawX(dragPointerIndex); final float rawY = e.getRawY(dragPointerIndex); - final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY); + final Rect taskBounds = mCallback.onDragPositioningMove(e.getDisplayId(), + rawX, rawY); updateInputSinkRegionForDrag(taskBounds); result = true; break; @@ -505,7 +506,7 @@ class DragResizeInputListener implements AutoCloseable { TAG, e.getActionMasked()); break; } - final Rect taskBounds = mCallback.onDragPositioningEnd( + final Rect taskBounds = mCallback.onDragPositioningEnd(e.getDisplayId(), e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); // If taskBounds has changed, setGeometry will be called and update the // sink region. Otherwise, we should revert it here. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java index c8aff78cbb36..5b027f3c039e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -168,7 +168,10 @@ public final class DragResizeWindowGeometry { return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; } - static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { + /** + * Whether resizing a window from the edge is permitted based on the motion event. + */ + public static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt index 3885761d0742..ab30d617af54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt @@ -47,10 +47,11 @@ class FixedAspectRatioTaskPositionerDecorator ( private var startingAspectRatio = 0f private var isTaskPortrait = false - override fun onDragPositioningStart(@CtrlType ctrlType: Int, x: Float, y: Float): Rect { + override fun onDragPositioningStart( + @CtrlType ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { originalCtrlType = ctrlType if (!requiresFixedAspectRatio()) { - return super.onDragPositioningStart(originalCtrlType, x, y) + return super.onDragPositioningStart(originalCtrlType, displayId, x, y) } lastRepositionedBounds.set(getBounds(windowDecoration.mTaskInfo)) @@ -72,27 +73,27 @@ class FixedAspectRatioTaskPositionerDecorator ( val verticalMidPoint = lastRepositionedBounds.top + (startingBoundHeight / 2) edgeResizeCtrlType = originalCtrlType + if (y < verticalMidPoint) CTRL_TYPE_TOP else CTRL_TYPE_BOTTOM - super.onDragPositioningStart(edgeResizeCtrlType, x, y) + super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y) } CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { val horizontalMidPoint = lastRepositionedBounds.left + (startingBoundWidth / 2) edgeResizeCtrlType = originalCtrlType + if (x < horizontalMidPoint) CTRL_TYPE_LEFT else CTRL_TYPE_RIGHT - super.onDragPositioningStart(edgeResizeCtrlType, x, y) + super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y) } // If resize is corner resize, no alteration to the ctrlType needs to be made. else -> { edgeResizeCtrlType = CTRL_TYPE_UNDEFINED - super.onDragPositioningStart(originalCtrlType, x, y) + super.onDragPositioningStart(originalCtrlType, displayId, x, y) } } ) return lastRepositionedBounds } - override fun onDragPositioningMove(x: Float, y: Float): Rect { + override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect { if (!requiresFixedAspectRatio()) { - return super.onDragPositioningMove(x, y) + return super.onDragPositioningMove(displayId, x, y) } val diffX = x - lastValidPoint.x @@ -103,7 +104,7 @@ class FixedAspectRatioTaskPositionerDecorator ( // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 // degrees from the corner the previous valid point). Allow resize with adjusted // coordinates to maintain aspect ratio. - lastRepositionedBounds.set(dragAdjustedMove(x, y)) + lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y)) } } CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { @@ -111,28 +112,28 @@ class FixedAspectRatioTaskPositionerDecorator ( // Drag coordinate falls within valid region (180 - 270 degrees or 0 - 90 // degrees from the corner the previous valid point). Allow resize with adjusted // coordinates to maintain aspect ratio. - lastRepositionedBounds.set(dragAdjustedMove(x, y)) + lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y)) } } CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { // If resize is on left or right edge, always adjust the y coordinate. val adjustedY = getScaledChangeForY(x) lastValidPoint.set(x, adjustedY) - lastRepositionedBounds.set(super.onDragPositioningMove(x, adjustedY)) + lastRepositionedBounds.set(super.onDragPositioningMove(displayId, x, adjustedY)) } CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { // If resize is on top or bottom edge, always adjust the x coordinate. val adjustedX = getScaledChangeForX(y) lastValidPoint.set(adjustedX, y) - lastRepositionedBounds.set(super.onDragPositioningMove(adjustedX, y)) + lastRepositionedBounds.set(super.onDragPositioningMove(displayId, adjustedX, y)) } } return lastRepositionedBounds } - override fun onDragPositioningEnd(x: Float, y: Float): Rect { + override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect { if (!requiresFixedAspectRatio()) { - return super.onDragPositioningEnd(x, y) + return super.onDragPositioningEnd(displayId, x, y) } val diffX = x - lastValidPoint.x @@ -144,55 +145,55 @@ class FixedAspectRatioTaskPositionerDecorator ( // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 // degrees from the corner the previous valid point). End resize with adjusted // coordinates to maintain aspect ratio. - return dragAdjustedEnd(x, y) + return dragAdjustedEnd(displayId, x, y) } // If end of resize is not within valid region, end resize from last valid // coordinates. - return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y) + return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y) } CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) { // Drag coordinate falls within valid region (180 - 260 degrees or 0 - 90 // degrees from the corner the previous valid point). End resize with adjusted // coordinates to maintain aspect ratio. - return dragAdjustedEnd(x, y) + return dragAdjustedEnd(displayId, x, y) } // If end of resize is not within valid region, end resize from last valid // coordinates. - return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y) + return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y) } CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { // If resize is on left or right edge, always adjust the y coordinate. - return super.onDragPositioningEnd(x, getScaledChangeForY(x)) + return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x)) } CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { // If resize is on top or bottom edge, always adjust the x coordinate. - return super.onDragPositioningEnd(getScaledChangeForX(y), y) + return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y) } else -> { - return super.onDragPositioningEnd(x, y) + return super.onDragPositioningEnd(displayId, x, y) } } } - private fun dragAdjustedMove(x: Float, y: Float): Rect { + private fun dragAdjustedMove(displayId: Int, x: Float, y: Float): Rect { val absDiffX = abs(x - lastValidPoint.x) val absDiffY = abs(y - lastValidPoint.y) if (absDiffY < absDiffX) { lastValidPoint.set(getScaledChangeForX(y), y) - return super.onDragPositioningMove(getScaledChangeForX(y), y) + return super.onDragPositioningMove(displayId, getScaledChangeForX(y), y) } lastValidPoint.set(x, getScaledChangeForY(x)) - return super.onDragPositioningMove(x, getScaledChangeForY(x)) + return super.onDragPositioningMove(displayId, x, getScaledChangeForY(x)) } - private fun dragAdjustedEnd(x: Float, y: Float): Rect { + private fun dragAdjustedEnd(displayId: Int, x: Float, y: Float): Rect { val absDiffX = abs(x - lastValidPoint.x) val absDiffY = abs(y - lastValidPoint.y) if (absDiffY < absDiffX) { - return super.onDragPositioningEnd(getScaledChangeForX(y), y) + return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y) } - return super.onDragPositioningEnd(x, getScaledChangeForY(x)) + return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x)) } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 3efae9d6375a..2d6f7459e0ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -91,7 +91,7 @@ class FluidResizeTaskPositioner implements TaskPositioner, Transitions.Transitio } @Override - public Rect onDragPositioningStart(int ctrlType, float x, float y) { + public Rect onDragPositioningStart(int ctrlType, int displayId, float x, float y) { mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); @@ -117,7 +117,7 @@ class FluidResizeTaskPositioner implements TaskPositioner, Transitions.Transitio } @Override - public Rect onDragPositioningMove(float x, float y) { + public Rect onDragPositioningMove(int displayId, float x, float y) { final WindowContainerTransaction wct = new WindowContainerTransaction(); PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType, @@ -147,7 +147,7 @@ class FluidResizeTaskPositioner implements TaskPositioner, Transitions.Transitio } @Override - public Rect onDragPositioningEnd(float x, float y) { + public Rect onDragPositioningEnd(int displayId, float x, float y) { // If task has been resized or task was dragged into area outside of // mDisallowedAreaForEndBounds, apply WCT to finish it. if (isResizing() && mHasDragResized) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 159759ede368..bb19a2cc2ad4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -694,7 +694,7 @@ class HandleMenu( setTextColor(style.textColor) compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } - + openByDefaultBtn.isGone = isBrowserApp openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index 376cd2a78baf..e23ebe6634ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -26,6 +26,7 @@ import android.graphics.drawable.RippleDrawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View +import android.view.ViewStub import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ProgressBar @@ -46,13 +47,17 @@ class MaximizeButtonView( private val hoverProgressAnimatorSet = AnimatorSet() var hoverDisabled = false - private val progressBar: ProgressBar + private lateinit var stubProgressBarContainer: ViewStub private val maximizeWindow: ImageButton + private val progressBar: ProgressBar by lazy { + (stubProgressBarContainer.inflate() as FrameLayout) + .requireViewById(R.id.progress_bar) + } init { LayoutInflater.from(context).inflate(R.layout.maximize_menu_button, this, true) - progressBar = requireViewById(R.id.progress_bar) + stubProgressBarContainer = requireViewById(R.id.stub_progress_bar_container) maximizeWindow = requireViewById(R.id.maximize_window) } @@ -115,21 +120,34 @@ class MaximizeButtonView( requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" } maximizeWindow.imageTintList = iconForegroundColor maximizeWindow.background = rippleDrawable - progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor) - .withAlpha(OPACITY_15) - progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) + stubProgressBarContainer.setOnInflateListener { _, inflated -> + val progressBar = (inflated as FrameLayout) + .requireViewById(R.id.progress_bar) as ProgressBar + progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor) + .withAlpha(OPACITY_15) + progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) + } } else { - if (darkMode) { - progressBar.progressTintList = ColorStateList.valueOf( + val progressTint = if (darkMode) { + ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark)) - maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, - R.color.desktop_mode_caption_button_color_selector_dark)) } else { - progressBar.progressTintList = ColorStateList.valueOf( + ColorStateList.valueOf( resources.getColor(R.color.desktop_mode_maximize_menu_progress_light)) - maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context, - R.color.desktop_mode_caption_button_color_selector_light)) } + val backgroundTint = if (darkMode) { + ContextCompat.getColorStateList(context, + R.color.desktop_mode_caption_button_color_selector_dark) + } else { + ContextCompat.getColorStateList(context, + R.color.desktop_mode_caption_button_color_selector_light) + } + stubProgressBarContainer.setOnInflateListener { _, inflated -> + val progressBar = (inflated as FrameLayout) + .requireViewById(R.id.progress_bar) as ProgressBar + progressBar.progressTintList = progressTint + } + maximizeWindow.background?.setTintList(backgroundTint) } } 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 1f03d7568130..e011cc08903b 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 @@ -104,7 +104,7 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T } @Override - public Rect onDragPositioningStart(int ctrlType, float x, float y) { + public Rect onDragPositioningStart(int ctrlType, int displayId, float x, float y) { mCtrlType = ctrlType; mTaskBoundsAtDragStart.set( mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); @@ -136,7 +136,7 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T } @Override - public Rect onDragPositioningMove(float x, float y) { + public Rect onDragPositioningMove(int displayId, 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. @@ -170,7 +170,7 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T } @Override - public Rect onDragPositioningEnd(float x, float y) { + public Rect onDragPositioningEnd(int displayId, float x, float y) { PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); if (isResizing()) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 39178cb2cd25..1e4d108a9cda 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -442,6 +442,18 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() { + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } + val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }, + newTaskIdInFront = null, + launchingNewIntent = true) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last().taskId) + } + + @Test fun minimizeTransitionReadyAndFinished_logsJankInstrumentationBeginAndEnd() { (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val transition = Binder() 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 0214da4660ad..aead0a7afb53 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 @@ -1015,11 +1015,11 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onCaptionButtonTouchListener = onTouchListenerCaptor ) - whenever(mockTaskPositioner.onDragPositioningStart(any(), any(), any())) + whenever(mockTaskPositioner.onDragPositioningStart(any(), any(), any(), any())) .thenReturn(INITIAL_BOUNDS) - whenever(mockTaskPositioner.onDragPositioningMove(any(), any())) + whenever(mockTaskPositioner.onDragPositioningMove(any(), any(), any())) .thenReturn(INITIAL_BOUNDS) - whenever(mockTaskPositioner.onDragPositioningEnd(any(), any())) + whenever(mockTaskPositioner.onDragPositioningEnd(any(), any(), any())) .thenReturn(INITIAL_BOUNDS) val view = mock(View::class.java) 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 8a1a9b5ef80b..855b3ddd99b5 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 @@ -278,9 +278,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); final ActivityInfo activityInfo = createActivityInfo(); when(mMockPackageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo); - final ResolveInfo resolveInfo = new ResolveInfo(); - resolveInfo.activityInfo = activityInfo; - when(mMockPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo); + final ResolveInfo resolveInfo = createResolveInfo(false /* handleAllWebDataUri */); + when(mMockPackageManager.resolveActivityAsUser(any(), anyInt(), anyInt())) + .thenReturn(resolveInfo); final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); @@ -1664,11 +1664,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) public void browserApp_transferSessionUriUsedForBrowserAppWhenAvailable() { // Make {@link AppToWebUtils#isBrowserApp} return true - ResolveInfo resolveInfo = new ResolveInfo(); - resolveInfo.handleAllWebDataURI = true; - resolveInfo.activityInfo = createActivityInfo(); + ResolveInfo browserResolveInfo = createResolveInfo(true /* handleAllWebUriData */); when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) - .thenReturn(List.of(resolveInfo)); + .thenReturn(List.of(browserResolveInfo)); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( @@ -1793,6 +1791,13 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return windowDecor; } + private ResolveInfo createResolveInfo(boolean handleAllWebDataURI) { + final ResolveInfo info = new ResolveInfo(); + info.handleAllWebDataURI = handleAllWebDataURI; + info.activityInfo = createActivityInfo(); + return info; + } + private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) { final ActivityManager.TaskDescription.Builder taskDescriptionBuilder = new ActivityManager.TaskDescription.Builder(); @@ -1821,6 +1826,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { applicationInfo.packageName = "DesktopModeWindowDecorationTestPackage"; final ActivityInfo activityInfo = new ActivityInfo(); activityInfo.applicationInfo = applicationInfo; + activityInfo.packageName = "DesktopModeWindowDecorationTestPackage"; activityInfo.name = "DesktopModeWindowDecorationTest"; return activityInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt index ce17c1df50bc..3c3d6b6bb258 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt @@ -68,9 +68,9 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ configuration.windowConfiguration.setBounds(PORTRAIT_BOUNDS) } doReturn(PORTRAIT_BOUNDS).`when`(mockTaskPositioner).onDragPositioningStart( - any(), any(), any()) - doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any()) - doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any()) + any(), any(), any(), any()) + doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any(), any()) + doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any(), any()) decoratedTaskPositioner = spy( FixedAspectRatioTaskPositionerDecorator( mockDesktopWindowDecoration, mockTaskPositioner) @@ -87,7 +87,8 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ isResizeable = testCase.isResizeable } - decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY) + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, DISPLAY_ID, originalX, originalY) val capturedValues = getLatestOnStartArguments() assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType) @@ -102,7 +103,8 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val originalX = 0f val originalY = 0f - decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY) + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, DISPLAY_ID, originalX, originalY) val capturedValues = getLatestOnStartArguments() assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType) @@ -119,7 +121,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) val adjustedCtrlType = testCase.ctrlType + testCase.additionalEdgeCtrlType val capturedValues = getLatestOnStartArguments() @@ -134,13 +136,14 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ ) { val originalX = 0f val originalY = 0f - decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX) + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, DISPLAY_ID, originalX, originalX) mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { isResizeable = testCase.isResizeable } decoratedTaskPositioner.onDragPositioningMove( - originalX + SMALL_DELTA, originalY + SMALL_DELTA) + DISPLAY_ID, originalX + SMALL_DELTA, originalY + SMALL_DELTA) val capturedValues = getLatestOnMoveArguments() assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA) @@ -156,13 +159,14 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) val updatedBounds = decoratedTaskPositioner.onDragPositioningMove( + DISPLAY_ID, startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) - verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any()) + verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any(), any()) assertThat(updatedBounds).isEqualTo(startingBounds) } @@ -176,10 +180,12 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) decoratedTaskPositioner.onDragPositioningMove( - startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) + DISPLAY_ID, + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) val adjustedDragDelta = calculateAdjustedDelta( testCase.ctrlType, testCase.dragDelta, orientation) @@ -202,9 +208,10 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) decoratedTaskPositioner.onDragPositioningMove( + DISPLAY_ID, startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) @@ -227,13 +234,14 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ ) { val originalX = 0f val originalY = 0f - decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX) + decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, DISPLAY_ID, + originalX, originalX) mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { isResizeable = testCase.isResizeable } decoratedTaskPositioner.onDragPositioningEnd( - originalX + SMALL_DELTA, originalY + SMALL_DELTA) + DISPLAY_ID, originalX + SMALL_DELTA, originalY + SMALL_DELTA) val capturedValues = getLatestOnEndArguments() assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA) @@ -249,9 +257,10 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) decoratedTaskPositioner.onDragPositioningEnd( + DISPLAY_ID, startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) @@ -269,10 +278,12 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) decoratedTaskPositioner.onDragPositioningEnd( - startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) + DISPLAY_ID, + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) val adjustedDragDelta = calculateAdjustedDelta( testCase.ctrlType, testCase.dragDelta, orientation) @@ -295,9 +306,10 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) decoratedTaskPositioner.onDragPositioningStart( - testCase.ctrlType, startingPoint.x, startingPoint.y) + testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y) decoratedTaskPositioner.onDragPositioningEnd( + DISPLAY_ID, startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) @@ -322,7 +334,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ val captorCtrlType = argumentCaptor<Int>() val captorCoordinates = argumentCaptor<Float>() verify(mockTaskPositioner).onDragPositioningStart( - captorCtrlType.capture(), captorCoordinates.capture(), captorCoordinates.capture()) + captorCtrlType.capture(), any(), captorCoordinates.capture(), captorCoordinates.capture()) return CtrlCoordinateCapture(captorCtrlType.firstValue, captorCoordinates.firstValue, captorCoordinates.secondValue) @@ -335,7 +347,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ private fun getLatestOnMoveArguments(): PointF { val captorCoordinates = argumentCaptor<Float>() verify(mockTaskPositioner).onDragPositioningMove( - captorCoordinates.capture(), captorCoordinates.capture()) + any(), captorCoordinates.capture(), captorCoordinates.capture()) return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue) } @@ -347,7 +359,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ private fun getLatestOnEndArguments(): PointF { val captorCoordinates = argumentCaptor<Float>() verify(mockTaskPositioner).onDragPositioningEnd( - captorCoordinates.capture(), captorCoordinates.capture()) + any(), captorCoordinates.capture(), captorCoordinates.capture()) return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue) } @@ -358,7 +370,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ private fun getAndMockBounds(orientation: Orientation): Rect { val mockBounds = if (orientation.isPortrait) PORTRAIT_BOUNDS else LANDSCAPE_BOUNDS doReturn(mockBounds).`when`(mockTaskPositioner).onDragPositioningStart( - any(), any(), any()) + any(), any(), any(), any()) doReturn(mockBounds).`when`(decoratedTaskPositioner).getBounds(any()) return mockBounds } @@ -458,6 +470,7 @@ class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ private val STARTING_ASPECT_RATIO = PORTRAIT_BOUNDS.height() / PORTRAIT_BOUNDS.width() private const val LARGE_DELTA = 50f private const val SMALL_DELTA = 30f + private const val DISPLAY_ID = 1 enum class Orientation( val isPortrait: Boolean diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index 3b80cb4936b9..cec52518edd2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -150,11 +150,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_notMove_skipsTransitionOnEnd() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) @@ -171,11 +173,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_noEffectiveMove_skipsTransitionOnMoveAndEnd() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -188,6 +192,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { }) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) @@ -204,11 +209,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_hasEffectiveMove_issuesTransitionOnMoveAndEnd() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() ) @@ -224,6 +231,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID)) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) @@ -242,6 +250,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_move_skipsDragResizingFlag() { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // Move + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -250,11 +259,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.left.toFloat() + 10 val newY = STARTING_BOUNDS.top.toFloat() taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -276,6 +286,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setsDragResizingFlag() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -284,11 +295,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() + 10 val newY = STARTING_BOUNDS.top.toFloat() taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -310,6 +322,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenLessThanMin() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -318,11 +331,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 5 val newY = STARTING_BOUNDS.top.toFloat() + 95 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -340,6 +354,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenLessThanMin() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -348,11 +363,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 95 val newY = STARTING_BOUNDS.top.toFloat() + 5 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -370,6 +386,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenNegative() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -378,11 +395,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 5 val newY = STARTING_BOUNDS.top.toFloat() + 105 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -400,6 +418,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenNegative() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -408,11 +427,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 105 val newY = STARTING_BOUNDS.top.toFloat() + 5 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -430,6 +450,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsRunsWhenResizeBoundsValid() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -438,11 +459,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 80 val newY = STARTING_BOUNDS.top.toFloat() + 80 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -456,6 +478,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_setBoundsDoesNotRunWithNegativeHeightAndWidth() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -464,11 +487,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 95 val newY = STARTING_BOUNDS.top.toFloat() + 95 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -484,6 +508,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -492,11 +517,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 97 val newY = STARTING_BOUNDS.top.toFloat() + 97 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -510,6 +536,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_useMinWidthWhenValid() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -518,11 +545,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.right.toFloat() - 93 val newY = STARTING_BOUNDS.top.toFloat() + 93 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -535,6 +563,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_toDisallowedBounds_freezesAtLimit() { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat() ) @@ -546,6 +575,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right + 10, STARTING_BOUNDS.bottom + 10) taskPositioner.onDragPositioningMove( + DISPLAY_ID, newBounds.right.toFloat(), newBounds.bottom.toFloat() ) @@ -559,11 +589,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { DISALLOWED_RESIZE_AREA.top ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, newBounds2.right.toFloat(), newBounds2.bottom.toFloat() ) - taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newBounds2.right.toFloat(), + newBounds2.bottom.toFloat()) // The first resize falls in the allowed area, verify there's a change for it. verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> @@ -629,6 +661,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration.mHasGlobalFocus = false taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -645,6 +678,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration.mHasGlobalFocus = true taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -661,6 +695,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { mockWindowDecoration.mHasGlobalFocus = false taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -729,11 +764,13 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20 ) @@ -742,6 +779,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { assertTrue(taskPositioner.isResizingOrAnimating) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -785,15 +823,18 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { ) { taskPositioner.onDragPositioningStart( ctrlType, + DISPLAY_ID, startX, startY ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, endX, endY ) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, endX, endY ) 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 e7df8643ba66..eb8c0dd365a3 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 @@ -168,12 +168,14 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -191,11 +193,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 60, STARTING_BOUNDS.top.toFloat() + 100 ) @@ -208,6 +212,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { eq(rectAfterMove.top.toFloat())) val endBounds = taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 70, STARTING_BOUNDS.top.toFloat() + 20 ) @@ -226,11 +231,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DISPLAY_ID, STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.right.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) @@ -248,6 +255,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { }) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.right.toFloat() + 20, STARTING_BOUNDS.top.toFloat() + 20 ) @@ -266,17 +274,20 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { @Test fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( + DISPLAY_ID, CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) @@ -300,6 +311,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -307,11 +319,12 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { val newX = STARTING_BOUNDS.left.toFloat() + 5 val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1 taskPositioner.onDragPositioningMove( + DISPLAY_ID, newX, newY ) - taskPositioner.onDragPositioningEnd(newX, newY) + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -326,6 +339,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration.mHasGlobalFocus = false taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -342,6 +356,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration.mHasGlobalFocus = true taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -358,6 +373,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration.mHasGlobalFocus = false taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -422,11 +438,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20 ) @@ -436,6 +454,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID)) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) @@ -501,15 +520,18 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { ) { taskPositioner.onDragPositioningStart( ctrlType, + DISPLAY_ID, startX, startY ) taskPositioner.onDragPositioningMove( + DISPLAY_ID, endX, endY ) taskPositioner.onDragPositioningEnd( + DISPLAY_ID, endX, endY ) diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 50387548b4ab..bcb70019b3ac 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -53,7 +53,7 @@ import java.util.stream.Collectors; * The format of the media data is specified as key/value pairs. Keys are strings. Values can * be integer, long, float, String or ByteBuffer. * <p> - * The feature metadata is specificed as string/boolean pairs. + * The feature metadata is specified as string/boolean pairs. * <p> * Keys common to all audio/video formats, <b>all keys not marked optional are mandatory</b>: * @@ -1244,12 +1244,12 @@ public final class MediaFormat { /** * An optional key describing the desired encoder latency in frames. This is an optional - * parameter that applies only to video encoders. If encoder supports it, it should ouput + * parameter that applies only to video encoders. If encoder supports it, it should output * at least one output frame after being queued the specified number of frames. This key * is ignored if the video encoder does not support the latency feature. Use the output * format to verify that this feature was enabled and the actual value used by the encoder. * <p> - * If the key is not specified, the default latency will be implenmentation specific. + * If the key is not specified, the default latency will be implementation specific. * The associated value is an integer. */ public static final String KEY_LATENCY = "latency"; @@ -1507,16 +1507,16 @@ public final class MediaFormat { */ public static final String KEY_COLOR_STANDARD = "color-standard"; - /** BT.709 color chromacity coordinates with KR = 0.2126, KB = 0.0722. */ + /** BT.709 color chromaticity coordinates with KR = 0.2126, KB = 0.0722. */ public static final int COLOR_STANDARD_BT709 = 1; - /** BT.601 625 color chromacity coordinates with KR = 0.299, KB = 0.114. */ + /** BT.601 625 color chromaticity coordinates with KR = 0.299, KB = 0.114. */ public static final int COLOR_STANDARD_BT601_PAL = 2; - /** BT.601 525 color chromacity coordinates with KR = 0.299, KB = 0.114. */ + /** BT.601 525 color chromaticity coordinates with KR = 0.299, KB = 0.114. */ public static final int COLOR_STANDARD_BT601_NTSC = 4; - /** BT.2020 color chromacity coordinates with KR = 0.2627, KB = 0.0593. */ + /** BT.2020 color chromaticity coordinates with KR = 0.2627, KB = 0.0593. */ public static final int COLOR_STANDARD_BT2020 = 6; /** @hide */ @@ -2150,7 +2150,7 @@ public final class MediaFormat { * Sets the value of a string key. * <p> * If value is {@code null}, it sets a null value that behaves similarly to a missing key. - * This could be used prior to API level {@link android os.Build.VERSION_CODES#Q} to effectively + * This could be used prior to API level {@link android.os.Build.VERSION_CODES#Q} to effectively * remove a key. */ public final void setString(@NonNull String name, @Nullable String value) { @@ -2161,7 +2161,7 @@ public final class MediaFormat { * Sets the value of a ByteBuffer key. * <p> * If value is {@code null}, it sets a null value that behaves similarly to a missing key. - * This could be used prior to API level {@link android os.Build.VERSION_CODES#Q} to effectively + * This could be used prior to API level {@link android.os.Build.VERSION_CODES#Q} to effectively * remove a key. */ public final void setByteBuffer(@NonNull String name, @Nullable ByteBuffer bytes) { diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 248efde4902c..bbb03e77c8c9 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -934,6 +934,15 @@ public final class MediaRoute2Info implements Parcelable { } /** + * Returns whether this route supports {@link #FLAG_ROUTING_TYPE_REMOTE remote routing}. + * + * @hide + */ + public boolean supportsRemoteRouting() { + return (mRoutingTypeFlags & MediaRoute2Info.FLAG_ROUTING_TYPE_REMOTE) != 0; + } + + /** * Returns true if the route info has all of the required field. * A route is valid if and only if it is obtained from * {@link com.android.server.media.MediaRouterService}. diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp index 074973188c66..ee2cd6f3fcde 100644 --- a/native/android/dynamic_instrumentation_manager.cpp +++ b/native/android/dynamic_instrumentation_manager.cpp @@ -73,6 +73,7 @@ ADynamicInstrumentationManager_TargetProcess* ADynamicInstrumentationManager_Tar void ADynamicInstrumentationManager_TargetProcess_destroy( const ADynamicInstrumentationManager_TargetProcess* instance) { + if (instance == nullptr) return; delete instance; } @@ -104,6 +105,7 @@ ADynamicInstrumentationManager_MethodDescriptor_create(const char* fullyQualifie void ADynamicInstrumentationManager_MethodDescriptor_destroy( const ADynamicInstrumentationManager_MethodDescriptor* instance) { + if (instance == nullptr) return; delete instance; } @@ -135,6 +137,7 @@ uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOff void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) { + if (instance == nullptr) return; delete instance; } diff --git a/native/android/include_platform/android/dynamic_instrumentation_manager.h b/native/android/include_platform/android/dynamic_instrumentation_manager.h index ab9f37034a22..7bb7615bc3a1 100644 --- a/native/android/include_platform/android/dynamic_instrumentation_manager.h +++ b/native/android/include_platform/android/dynamic_instrumentation_manager.h @@ -40,9 +40,12 @@ typedef struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets * * @param uid of targeted process. * @param pid of targeted process. - * @param processName to disambiguate from corner cases that may arise from pid reuse. + * @param processName UTF-8 encoded string representing the same process as specified by `pid`. + * Supplied to disambiguate from corner cases that may arise from pid reuse. + * Referenced parameter must outlive the returned + * ADynamicInstrumentationManager_TargetProcess. */ -ADynamicInstrumentationManager_TargetProcess* _Nonnull +ADynamicInstrumentationManager_TargetProcess* _Nullable ADynamicInstrumentationManager_TargetProcess_create( uid_t uid, pid_t pid, const char* _Nonnull processName) __INTRODUCED_IN(36); /** @@ -51,22 +54,27 @@ ADynamicInstrumentationManager_TargetProcess* _Nonnull * @param instance returned from ADynamicInstrumentationManager_TargetProcess_create. */ void ADynamicInstrumentationManager_TargetProcess_destroy( - const ADynamicInstrumentationManager_TargetProcess* _Nonnull instance) __INTRODUCED_IN(36); + const ADynamicInstrumentationManager_TargetProcess* _Nullable instance) __INTRODUCED_IN(36); /** * Initializes an ADynamicInstrumentationManager_MethodDescriptor. Caller must clean up when they - * are done with ADynamicInstrumentationManager_MethodDescriptor_Destroy. + * are done with ADynamicInstrumentationManager_MethodDescriptor_destroy. * - * @param fullyQualifiedClassName fqcn of class containing the method. - * @param methodName - * @param fullyQualifiedParameters fqcn of parameters of the method's signature, or e.g. "int" for - * primitives. + * @param fullyQualifiedClassName UTF-8 encoded fqcn of class containing the method. Referenced + * parameter must outlive the returned + * ADynamicInstrumentationManager_MethodDescriptor. + * @param methodName UTF-8 encoded method name. Referenced parameter must outlive the returned + * ADynamicInstrumentationManager_MethodDescriptor. + * @param fullyQualifiedParameters UTF-8 encoded fqcn of parameters of the method's signature, + * or e.g. "int" for primitives. Referenced parameter should + * outlive the returned + * ADynamicInstrumentationManager_MethodDescriptor. * @param numParameters length of `fullyQualifiedParameters` array. */ -ADynamicInstrumentationManager_MethodDescriptor* _Nonnull +ADynamicInstrumentationManager_MethodDescriptor* _Nullable ADynamicInstrumentationManager_MethodDescriptor_create( const char* _Nonnull fullyQualifiedClassName, const char* _Nonnull methodName, - const char* _Nonnull fullyQualifiedParameters[_Nonnull], size_t numParameters) + const char* _Nonnull* _Nonnull fullyQualifiedParameters, size_t numParameters) __INTRODUCED_IN(36); /** * Clean up an ADynamicInstrumentationManager_MethodDescriptor. @@ -74,14 +82,16 @@ ADynamicInstrumentationManager_MethodDescriptor* _Nonnull * @param instance returned from ADynamicInstrumentationManager_MethodDescriptor_create. */ void ADynamicInstrumentationManager_MethodDescriptor_destroy( - const ADynamicInstrumentationManager_MethodDescriptor* _Nonnull instance) + const ADynamicInstrumentationManager_MethodDescriptor* _Nullable instance) __INTRODUCED_IN(36); /** * Get the containerPath calculated by * ADynamicInstrumentationManager_getExecutableMethodFileOffsets. * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. - * @return The OS path of the containing file. + * @return The OS path of the containing file as a UTF-8 string, which has the same lifetime + * as the ADynamicInstrumentationManager_ExecutableMethodFileOffsets instance passed + * as a param. */ const char* _Nullable ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath( const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) @@ -90,7 +100,8 @@ const char* _Nullable ADynamicInstrumentationManager_ExecutableMethodFileOffsets * Get the containerOffset calculated by * ADynamicInstrumentationManager_getExecutableMethodFileOffsets. * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. - * @return The offset of the containing file within the process' memory. + * @return The absolute address of the containing file within remote the process' virtual memory + * space. */ uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset( const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) @@ -98,7 +109,8 @@ uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainer /** * Get the methodOffset calculated by ADynamicInstrumentationManager_getExecutableMethodFileOffsets. * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets. - * @return The offset of the method within the containing file. + * @return The offset of the method within the container whose address is returned by + * ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset. */ uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset( const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) @@ -109,7 +121,7 @@ uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOff * @param instance returned from ADynamicInstrumentationManager_getExecutableMethodFileOffsets. */ void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( - const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance) + const ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nullable instance) __INTRODUCED_IN(36); /** * Provides ART metadata about the described java method within the target process. @@ -118,7 +130,9 @@ void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy( * @param methodDescriptor describes the targeted method. * @param out will be populated with the data if successful. A nullptr combined * with an OK status means that the program method is defined, but the offset - * info was unavailable because it is not AOT compiled. + * info was unavailable because it is not AOT compiled. Caller owns `out` and + * should clean it up with + * ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy. * @return status indicating success or failure. The values correspond to the `binder_exception_t` * enum values from <android/binder_status.h>. */ diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index e0bc15fe6e94..baae05b4ea03 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -1327,7 +1327,10 @@ public final class CardEmulation { /** * This method is called when an AID conflict is detected during an NFC transaction. This - * can happen when multiple services are registered for the same AID. + * can happen when multiple services are registered for the same AID. If your service is + * registered for this AID you may want to instruct users to bring your app to the + * foreground and ensure you call {@link #setPreferredService(Activity, ComponentName)} + * to ensure the transaction is routed to your service. * * @param aid The AID that is in conflict */ diff --git a/packages/NeuralNetworks/framework/Android.bp b/packages/NeuralNetworks/framework/Android.bp index 6f45daae0802..af071ba48b1f 100644 --- a/packages/NeuralNetworks/framework/Android.bp +++ b/packages/NeuralNetworks/framework/Android.bp @@ -19,10 +19,21 @@ package { filegroup { name: "framework-ondeviceintelligence-sources", srcs: [ - "java/**/*.aidl", - "java/**/*.java", + "module/java/**/*.aidl", + "module/java/**/*.java", + ], + visibility: [ + "//frameworks/base:__subpackages__", + "//packages/modules/NeuralNetworks:__subpackages__", + ], +} + +filegroup { + name: "framework-ondeviceintelligence-sources-platform", + srcs: [ + "platform/java/**/*.aidl", + "platform/java/**/*.java", ], - path: "java", visibility: [ "//frameworks/base:__subpackages__", "//packages/modules/NeuralNetworks:__subpackages__", diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/DownloadCallback.java index 95fb2888a3e9..95fb2888a3e9 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/DownloadCallback.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.aidl index 47cfb4a60dc4..47cfb4a60dc4 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.java index 88f4de2989e4..88f4de2989e4 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.aidl index c5b3532796cd..c5b3532796cd 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.java index 063cfb8c321e..063cfb8c321e 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ICancellationSignal.aidl index 1fe201f8f1f8..1fe201f8f1f8 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ICancellationSignal.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IDownloadCallback.aidl index 2d7ea1a7b016..2d7ea1a7b016 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IDownloadCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureCallback.aidl index 2e056926e400..2e056926e400 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl index 8688028743d7..8688028743d7 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl index 7e5eb57bbc4a..7e5eb57bbc4a 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl index fac5ec6064f8..fac5ec6064f8 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IProcessingSignal.aidl index 03946eebd40b..03946eebd40b 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IProcessingSignal.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IRemoteCallback.aidl index 6f07693dd39c..6f07693dd39c 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IRemoteCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IResponseCallback.aidl index 270b600e2de5..270b600e2de5 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IResponseCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl index 3e902405f3e0..3e902405f3e0 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl index 958bef0a93e0..958bef0a93e0 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.aidl index 6f6325408979..6f6325408979 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java index cae8db27a435..cae8db27a435 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java index 2881c9d217dc..2881c9d217dc 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java index 7d35dd7f2237..7d35dd7f2237 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java index dc0665a5cea7..dc0665a5cea7 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingCallback.java index e50d6b1fa97a..e50d6b1fa97a 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingCallback.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingSignal.java index 733f4fad96f4..733f4fad96f4 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingSignal.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java index 7ee2af7376ed..7ee2af7376ed 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.aidl index 599b337fd20f..599b337fd20f 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.aidl diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.java index 035cc4b365b5..035cc4b365b5 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.java diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/utils/BinderUtils.java index 2916f030e3d0..2916f030e3d0 100644 --- a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java +++ b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/utils/BinderUtils.java diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl index cba18c1ef36d..cba18c1ef36d 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl index 504fdd9b17f9..504fdd9b17f9 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl index 7ead8690abb4..7ead8690abb4 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl index 32a8a6a70406..32a8a6a70406 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl index 253df890b198..253df890b198 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java index 6907e2bdf2b3..6907e2bdf2b3 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index 315dbaf919e5..315dbaf919e5 100644 --- a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/DownloadCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/DownloadCallback.java new file mode 100644 index 000000000000..95fb2888a3e9 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/DownloadCallback.java @@ -0,0 +1,113 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.PersistableBundle; +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Callback functions used for feature downloading via the + * {@link OnDeviceIntelligenceManager#requestFeatureDownload}. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public interface DownloadCallback { + int DOWNLOAD_FAILURE_STATUS_UNKNOWN = 0; + + /** + * Sent when feature download could not succeed due to there being no available disk space on + * the device. + */ + int DOWNLOAD_FAILURE_STATUS_NOT_ENOUGH_DISK_SPACE = 1; + + /** + * Sent when feature download could not succeed due to a network error. + */ + int DOWNLOAD_FAILURE_STATUS_NETWORK_FAILURE = 2; + + /** + * Sent when feature download has been initiated already, hence no need to request download + * again. Caller can query {@link OnDeviceIntelligenceManager#getFeatureDetails} to check if + * download has been completed. + */ + int DOWNLOAD_FAILURE_STATUS_DOWNLOADING = 3; + + /** + * Sent when feature download did not start due to errors (e.g. remote exception of features not + * available). Caller can query {@link OnDeviceIntelligenceManager#getFeatureDetails} to check + * if feature-status is {@link FeatureDetails#FEATURE_STATUS_DOWNLOADABLE}. + */ + int DOWNLOAD_FAILURE_STATUS_UNAVAILABLE = 4; + + /** @hide */ + @IntDef(value = { + DOWNLOAD_FAILURE_STATUS_UNKNOWN, + DOWNLOAD_FAILURE_STATUS_NOT_ENOUGH_DISK_SPACE, + DOWNLOAD_FAILURE_STATUS_NETWORK_FAILURE, + DOWNLOAD_FAILURE_STATUS_DOWNLOADING, + DOWNLOAD_FAILURE_STATUS_UNAVAILABLE + }) + @Retention(RetentionPolicy.SOURCE) + @interface DownloadFailureStatus { + } + + /** + * Called when model download started properly. + * + * @param bytesToDownload the total bytes to be downloaded for this {@link Feature} + */ + default void onDownloadStarted(long bytesToDownload) { + } + + /** + * Called when model download failed. + * + * @param failureStatus the download failure status + * @param errorMessage the error message associated with the download failure + */ + void onDownloadFailed( + @DownloadFailureStatus int failureStatus, + @Nullable String errorMessage, + @NonNull PersistableBundle errorParams); + + /** + * Called when model download is in progress. + * + * @param totalBytesDownloaded the already downloaded bytes for this {@link Feature} + */ + default void onDownloadProgress(long totalBytesDownloaded) { + } + + /** + * Called when model download is completed. The remote implementation can populate any + * associated download params like file stats etc. in this callback to inform the client. + * + * @param downloadParams params containing info about the completed download. + */ + void onDownloadCompleted(@NonNull PersistableBundle downloadParams); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl new file mode 100644 index 000000000000..18494d754674 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + +/** + * @hide + */ +parcelable Feature; diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java new file mode 100644 index 000000000000..bcc56073e51c --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java @@ -0,0 +1,267 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; + +/** + * Represents a typical feature associated with on-device intelligence. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public final class Feature implements Parcelable { + private final int mId; + @Nullable + private final String mName; + @Nullable + private final String mModelName; + private final int mType; + private final int mVariant; + @NonNull + private final PersistableBundle mFeatureParams; + + /* package-private */ Feature( + int id, + @Nullable String name, + @Nullable String modelName, + int type, + int variant, + @NonNull PersistableBundle featureParams) { + this.mId = id; + this.mName = name; + this.mModelName = modelName; + this.mType = type; + this.mVariant = variant; + this.mFeatureParams = featureParams; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFeatureParams); + } + + /** Returns the unique and immutable identifier of this feature. */ + public int getId() { + return mId; + } + + /** Returns human-readable name of this feature. */ + public @Nullable String getName() { + return mName; + } + + /** Returns base model name of this feature. */ + public @Nullable String getModelName() { + return mModelName; + } + + /** Returns type identifier of this feature. */ + public int getType() { + return mType; + } + + /** Returns variant kind for this feature. */ + public int getVariant() { + return mVariant; + } + + public @NonNull PersistableBundle getFeatureParams() { + return mFeatureParams; + } + + @Override + public String toString() { + return "Feature { " + + "id = " + mId + ", " + + "name = " + mName + ", " + + "modelName = " + mModelName + ", " + + "type = " + mType + ", " + + "variant = " + mVariant + ", " + + "featureParams = " + mFeatureParams + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + Feature that = (Feature) o; + //noinspection PointlessBooleanExpression + return true + && mId == that.mId + && java.util.Objects.equals(mName, that.mName) + && java.util.Objects.equals(mModelName, that.mModelName) + && mType == that.mType + && mVariant == that.mVariant + && java.util.Objects.equals(mFeatureParams, that.mFeatureParams); + } + + @Override + public int hashCode() { + int _hash = 1; + _hash = 31 * _hash + mId; + _hash = 31 * _hash + java.util.Objects.hashCode(mName); + _hash = 31 * _hash + java.util.Objects.hashCode(mModelName); + _hash = 31 * _hash + mType; + _hash = 31 * _hash + mVariant; + _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureParams); + return _hash; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + byte flg = 0; + if (mName != null) flg |= 0x2; + if (mModelName != null) flg |= 0x4; + dest.writeByte(flg); + dest.writeInt(mId); + if (mName != null) dest.writeString(mName); + if (mModelName != null) dest.writeString(mModelName); + dest.writeInt(mType); + dest.writeInt(mVariant); + dest.writeTypedObject(mFeatureParams, flags); + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + /* package-private */ Feature(@NonNull Parcel in) { + byte flg = in.readByte(); + int id = in.readInt(); + String name = (flg & 0x2) == 0 ? null : in.readString(); + String modelName = (flg & 0x4) == 0 ? null : in.readString(); + int type = in.readInt(); + int variant = in.readInt(); + PersistableBundle featureParams = (PersistableBundle) in.readTypedObject( + PersistableBundle.CREATOR); + + this.mId = id; + this.mName = name; + this.mModelName = modelName; + this.mType = type; + this.mVariant = variant; + this.mFeatureParams = featureParams; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFeatureParams); + } + + public static final @NonNull Parcelable.Creator<Feature> CREATOR + = new Parcelable.Creator<Feature>() { + @Override + public Feature[] newArray(int size) { + return new Feature[size]; + } + + @Override + public Feature createFromParcel(@NonNull Parcel in) { + return new Feature(in); + } + }; + + /** + * A builder for {@link Feature} + */ + @SuppressWarnings("WeakerAccess") + public static final class Builder { + private int mId; + private @Nullable String mName; + private @Nullable String mModelName; + private int mType; + private int mVariant; + private @NonNull PersistableBundle mFeatureParams; + + private long mBuilderFieldsSet = 0L; + + /** + * Provides a builder instance to create a feature for given id. + * @param id the unique identifier for the feature. + */ + public Builder(int id) { + mId = id; + mFeatureParams = new PersistableBundle(); + } + + public @NonNull Builder setName(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mName = value; + return this; + } + + public @NonNull Builder setModelName(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mModelName = value; + return this; + } + + public @NonNull Builder setType(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mType = value; + return this; + } + + public @NonNull Builder setVariant(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; + mVariant = value; + return this; + } + + public @NonNull Builder setFeatureParams(@NonNull PersistableBundle value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20; + mFeatureParams = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull Feature build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x40; // Mark builder used + + Feature o = new Feature( + mId, + mName, + mModelName, + mType, + mVariant, + mFeatureParams); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x40) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl new file mode 100644 index 000000000000..0589bf8bacb9 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + +/** + * @hide + */ +parcelable FeatureDetails; diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java new file mode 100644 index 000000000000..0ee0cc34c512 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java @@ -0,0 +1,178 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcelable; +import android.os.PersistableBundle; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.text.MessageFormat; + +/** + * Represents a status of a requested {@link Feature}. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public final class FeatureDetails implements Parcelable { + @Status + private final int mFeatureStatus; + @NonNull + private final PersistableBundle mFeatureDetailParams; + + /** Invalid or unavailable {@code AiFeature}. */ + public static final int FEATURE_STATUS_UNAVAILABLE = 0; + + /** Feature can be downloaded on request. */ + public static final int FEATURE_STATUS_DOWNLOADABLE = 1; + + /** Feature is being downloaded. */ + public static final int FEATURE_STATUS_DOWNLOADING = 2; + + /** Feature is fully downloaded and ready to use. */ + public static final int FEATURE_STATUS_AVAILABLE = 3; + + /** Underlying service is unavailable and feature status cannot be fetched. */ + public static final int FEATURE_STATUS_SERVICE_UNAVAILABLE = 4; + + /** + * @hide + */ + @IntDef(value = { + FEATURE_STATUS_UNAVAILABLE, + FEATURE_STATUS_DOWNLOADABLE, + FEATURE_STATUS_DOWNLOADING, + FEATURE_STATUS_AVAILABLE, + FEATURE_STATUS_SERVICE_UNAVAILABLE + }) + @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) + @Retention(RetentionPolicy.SOURCE) + public @interface Status { + } + + public FeatureDetails( + @Status int featureStatus, + @NonNull PersistableBundle featureDetailParams) { + this.mFeatureStatus = featureStatus; + com.android.internal.util.AnnotationValidations.validate( + Status.class, null, mFeatureStatus); + this.mFeatureDetailParams = featureDetailParams; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFeatureDetailParams); + } + + public FeatureDetails( + @Status int featureStatus) { + this.mFeatureStatus = featureStatus; + com.android.internal.util.AnnotationValidations.validate( + Status.class, null, mFeatureStatus); + this.mFeatureDetailParams = new PersistableBundle(); + } + + + /** + * Returns an integer value associated with the feature status. + */ + public @Status int getFeatureStatus() { + return mFeatureStatus; + } + + + /** + * Returns a persistable bundle contain any additional status related params. + */ + public @NonNull PersistableBundle getFeatureDetailParams() { + return mFeatureDetailParams; + } + + @Override + public String toString() { + return MessageFormat.format("FeatureDetails '{' status = {0}, " + + "persistableBundle = {1} '}'", + mFeatureStatus, + mFeatureDetailParams); + } + + @Override + public boolean equals(@android.annotation.Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + FeatureDetails that = (FeatureDetails) o; + return mFeatureStatus == that.mFeatureStatus + && java.util.Objects.equals(mFeatureDetailParams, that.mFeatureDetailParams); + } + + @Override + public int hashCode() { + int _hash = 1; + _hash = 31 * _hash + mFeatureStatus; + _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureDetailParams); + return _hash; + } + + @Override + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + dest.writeInt(mFeatureStatus); + dest.writeTypedObject(mFeatureDetailParams, flags); + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + FeatureDetails(@NonNull android.os.Parcel in) { + int status = in.readInt(); + PersistableBundle persistableBundle = (PersistableBundle) in.readTypedObject( + PersistableBundle.CREATOR); + + this.mFeatureStatus = status; + com.android.internal.util.AnnotationValidations.validate( + Status.class, null, mFeatureStatus); + this.mFeatureDetailParams = persistableBundle; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFeatureDetailParams); + } + + + public static final @NonNull Parcelable.Creator<FeatureDetails> CREATOR = + new Parcelable.Creator<>() { + @Override + public FeatureDetails[] newArray(int size) { + return new FeatureDetails[size]; + } + + @Override + public FeatureDetails createFromParcel(@NonNull android.os.Parcel in) { + return new FeatureDetails(in); + } + }; + +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ICancellationSignal.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ICancellationSignal.aidl new file mode 100644 index 000000000000..1fe201f8f1f8 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ICancellationSignal.aidl @@ -0,0 +1,24 @@ +/* + * 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 android.app.ondeviceintelligence; + +/** + * @hide + */ +oneway interface ICancellationSignal { + void cancel(); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IDownloadCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IDownloadCallback.aidl new file mode 100644 index 000000000000..2d7ea1a7b016 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IDownloadCallback.aidl @@ -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 android.app.ondeviceintelligence; + +import android.os.PersistableBundle; + +/** + * Interface for Download callback to be passed onto service implementation, + * + * @hide + */ +oneway interface IDownloadCallback { + void onDownloadStarted(long bytesToDownload) = 1; + void onDownloadProgress(long bytesDownloaded) = 2; + void onDownloadFailed(int failureStatus, String errorMessage, in PersistableBundle errorParams) = 3; + void onDownloadCompleted(in PersistableBundle downloadParams) = 4; +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureCallback.aidl new file mode 100644 index 000000000000..2e056926e400 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureCallback.aidl @@ -0,0 +1,14 @@ +package android.app.ondeviceintelligence; + +import android.app.ondeviceintelligence.Feature; +import android.os.PersistableBundle; + +/** + * Interface for receiving a feature for the given identifier. + * + * @hide + */ +oneway interface IFeatureCallback { + void onSuccess(in Feature result) = 1; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl new file mode 100644 index 000000000000..8688028743d7 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl @@ -0,0 +1,14 @@ +package android.app.ondeviceintelligence; + +import android.app.ondeviceintelligence.FeatureDetails; +import android.os.PersistableBundle; + +/** + * Interface for receiving details about a given feature. . + * + * @hide + */ +oneway interface IFeatureDetailsCallback { + void onSuccess(in FeatureDetails result) = 1; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl new file mode 100644 index 000000000000..7e5eb57bbc4a --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl @@ -0,0 +1,15 @@ +package android.app.ondeviceintelligence; + +import java.util.List; +import android.app.ondeviceintelligence.Feature; +import android.os.PersistableBundle; + +/** + * Interface for receiving list of supported features. + * + * @hide + */ +oneway interface IListFeaturesCallback { + void onSuccess(in List<Feature> result) = 1; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl new file mode 100644 index 000000000000..1977a3923578 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.app.ondeviceintelligence; + + import com.android.internal.infra.AndroidFuture; + import android.os.ICancellationSignal; + import android.os.ParcelFileDescriptor; + import android.os.PersistableBundle; + import android.os.RemoteCallback; + import android.os.Bundle; + import android.app.ondeviceintelligence.Feature; + import android.app.ondeviceintelligence.FeatureDetails; + import android.app.ondeviceintelligence.InferenceInfo; + import java.util.List; + import android.app.ondeviceintelligence.IDownloadCallback; + import android.app.ondeviceintelligence.IListFeaturesCallback; + import android.app.ondeviceintelligence.IFeatureCallback; + import android.app.ondeviceintelligence.IFeatureDetailsCallback; + import android.app.ondeviceintelligence.IResponseCallback; + import android.app.ondeviceintelligence.IStreamingResponseCallback; + import android.app.ondeviceintelligence.IProcessingSignal; + import android.app.ondeviceintelligence.ITokenInfoCallback; + + + /** + * Interface for a OnDeviceIntelligenceManager for managing OnDeviceIntelligenceService and OnDeviceSandboxedInferenceService. + * + * @hide + */ +interface IOnDeviceIntelligenceManager { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void getVersion(in RemoteCallback remoteCallback) = 1; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void getFeature(in int featureId, in IFeatureCallback remoteCallback) = 2; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void listFeatures(in IListFeaturesCallback listFeaturesCallback) = 3; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void requestFeatureDownload(in Feature feature, in AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void requestTokenInfo(in Feature feature, in Bundle requestBundle, in AndroidFuture cancellationSignalFuture, + in ITokenInfoCallback tokenInfocallback) = 6; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void processRequest(in Feature feature, in Bundle requestBundle, int requestType, + in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, + in IResponseCallback responseCallback) = 7; + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") + void processRequestStreaming(in Feature feature, + in Bundle requestBundle, int requestType, in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, + in IStreamingResponseCallback streamingCallback) = 8; + + String getRemoteServicePackageName() = 9; + + List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) = 10; + } diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IProcessingSignal.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IProcessingSignal.aidl new file mode 100644 index 000000000000..03946eebd40b --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IProcessingSignal.aidl @@ -0,0 +1,14 @@ +package android.app.ondeviceintelligence; + +import android.os.PersistableBundle; + +/** +* Signal to provide to the remote implementation in context of a given request or +* feature specific event. +* +* @hide +*/ + +oneway interface IProcessingSignal { + void sendSignal(in PersistableBundle actionParams) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IRemoteCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IRemoteCallback.aidl new file mode 100644 index 000000000000..6f07693dd39c --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IRemoteCallback.aidl @@ -0,0 +1,24 @@ +/* +* Copyright 2024, The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package android.app.ondeviceintelligence; + +import android.os.Bundle; + +/* @hide */ +oneway interface IRemoteCallback { + void sendResult(in Bundle data); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IResponseCallback.aidl new file mode 100644 index 000000000000..270b600e2de5 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IResponseCallback.aidl @@ -0,0 +1,16 @@ +package android.app.ondeviceintelligence; + +import android.os.PersistableBundle; +import android.os.Bundle; +import android.os.RemoteCallback; + +/** + * Interface for a IResponseCallback for receiving response from on-device intelligence service. + * + * @hide + */ +oneway interface IResponseCallback { + void onSuccess(in Bundle resultBundle) = 1; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; + void onDataAugmentRequest(in Bundle processedContent, in RemoteCallback responseCallback) = 3; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl new file mode 100644 index 000000000000..3e902405f3e0 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl @@ -0,0 +1,18 @@ +package android.app.ondeviceintelligence; + +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.Bundle; + + +/** + * This callback is a streaming variant of {@link IResponseCallback}. + * + * @hide + */ +oneway interface IStreamingResponseCallback { + void onNewContent(in Bundle processedResult) = 1; + void onSuccess(in Bundle result) = 2; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 3; + void onDataAugmentRequest(in Bundle processedContent, in RemoteCallback responseCallback) = 4; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl new file mode 100644 index 000000000000..958bef0a93e0 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl @@ -0,0 +1,14 @@ +package android.app.ondeviceintelligence; + +import android.os.PersistableBundle; +import android.app.ondeviceintelligence.TokenInfo; + +/** + * Interface for receiving the token info of a request for a given feature. + * + * @hide + */ +oneway interface ITokenInfoCallback { + void onSuccess(in TokenInfo tokenInfo) = 1; + void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl new file mode 100644 index 000000000000..6d70fc4577a2 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + +/** + * @hide + */ +parcelable InferenceInfo; diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java new file mode 100644 index 000000000000..cae8db27a435 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java @@ -0,0 +1,220 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class represents the information related to an inference event to track the resource usage + * as a function of inference time. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) +public final class InferenceInfo implements Parcelable { + + /** + * Uid for the caller app. + */ + private final int uid; + + /** + * Inference start time (milliseconds from the epoch time). + */ + private final long startTimeMs; + + /** + * Inference end time (milliseconds from the epoch time). + */ + private final long endTimeMs; + + /** + * Suspended time in milliseconds. + */ + private final long suspendedTimeMs; + + /** + * Constructs an InferenceInfo object with the specified parameters. + * + * @param uid Uid for the caller app. + * @param startTimeMs Inference start time (milliseconds from the epoch time). + * @param endTimeMs Inference end time (milliseconds from the epoch time). + * @param suspendedTimeMs Suspended time in milliseconds. + */ + InferenceInfo(int uid, long startTimeMs, long endTimeMs, + long suspendedTimeMs) { + this.uid = uid; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; + this.suspendedTimeMs = suspendedTimeMs; + } + + /** + * Constructs an InferenceInfo object from a Parcel. + * + * @param in The Parcel to read the object's data from. + */ + private InferenceInfo(@NonNull Parcel in) { + uid = in.readInt(); + startTimeMs = in.readLong(); + endTimeMs = in.readLong(); + suspendedTimeMs = in.readLong(); + } + + + /** + * Writes the object's data to the provided Parcel. + * + * @param dest The Parcel to write the object's data to. + * @param flags Additional flags about how the object should be written. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(uid); + dest.writeLong(startTimeMs); + dest.writeLong(endTimeMs); + dest.writeLong(suspendedTimeMs); + } + + /** + * Returns the UID for the caller app. + * + * @return the UID for the caller app. + */ + public int getUid() { + return uid; + } + + /** + * Returns the inference start time in milliseconds from the epoch time. + * + * @return the inference start time in milliseconds from the epoch time. + */ + @CurrentTimeMillisLong + public long getStartTimeMillis() { + return startTimeMs; + } + + /** + * Returns the inference end time in milliseconds from the epoch time. + * + * @return the inference end time in milliseconds from the epoch time. + */ + @CurrentTimeMillisLong + public long getEndTimeMillis() { + return endTimeMs; + } + + /** + * Returns the suspended time in milliseconds. + * + * @return the suspended time in milliseconds. + */ + @CurrentTimeMillisLong + public long getSuspendedTimeMillis() { + return suspendedTimeMs; + } + + @Override + public int describeContents() { + return 0; + } + + + public static final @android.annotation.NonNull Parcelable.Creator<InferenceInfo> CREATOR + = new Parcelable.Creator<InferenceInfo>() { + @Override + public InferenceInfo[] newArray(int size) { + return new InferenceInfo[size]; + } + + @Override + public InferenceInfo createFromParcel(@android.annotation.NonNull Parcel in) { + return new InferenceInfo(in); + } + }; + + /** + * Builder class for creating instances of {@link InferenceInfo}. + */ + public static final class Builder { + private final int uid; + private long startTimeMs; + private long endTimeMs; + private long suspendedTimeMs; + + /** + * Provides a builder instance to create a InferenceInfo for given caller uid. + * + * @param uid the caller uid associated with the inference info. + */ + public Builder(int uid) { + this.uid = uid; + } + + /** + * Sets the inference start time in milliseconds from the epoch time. + * + * @param startTimeMs the inference start time in milliseconds from the epoch time. + * @return the Builder instance. + */ + public @NonNull Builder setStartTimeMillis(@CurrentTimeMillisLong long startTimeMs) { + this.startTimeMs = startTimeMs; + return this; + } + + /** + * Sets the inference end time in milliseconds from the epoch time. + * + * @param endTimeMs the inference end time in milliseconds from the epoch time. + * @return the Builder instance. + */ + public @NonNull Builder setEndTimeMillis(@CurrentTimeMillisLong long endTimeMs) { + this.endTimeMs = endTimeMs; + return this; + } + + /** + * Sets the suspended time in milliseconds. + * + * @param suspendedTimeMs the suspended time in milliseconds. + * @return the Builder instance. + */ + public @NonNull Builder setSuspendedTimeMillis(@CurrentTimeMillisLong long suspendedTimeMs) { + this.suspendedTimeMs = suspendedTimeMs; + return this; + } + + /** + * Builds and returns an instance of {@link InferenceInfo}. + * + * @return an instance of {@link InferenceInfo}. + */ + public @NonNull InferenceInfo build() { + return new InferenceInfo(uid, startTimeMs, endTimeMs, + suspendedTimeMs); + } + } +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java new file mode 100644 index 000000000000..2881c9d217dc --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java @@ -0,0 +1,200 @@ +/* + * 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 android.app.ondeviceintelligence; + + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.PersistableBundle; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Exception type to be used for errors related to on-device intelligence system service with + * appropriate error code. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public class OnDeviceIntelligenceException extends Exception { + + public static final int PROCESSING_ERROR_UNKNOWN = 1; + + /** Request passed contains bad data for e.g. format. */ + public static final int PROCESSING_ERROR_BAD_DATA = 2; + + /** Bad request for inputs. */ + public static final int PROCESSING_ERROR_BAD_REQUEST = 3; + + /** Whole request was classified as not safe, and no response will be generated. */ + public static final int PROCESSING_ERROR_REQUEST_NOT_SAFE = 4; + + /** Underlying processing encountered an error and failed to compute results. */ + public static final int PROCESSING_ERROR_COMPUTE_ERROR = 5; + + /** Encountered an error while performing IPC */ + public static final int PROCESSING_ERROR_IPC_ERROR = 6; + + /** Request was cancelled either by user signal or by the underlying implementation. */ + public static final int PROCESSING_ERROR_CANCELLED = 7; + + /** Underlying processing in the remote implementation is not available. */ + public static final int PROCESSING_ERROR_NOT_AVAILABLE = 8; + + /** The service is currently busy. Callers should retry with exponential backoff. */ + public static final int PROCESSING_ERROR_BUSY = 9; + + /** Something went wrong with safety classification service. */ + public static final int PROCESSING_ERROR_SAFETY_ERROR = 10; + + /** Response generated was classified unsafe. */ + public static final int PROCESSING_ERROR_RESPONSE_NOT_SAFE = 11; + + /** Request is too large to be processed. */ + public static final int PROCESSING_ERROR_REQUEST_TOO_LARGE = 12; + + /** Inference suspended so that higher-priority inference can run. */ + public static final int PROCESSING_ERROR_SUSPENDED = 13; + + /** + * Underlying processing encountered an internal error, like a violated precondition + * . + */ + public static final int PROCESSING_ERROR_INTERNAL = 14; + + /** + * The processing was not able to be passed on to the remote implementation, as the + * service + * was unavailable. + */ + public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15; + /** + * Error code returned when the OnDeviceIntelligenceManager service is unavailable. + */ + public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 100; + + /** + * The connection to remote service failed and the processing state could not be updated. + */ + public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 200; + + + /** + * Error code associated with the on-device intelligence failure. + * + * @hide + */ + @IntDef( + value = { + PROCESSING_ERROR_UNKNOWN, + PROCESSING_ERROR_BAD_DATA, + PROCESSING_ERROR_BAD_REQUEST, + PROCESSING_ERROR_REQUEST_NOT_SAFE, + PROCESSING_ERROR_COMPUTE_ERROR, + PROCESSING_ERROR_IPC_ERROR, + PROCESSING_ERROR_CANCELLED, + PROCESSING_ERROR_NOT_AVAILABLE, + PROCESSING_ERROR_BUSY, + PROCESSING_ERROR_SAFETY_ERROR, + PROCESSING_ERROR_RESPONSE_NOT_SAFE, + PROCESSING_ERROR_REQUEST_TOO_LARGE, + PROCESSING_ERROR_SUSPENDED, + PROCESSING_ERROR_INTERNAL, + PROCESSING_ERROR_SERVICE_UNAVAILABLE, + ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + PROCESSING_UPDATE_STATUS_CONNECTION_FAILED + }) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @interface OnDeviceIntelligenceError { + } + + private final int mErrorCode; + private final PersistableBundle mErrorParams; + + /** Returns the error code of the exception. */ + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error params of the exception. */ + @NonNull + public PersistableBundle getErrorParams() { + return mErrorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code, error message and + * error params. + * + * @param errorCode The error code. + * @param errorMessage The error message. + * @param errorParams The error params. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, @NonNull String errorMessage, + @NonNull PersistableBundle errorParams) { + super(errorMessage); + this.mErrorCode = errorCode; + this.mErrorParams = errorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code and error params. + * + * @param errorCode The error code. + * @param errorParams The error params. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, + @NonNull PersistableBundle errorParams) { + this.mErrorCode = errorCode; + this.mErrorParams = errorParams; + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code and error message. + * + * @param errorCode The error code. + * @param errorMessage The error message. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode, @NonNull String errorMessage) { + super(errorMessage); + this.mErrorCode = errorCode; + this.mErrorParams = new PersistableBundle(); + } + + /** + * Creates a new OnDeviceIntelligenceException with the specified error code. + * + * @param errorCode The error code. + */ + public OnDeviceIntelligenceException( + @OnDeviceIntelligenceError int errorCode) { + this.mErrorCode = errorCode; + this.mErrorParams = new PersistableBundle(); + } +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java new file mode 100644 index 000000000000..7d35dd7f2237 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java @@ -0,0 +1,54 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class for performing registration for OnDeviceIntelligence service. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public class OnDeviceIntelligenceFrameworkInitializer { + private OnDeviceIntelligenceFrameworkInitializer() { + } + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers + * OnDeviceIntelligence service to {@link Context}, so that {@link Context#getSystemService} can + * return them. + * + * @throws IllegalStateException if this is called from anywhere besides {@link + * SystemServiceRegistry} + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService(Context.ON_DEVICE_INTELLIGENCE_SERVICE, + OnDeviceIntelligenceManager.class, + (context, serviceBinder) -> { + IOnDeviceIntelligenceManager manager = + IOnDeviceIntelligenceManager.Stub.asInterface(serviceBinder); + return new OnDeviceIntelligenceManager(context, manager); + }); + } +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java new file mode 100644 index 000000000000..78cf1d7a611b --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java @@ -0,0 +1,642 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; + +import android.Manifest; +import android.annotation.CallbackExecutor; +import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.system.OsConstants; +import android.util.Log; + +import com.android.internal.infra.AndroidFuture; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.LongConsumer; + +/** + * Allows granted apps to manage on-device intelligence service configured on the device. Typical + * calling pattern will be to query and setup a required feature before proceeding to request + * processing. + * + * The contracts in this Manager class are designed to be open-ended in general, to allow + * interoperability. Therefore, it is recommended that implementations of this system-service + * expose this API to the clients via a separate sdk or library which has more defined contract. + * + * @hide + */ +@SystemApi +@SystemService(Context.ON_DEVICE_INTELLIGENCE_SERVICE) +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public final class OnDeviceIntelligenceManager { + /** + * @hide + */ + public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey"; + + /** + * @hide + */ + public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY = + "AugmentRequestContentBundleKey"; + + private static final String TAG = "OnDeviceIntelligence"; + private final Context mContext; + private final IOnDeviceIntelligenceManager mService; + + /** + * @hide + */ + public OnDeviceIntelligenceManager(Context context, IOnDeviceIntelligenceManager service) { + mContext = context; + mService = service; + } + + /** + * Asynchronously get the version of the underlying remote implementation. + * + * @param versionConsumer consumer to populate the version of remote implementation. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void getVersion( + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull LongConsumer versionConsumer) { + try { + RemoteCallback callback = new RemoteCallback(result -> { + if (result == null) { + Binder.withCleanCallingIdentity( + () -> callbackExecutor.execute(() -> versionConsumer.accept(0))); + } + long version = result.getLong(API_VERSION_BUNDLE_KEY); + Binder.withCleanCallingIdentity( + () -> callbackExecutor.execute(() -> versionConsumer.accept(version))); + }); + mService.getVersion(callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Get package name configured for providing the remote implementation for this system service. + */ + @Nullable + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public String getRemoteServicePackageName() { + String result; + try { + result = mService.getRemoteServicePackageName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return result; + } + + /** + * Asynchronously get feature for a given id. + * + * @param featureId the identifier pointing to the feature. + * @param featureReceiver callback to populate the feature object for given identifier. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void getFeature( + int featureId, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull OutcomeReceiver<Feature, OnDeviceIntelligenceException> featureReceiver) { + try { + IFeatureCallback callback = + new IFeatureCallback.Stub() { + @Override + public void onSuccess(Feature result) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureReceiver.onResult(result))); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureReceiver.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage, errorParams)))); + } + }; + mService.getFeature(featureId, callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Asynchronously get a list of features that are supported for the caller. + * + * @param featureListReceiver callback to populate the list of features. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void listFeatures( + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull OutcomeReceiver<List<Feature>, OnDeviceIntelligenceException> featureListReceiver) { + try { + IListFeaturesCallback callback = + new IListFeaturesCallback.Stub() { + @Override + public void onSuccess(List<Feature> result) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureListReceiver.onResult(result))); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureListReceiver.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage, errorParams)))); + } + }; + mService.listFeatures(callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * This method should be used to fetch details about a feature which need some additional + * computation, that can be inefficient to return in all calls to {@link #getFeature}. Callers + * and implementation can utilize the {@link Feature#getFeatureParams()} to pass hint on what + * details are expected by the caller. + * + * @param feature the feature to check status for. + * @param featureDetailsReceiver callback to populate the feature details to. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void getFeatureDetails(@NonNull Feature feature, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull OutcomeReceiver<FeatureDetails, OnDeviceIntelligenceException> featureDetailsReceiver) { + try { + IFeatureDetailsCallback callback = new IFeatureDetailsCallback.Stub() { + + @Override + public void onSuccess(FeatureDetails result) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureDetailsReceiver.onResult(result))); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> featureDetailsReceiver.onError( + new OnDeviceIntelligenceException(errorCode, + errorMessage, errorParams)))); + } + }; + mService.getFeatureDetails(feature, callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * This method handles downloading all model and config files required to process requests + * sent against a given feature. The caller can listen to updates on the download status via + * the callback. + * + * Note: If a feature was already requested for downloaded previously, the onDownloadFailed + * callback would be invoked with {@link DownloadCallback#DOWNLOAD_FAILURE_STATUS_DOWNLOADING}. + * In such cases, clients should query the feature status via {@link #getFeatureDetails} to + * check on the feature's download status. + * + * @param feature feature to request download for. + * @param callback callback to populate updates about download status. + * @param cancellationSignal signal to invoke cancellation on the operation in the remote + * implementation. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void requestFeatureDownload(@NonNull Feature feature, + @Nullable CancellationSignal cancellationSignal, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull DownloadCallback callback) { + try { + IDownloadCallback downloadCallback = new IDownloadCallback.Stub() { + + @Override + public void onDownloadStarted(long bytesToDownload) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> callback.onDownloadStarted(bytesToDownload))); + } + + @Override + public void onDownloadProgress(long bytesDownloaded) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> callback.onDownloadProgress(bytesDownloaded))); + } + + @Override + public void onDownloadFailed(int failureStatus, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> callback.onDownloadFailed(failureStatus, errorMessage, + errorParams))); + } + + @Override + public void onDownloadCompleted(PersistableBundle downloadParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> callback.onDownloadCompleted(downloadParams))); + } + }; + + mService.requestFeatureDownload(feature, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + downloadCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * The methods computes the token related information for a given request payload using the + * provided {@link Feature}. + * + * @param feature feature associated with the request. + * @param request request and associated params represented by the Bundle + * data. + * @param outcomeReceiver callback to populate the token info or exception in case of + * failure. + * @param cancellationSignal signal to invoke cancellation on the operation in the remote + * implementation. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void requestTokenInfo(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, + @Nullable CancellationSignal cancellationSignal, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull OutcomeReceiver<TokenInfo, + OnDeviceIntelligenceException> outcomeReceiver) { + try { + ITokenInfoCallback callback = new ITokenInfoCallback.Stub() { + @Override + public void onSuccess(TokenInfo tokenInfo) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> outcomeReceiver.onResult(tokenInfo))); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> outcomeReceiver.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage, errorParams)))); + } + }; + + mService.requestTokenInfo(feature, request, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Asynchronously Process a request based on the associated params, to populate a + * response in + * {@link OutcomeReceiver#onResult} callback or failure callback status code if there + * was a + * failure. + * + * @param feature feature associated with the request. + * @param request request and associated params represented by the Bundle + * data. + * @param requestType type of request being sent for processing the content. + * @param cancellationSignal signal to invoke cancellation. + * @param processingSignal signal to send custom signals in the + * remote implementation. + * @param callbackExecutor executor to run the callback on. + * @param processingCallback callback to populate the response content and + * associated params. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void processRequest(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, + @RequestType int requestType, + @Nullable CancellationSignal cancellationSignal, + @Nullable ProcessingSignal processingSignal, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ProcessingCallback processingCallback) { + try { + IResponseCallback callback = new IResponseCallback.Stub() { + @Override + public void onSuccess(@InferenceParams Bundle result) { + Binder.withCleanCallingIdentity(() -> { + callbackExecutor.execute(() -> processingCallback.onResult(result)); + }); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> processingCallback.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage, errorParams)))); + } + + @Override + public void onDataAugmentRequest(@NonNull @InferenceParams Bundle request, + @NonNull RemoteCallback contentCallback) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> processingCallback.onDataAugmentRequest(request, result -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, result); + callbackExecutor.execute(() -> contentCallback.sendResult(bundle)); + }))); + } + }; + + + mService.processRequest(feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), + callback); + + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Variation of {@link #processRequest} that asynchronously processes a request in a + * streaming + * fashion, where new content is pushed to caller in chunks via the + * {@link StreamingProcessingCallback#onPartialResult}. After the streaming is complete, + * the service should call {@link StreamingProcessingCallback#onResult} and can optionally + * populate the complete the full response {@link Bundle} as part of the callback in cases + * when the final response contains an enhanced aggregation of the contents already + * streamed. + * + * @param feature feature associated with the request. + * @param request request and associated params represented by the Bundle + * data. + * @param requestType type of request being sent for processing the content. + * @param cancellationSignal signal to invoke cancellation. + * @param processingSignal signal to send custom signals in the + * remote implementation. + * @param streamingProcessingCallback streaming callback to populate the response content and + * associated params. + * @param callbackExecutor executor to run the callback on. + */ + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void processRequestStreaming(@NonNull Feature feature, + @NonNull @InferenceParams Bundle request, + @RequestType int requestType, + @Nullable CancellationSignal cancellationSignal, + @Nullable ProcessingSignal processingSignal, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull StreamingProcessingCallback streamingProcessingCallback) { + try { + IStreamingResponseCallback callback = new IStreamingResponseCallback.Stub() { + @Override + public void onNewContent(@InferenceParams Bundle result) { + Binder.withCleanCallingIdentity(() -> { + callbackExecutor.execute( + () -> streamingProcessingCallback.onPartialResult(result)); + }); + } + + @Override + public void onSuccess(@InferenceParams Bundle result) { + Binder.withCleanCallingIdentity(() -> { + callbackExecutor.execute( + () -> streamingProcessingCallback.onResult(result)); + }); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) { + Binder.withCleanCallingIdentity(() -> { + callbackExecutor.execute( + () -> streamingProcessingCallback.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage, errorParams))); + }); + } + + + @Override + public void onDataAugmentRequest(@NonNull @InferenceParams Bundle content, + @NonNull RemoteCallback contentCallback) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> streamingProcessingCallback.onDataAugmentRequest(content, + contentResponse -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, + contentResponse); + callbackExecutor.execute( + () -> contentCallback.sendResult(bundle)); + }))); + } + }; + + mService.processRequestStreaming( + feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), + callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * This is primarily intended to be used to attribute/blame on-device intelligence power usage, + * via the configured remote implementation, to its actual caller. + * + * @param startTimeEpochMillis epoch millis used to filter the InferenceInfo events. + * @return InferenceInfo events since the passed in startTimeEpochMillis. + */ + @RequiresPermission(Manifest.permission.DUMP) + @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) + public @NonNull List<InferenceInfo> getLatestInferenceInfo(@CurrentTimeMillisLong long startTimeEpochMillis) { + try { + return mService.getLatestInferenceInfo(startTimeEpochMillis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** Request inference with provided Bundle and Params. */ + public static final int REQUEST_TYPE_INFERENCE = 0; + + /** + * Prepares the remote implementation environment for e.g.loading inference runtime etc + * .which + * are time consuming beforehand to remove overhead and allow quick processing of requests + * thereof. + */ + public static final int REQUEST_TYPE_PREPARE = 1; + + /** Request Embeddings of the passed-in Bundle. */ + public static final int REQUEST_TYPE_EMBEDDINGS = 2; + + /** + * @hide + */ + @IntDef(value = { + REQUEST_TYPE_INFERENCE, + REQUEST_TYPE_PREPARE, + REQUEST_TYPE_EMBEDDINGS + }) + @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, + ElementType.FIELD}) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestType { + } + + /** + * {@link Bundle}s annotated with this type will be validated that they are in-effect read-only + * when passed via Binder IPC. Following restrictions apply : + * <ul> + * <li> {@link PersistableBundle}s are allowed.</li> + * <li> Any primitive types or their collections can be added as usual.</li> + * <li>IBinder objects should *not* be added.</li> + * <li>Parcelable data which has no active-objects, should be added as + * {@link Bundle#putByteArray}</li> + * <li>Parcelables have active-objects, only following types will be allowed</li> + * <ul> + * <li>{@link android.os.ParcelFileDescriptor} opened in + * {@link android.os.ParcelFileDescriptor#MODE_READ_ONLY}</li> + * </ul> + * </ul> + * + * In all other scenarios the system-server might throw a + * {@link android.os.BadParcelableException} if the Bundle validation fails. + * + * @hide + */ + @Target({ElementType.PARAMETER, ElementType.FIELD}) + public @interface StateParams { + } + + /** + * This is an extension of {@link StateParams} but for purpose of inference few other types are + * also allowed as read-only, as listed below. + * + * <li>{@link Bitmap} set as immutable.</li> + * <li>{@link android.database.CursorWindow}</li> + * <li>{@link android.os.SharedMemory} set to {@link OsConstants#PROT_READ}</li> + * </ul> + * </ul> + * + * In all other scenarios the system-server might throw a + * {@link android.os.BadParcelableException} if the Bundle validation fails. + * + * @hide + */ + @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE}) + public @interface InferenceParams { + } + + /** + * This is an extension of {@link StateParams} with the exception that it allows writing + * {@link Bitmap} as part of the response. + * + * In all other scenarios the system-server might throw a + * {@link android.os.BadParcelableException} if the Bundle validation fails. + * + * @hide + */ + @Target({ElementType.PARAMETER, ElementType.FIELD}) + public @interface ResponseParams { + } + + @Nullable + private static AndroidFuture<IBinder> configureRemoteCancellationFuture( + @Nullable CancellationSignal cancellationSignal, + @NonNull Executor callbackExecutor) { + if (cancellationSignal == null) { + return null; + } + AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>(); + cancellationFuture.whenCompleteAsync( + (cancellationTransport, error) -> { + if (error != null || cancellationTransport == null) { + Log.e(TAG, "Unable to receive the remote cancellation signal.", error); + } else { + cancellationSignal.setRemote( + ICancellationSignal.Stub.asInterface(cancellationTransport)); + } + }, callbackExecutor); + return cancellationFuture; + } + + @Nullable + private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture( + ProcessingSignal processingSignal, Executor executor) { + if (processingSignal == null) { + return null; + } + AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>(); + processingSignalFuture.whenCompleteAsync( + (transport, error) -> { + if (error != null || transport == null) { + Log.e(TAG, "Unable to receive the remote processing signal.", error); + } else { + processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport)); + } + }, executor); + return processingSignalFuture; + } + + +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingCallback.java new file mode 100644 index 000000000000..e50d6b1fa97a --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingCallback.java @@ -0,0 +1,72 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.ResponseParams; + +import java.util.function.Consumer; + +/** + * Callback to populate the processed response or any error that occurred during the + * request processing. This callback also provides a method to request additional data to be + * augmented to the request-processing, using the partial response that was already + * processed in the remote implementation. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public interface ProcessingCallback { + /** + * Invoked when request has been processed and result is ready to be propagated to the + * caller. + * + * @param result Response to be passed as a result. + */ + void onResult(@NonNull @ResponseParams Bundle result); + + /** + * Called when the request processing fails. The failure details are indicated by the + * {@link OnDeviceIntelligenceException} passed as an argument to this method. + * + * @param error An exception with more details about the error that occurred. + */ + void onError(@NonNull OnDeviceIntelligenceException error); + + /** + * Callback to be invoked in cases where the remote service needs to perform retrieval or + * transformation operations based on a partially processed request, in order to augment the + * final response, by using the additional context sent via this callback. + * + * @param processedContent The content payload that should be used to augment ongoing request. + * @param contentConsumer The augmentation data that should be sent to remote + * service for further processing a request. Bundle passed in here is + * expected to be non-null or EMPTY when there is no response. + */ + default void onDataAugmentRequest( + @NonNull @ResponseParams Bundle processedContent, + @NonNull Consumer<@InferenceParams Bundle> contentConsumer) { + contentConsumer.accept(Bundle.EMPTY); + } +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingSignal.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingSignal.java new file mode 100644 index 000000000000..733f4fad96f4 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingSignal.java @@ -0,0 +1,225 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * A signal to perform orchestration actions on the inference and optionally receive a output about + * the result of the signal. This is an extension of {@link android.os.CancellationSignal}. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public final class ProcessingSignal { + private final Object mLock = new Object(); + + private static final int MAX_QUEUE_SIZE = 10; + + @GuardedBy("mLock") + private final ArrayDeque<PersistableBundle> mActionParamsQueue; + + @GuardedBy("mLock") + private IProcessingSignal mRemote; + + private OnProcessingSignalCallback mOnProcessingSignalCallback; + private Executor mExecutor; + + public ProcessingSignal() { + mActionParamsQueue = new ArrayDeque<>(MAX_QUEUE_SIZE); + } + + /** + * Interface definition for a callback to be invoked when processing signals are received. + */ + public interface OnProcessingSignalCallback { + /** + * Called when a custom signal was received. + * This method allows the receiver to provide logic to be executed based on the signal + * received. + * + * @param actionParams Parameters for the signal in the form of a {@link PersistableBundle}. + */ + + void onSignalReceived(@NonNull PersistableBundle actionParams); + } + + + /** + * Sends a custom signal with the provided parameters. If there are multiple concurrent + * requests to this method, the actionParams are queued in a blocking fashion, in the order they + * are received. + * + * It also signals the remote callback + * with the same params if already configured, if not the action is queued to be sent when a + * remote is configured. Similarly, on the receiver side, the callback will be invoked if + * already set, if not all actions are queued to be sent to callback when it is set. + * + * @param actionParams Parameters for the signal. + */ + public void sendSignal(@NonNull PersistableBundle actionParams) { + final OnProcessingSignalCallback callback; + final IProcessingSignal remote; + synchronized (mLock) { + if (mActionParamsQueue.size() > MAX_QUEUE_SIZE) { + throw new RuntimeException( + "Maximum actions that can be queued are : " + MAX_QUEUE_SIZE); + } + + mActionParamsQueue.add(actionParams); + callback = mOnProcessingSignalCallback; + remote = mRemote; + + if (callback != null) { + while (!mActionParamsQueue.isEmpty()) { + PersistableBundle params = mActionParamsQueue.removeFirst(); + mExecutor.execute( + () -> callback.onSignalReceived(params)); + } + } + if (remote != null) { + while (!mActionParamsQueue.isEmpty()) { + try { + remote.sendSignal(mActionParamsQueue.removeFirst()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + } + } + + + /** + * Sets the processing signal callback to be called when signals are received. + * + * This method is intended to be used by the recipient of a processing signal + * such as the remote implementation in + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle + * processing signals while performing a long-running operation. This method is not + * intended to be used by the caller themselves. + * + * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback + * is invoked immediately and all previously queued actions are passed to remote signal. + * + * This method is guaranteed that the callback will not be called after it + * has been removed. + * + * @param callback The processing signal callback, or null to remove the current callback. + * @param executor Executor to the run the callback methods on. + */ + public void setOnProcessingSignalCallback( + @NonNull @CallbackExecutor Executor executor, + @Nullable OnProcessingSignalCallback callback) { + Objects.requireNonNull(executor); + synchronized (mLock) { + if (mOnProcessingSignalCallback == callback) { + return; + } + + mOnProcessingSignalCallback = callback; + mExecutor = executor; + if (callback == null || mActionParamsQueue.isEmpty()) { + return; + } + + while (!mActionParamsQueue.isEmpty()) { + PersistableBundle params = mActionParamsQueue.removeFirst(); + mExecutor.execute(() -> callback.onSignalReceived(params)); + } + } + } + + /** + * Sets the remote transport. + * + * If there are actions queued from {@link ProcessingSignal#sendSignal}, they are also + * sequentially sent to the configured remote. + * + * This method guarantees that the remote transport will not be called after it + * has been removed. + * + * @param remote The remote transport, or null to remove. + * @hide + */ + void setRemote(IProcessingSignal remote) { + synchronized (mLock) { + mRemote = remote; + if (mActionParamsQueue.isEmpty() || remote == null) { + return; + } + + while (!mActionParamsQueue.isEmpty()) { + try { + remote.sendSignal(mActionParamsQueue.removeFirst()); + } catch (RemoteException e) { + throw new RuntimeException("Failed to send action to remote signal", e); + } + } + } + } + + /** + * Creates a transport that can be returned back to the caller of + * a Binder function and subsequently used to dispatch a processing signal. + * + * @return The new processing signal transport. + * @hide + */ + public static IProcessingSignal createTransport() { + return new Transport(); + } + + /** + * Given a locally created transport, returns its associated processing signal. + * + * @param transport The locally created transport, or null if none. + * @return The associated processing signal, or null if none. + * @hide + */ + public static ProcessingSignal fromTransport(IProcessingSignal transport) { + if (transport instanceof Transport) { + return ((Transport) transport).mProcessingSignal; + } + return null; + } + + private static final class Transport extends IProcessingSignal.Stub { + final ProcessingSignal mProcessingSignal = new ProcessingSignal(); + + @Override + public void sendSignal(PersistableBundle actionParams) { + mProcessingSignal.sendSignal(actionParams); + } + } + +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java new file mode 100644 index 000000000000..7ee2af7376ed --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java @@ -0,0 +1,41 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.ResponseParams; + +/** + * Streaming variant of {@link ProcessingCallback} to populate response while processing a given + * request, possibly in chunks to provide a async processing behaviour to the caller. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public interface StreamingProcessingCallback extends ProcessingCallback { + /** + * Callback that would be invoked when a part of the response i.e. some response is + * already processed, and needs to be passed onto the caller. + */ + void onPartialResult(@NonNull @ResponseParams Bundle partialResult); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl new file mode 100644 index 000000000000..2c19c1eb246c --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.ondeviceintelligence; + +/** + * @hide + */ +parcelable TokenInfo; diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.java new file mode 100644 index 000000000000..035cc4b365b5 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.java @@ -0,0 +1,94 @@ +/* + * 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 android.app.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; + +/** + * This class is used to provide a token count response for the + * {@link OnDeviceIntelligenceManager#requestTokenInfo} outcome receiver. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public final class TokenInfo implements Parcelable { + private final long mCount; + private final PersistableBundle mInfoParams; + + /** + * Construct a token count using the count value and associated params. + */ + public TokenInfo(long count, @NonNull PersistableBundle persistableBundle) { + this.mCount = count; + mInfoParams = persistableBundle; + } + + /** + * Construct a token count using the count value. + */ + public TokenInfo(long count) { + this.mCount = count; + this.mInfoParams = new PersistableBundle(); + } + + /** + * Returns the token count associated with a request payload. + */ + public long getCount() { + return mCount; + } + + /** + * Returns the params representing token info. + */ + @NonNull + public PersistableBundle getInfoParams() { + return mInfoParams; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mCount); + dest.writePersistableBundle(mInfoParams); + } + + public static final @NonNull Parcelable.Creator<TokenInfo> CREATOR + = new Parcelable.Creator<>() { + @Override + public TokenInfo[] newArray(int size) { + return new TokenInfo[size]; + } + + @Override + public TokenInfo createFromParcel(@NonNull Parcel in) { + return new TokenInfo(in.readLong(), in.readPersistableBundle()); + } + }; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/utils/BinderUtils.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/utils/BinderUtils.java new file mode 100644 index 000000000000..2916f030e3d0 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/utils/BinderUtils.java @@ -0,0 +1,96 @@ +/* + * 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 android.app.ondeviceintelligence.utils; + +import android.annotation.NonNull; +import android.os.Binder; + +import java.util.function.Supplier; + +/** + * Collection of utilities for {@link Binder} and related classes. + * @hide + */ +public class BinderUtils { + /** + * Convenience method for running the provided action enclosed in + * {@link Binder#clearCallingIdentity}/{@link Binder#restoreCallingIdentity} + * + * Any exception thrown by the given action will be caught and rethrown after the call to + * {@link Binder#restoreCallingIdentity} + * + * Note that this is copied from Binder#withCleanCallingIdentity with minor changes + * since it is not public. + * + * @hide + */ + public static final <T extends Exception> void withCleanCallingIdentity( + @NonNull ThrowingRunnable<T> action) throws T { + final long callingIdentity = Binder.clearCallingIdentity(); + try { + action.run(); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + /** + * Like a Runnable, but declared to throw an exception. + * + * @param <T> The exception class which is declared to be thrown. + */ + @FunctionalInterface + public interface ThrowingRunnable<T extends Exception> { + /** @see java.lang.Runnable */ + void run() throws T; + } + + /** + * Convenience method for running the provided action enclosed in + * {@link Binder#clearCallingIdentity}/{@link Binder#restoreCallingIdentity} returning the + * result. + * + * <p>Any exception thrown by the given action will be caught and rethrown after + * the call to {@link Binder#restoreCallingIdentity}. + * + * Note that this is copied from Binder#withCleanCallingIdentity with minor changes + * since it is not public. + * + * @hide + */ + public static final <T, E extends Exception> T withCleanCallingIdentity( + @NonNull ThrowingSupplier<T, E> action) throws E { + final long callingIdentity = Binder.clearCallingIdentity(); + try { + return action.get(); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + /** + * An equivalent of {@link Supplier} + * + * @param <T> The class which is declared to be returned. + * @param <E> The exception class which is declared to be thrown. + */ + @FunctionalInterface + public interface ThrowingSupplier<T, E extends Exception> { + /** @see java.util.function.Supplier */ + T get() throws E; + } +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl new file mode 100644 index 000000000000..45c43502d6de --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import android.os.PersistableBundle; +import android.os.ParcelFileDescriptor; +import android.os.ICancellationSignal; +import android.os.RemoteCallback; +import android.app.ondeviceintelligence.IDownloadCallback; +import android.app.ondeviceintelligence.Feature; +import android.app.ondeviceintelligence.IFeatureCallback; +import android.app.ondeviceintelligence.IListFeaturesCallback; +import android.app.ondeviceintelligence.IFeatureDetailsCallback; +import com.android.internal.infra.AndroidFuture; +import android.service.ondeviceintelligence.IRemoteProcessingService; + + +/** + * Interface for a concrete implementation to provide on device intelligence services. + * + * @hide + */ +oneway interface IOnDeviceIntelligenceService { + void getVersion(in RemoteCallback remoteCallback); + void getFeature(int callerUid, int featureId, in IFeatureCallback featureCallback); + void listFeatures(int callerUid, in IListFeaturesCallback listFeaturesCallback); + void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback); + void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future); + void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback); + void requestFeatureDownload(int callerUid, in Feature feature, + in AndroidFuture cancellationSignal, + in IDownloadCallback downloadCallback); + void registerRemoteServices(in IRemoteProcessingService remoteProcessingService); + void notifyInferenceServiceConnected(); + void notifyInferenceServiceDisconnected(); + void ready(); +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl new file mode 100644 index 000000000000..1af3b0f374f1 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import android.app.ondeviceintelligence.IStreamingResponseCallback; +import android.app.ondeviceintelligence.IResponseCallback; +import android.app.ondeviceintelligence.ITokenInfoCallback; +import android.app.ondeviceintelligence.IProcessingSignal; +import android.app.ondeviceintelligence.Feature; +import android.os.IRemoteCallback; +import android.os.ICancellationSignal; +import android.os.PersistableBundle; +import android.os.Bundle; +import com.android.internal.infra.AndroidFuture; +import android.service.ondeviceintelligence.IRemoteStorageService; +import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; + +/** + * Interface for a concrete implementation to provide on-device sandboxed inference. + * + * @hide + */ +oneway interface IOnDeviceSandboxedInferenceService { + void registerRemoteStorageService(in IRemoteStorageService storageService, + in IRemoteCallback remoteCallback) = 0; + void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, + in AndroidFuture cancellationSignal, + in ITokenInfoCallback tokenInfoCallback) = 1; + void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType, + in AndroidFuture cancellationSignal, + in AndroidFuture processingSignal, + in IResponseCallback callback) = 2; + void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType, + in AndroidFuture cancellationSignal, + in AndroidFuture processingSignal, + in IStreamingResponseCallback callback) = 3; + void updateProcessingState(in Bundle processingState, + in IProcessingUpdateStatusCallback callback) = 4; +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl new file mode 100644 index 000000000000..7ead8690abb4 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl @@ -0,0 +1,14 @@ +package android.service.ondeviceintelligence; + +import android.os.PersistableBundle; + +/** + * Interface for receiving status from a updateProcessingState call from on-device intelligence + * service. + * + * @hide + */ +interface IProcessingUpdateStatusCallback { + void onSuccess(in PersistableBundle statusParams) = 1; + void onFailure(int errorCode, in String errorMessage) = 2; +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl new file mode 100644 index 000000000000..32a8a6a70406 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import android.os.Bundle; +import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; + + +/** + * Interface for a concrete implementation to provide methods to update state of a remote service. + * + * @hide + */ +oneway interface IRemoteProcessingService { + void updateProcessingState(in Bundle processingState, + in IProcessingUpdateStatusCallback callback); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl new file mode 100644 index 000000000000..a6f49e17d80e --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import android.app.ondeviceintelligence.Feature; +import android.os.ParcelFileDescriptor; +import android.os.RemoteCallback; + +import com.android.internal.infra.AndroidFuture; + +/** + * Interface for a concrete implementation to provide access to storage read access + * for the isolated process. + * + * @hide + */ +oneway interface IRemoteStorageService { + void getReadOnlyFileDescriptor(in String filePath, in AndroidFuture<ParcelFileDescriptor> future); + void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback); +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java new file mode 100644 index 000000000000..618d2a096916 --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.ondeviceintelligence.DownloadCallback; +import android.app.ondeviceintelligence.Feature; +import android.app.ondeviceintelligence.FeatureDetails; +import android.app.ondeviceintelligence.IDownloadCallback; +import android.app.ondeviceintelligence.IFeatureCallback; +import android.app.ondeviceintelligence.IFeatureDetailsCallback; +import android.app.ondeviceintelligence.IListFeaturesCallback; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.Looper; +import android.os.OutcomeReceiver; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.infra.AndroidFuture; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.LongConsumer; + +/** + * Abstract base class for performing setup for on-device inference and providing file access to + * the isolated counter part {@link OnDeviceSandboxedInferenceService}. + * + * <p> A service that provides configuration and model files relevant to performing inference on + * device. The system's default OnDeviceIntelligenceService implementation is configured in + * {@code config_defaultOnDeviceIntelligenceService}. If this config has no value, a stub is + * returned. + * + * <p> Similar to {@link OnDeviceIntelligenceManager} class, the contracts in this service are + * defined to be open-ended in general, to allow interoperability. Therefore, it is recommended + * that implementations of this system-service expose this API to the clients via a library which + * has more defined contract.</p> + * <pre> + * {@literal + * <service android:name=".SampleOnDeviceIntelligenceService" + * android:permission="android.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE"> + * </service>} + * </pre> + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public abstract class OnDeviceIntelligenceService extends Service { + private static final String TAG = OnDeviceIntelligenceService.class.getSimpleName(); + + private volatile IRemoteProcessingService mRemoteProcessingService; + private Handler mHandler; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null /* callback */, true /* async */); + } + + /** + * The {@link Intent} that must be declared as handled by the service. To be supported, the + * service must also require the + * {@link android.Manifest.permission#BIND_ON_DEVICE_INTELLIGENCE_SERVICE} + * permission so that other applications can not abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.ondeviceintelligence.OnDeviceIntelligenceService"; + + + /** + * @hide + */ + @Nullable + @Override + public final IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return new IOnDeviceIntelligenceService.Stub() { + /** {@inheritDoc} */ + @Override + public void ready() { + mHandler.executeOrSendMessage( + obtainMessage(OnDeviceIntelligenceService::onReady, + OnDeviceIntelligenceService.this)); + } + + @Override + public void getVersion(RemoteCallback remoteCallback) { + Objects.requireNonNull(remoteCallback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onGetVersion, + OnDeviceIntelligenceService.this, l -> { + Bundle b = new Bundle(); + b.putLong( + OnDeviceIntelligenceManager.API_VERSION_BUNDLE_KEY, + l); + remoteCallback.sendResult(b); + })); + } + + @Override + public void listFeatures(int callerUid, + IListFeaturesCallback listFeaturesCallback) { + Objects.requireNonNull(listFeaturesCallback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onListFeatures, + OnDeviceIntelligenceService.this, callerUid, + wrapListFeaturesCallback(listFeaturesCallback))); + } + + @Override + public void getFeature(int callerUid, int id, IFeatureCallback featureCallback) { + Objects.requireNonNull(featureCallback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onGetFeature, + OnDeviceIntelligenceService.this, callerUid, + id, wrapFeatureCallback(featureCallback))); + } + + + @Override + public void getFeatureDetails(int callerUid, Feature feature, + IFeatureDetailsCallback featureDetailsCallback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(featureDetailsCallback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onGetFeatureDetails, + OnDeviceIntelligenceService.this, callerUid, + feature, wrapFeatureDetailsCallback(featureDetailsCallback))); + } + + @Override + public void requestFeatureDownload(int callerUid, Feature feature, + AndroidFuture cancellationSignalFuture, + IDownloadCallback downloadCallback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(downloadCallback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onDownloadFeature, + OnDeviceIntelligenceService.this, callerUid, + feature, + CancellationSignal.fromTransport(transport), + wrapDownloadCallback(downloadCallback))); + } + + @Override + public void getReadOnlyFileDescriptor(String fileName, + AndroidFuture<ParcelFileDescriptor> future) { + Objects.requireNonNull(fileName); + Objects.requireNonNull(future); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onGetReadOnlyFileDescriptor, + OnDeviceIntelligenceService.this, fileName, + future)); + } + + @Override + public void getReadOnlyFeatureFileDescriptorMap( + Feature feature, RemoteCallback remoteCallback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(remoteCallback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onGetReadOnlyFeatureFileDescriptorMap, + OnDeviceIntelligenceService.this, feature, + parcelFileDescriptorMap -> { + Bundle bundle = new Bundle(); + parcelFileDescriptorMap.forEach(bundle::putParcelable); + remoteCallback.sendResult(bundle); + tryClosePfds(parcelFileDescriptorMap.values()); + })); + } + + @Override + public void registerRemoteServices( + IRemoteProcessingService remoteProcessingService) { + mRemoteProcessingService = remoteProcessingService; + } + + @Override + public void notifyInferenceServiceConnected() { + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onInferenceServiceConnected, + OnDeviceIntelligenceService.this)); + } + + @Override + public void notifyInferenceServiceDisconnected() { + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceIntelligenceService::onInferenceServiceDisconnected, + OnDeviceIntelligenceService.this)); + } + }; + } + Slog.w(TAG, "Incorrect service interface, returning null."); + return null; + } + + /** + * Using this signal to assertively a signal each time service binds successfully, used only in + * tests to get a signal that service instance is ready. This is needed because we cannot rely + * on {@link #onCreate} or {@link #onBind} to be invoke on each binding. + */ + public void onReady() { + } + + + /** + * Invoked when a new instance of the remote inference service is created. + * This method should be used as a signal to perform any initialization operations, for e.g. by + * invoking the {@link #updateProcessingState} method to initialize the remote processing + * service. + */ + public abstract void onInferenceServiceConnected(); + + + /** + * Invoked when an instance of the remote inference service is disconnected. + */ + public abstract void onInferenceServiceDisconnected(); + + + /** + * Invoked by the {@link OnDeviceIntelligenceService} inorder to send updates to the inference + * service if there is a state change to be performed. State change could be config updates, + * performing initialization or cleanup tasks in the remote inference service. + * The Bundle passed in here is expected to be read-only and will be rejected if it has any + * writable fields as detailed under {@link StateParams}. + * + * @param processingState the updated state to be applied. + * @param callbackExecutor executor to the run status callback on. + * @param statusReceiver receiver to get status of the update state operation. + */ + public final void updateProcessingState(@NonNull @StateParams Bundle processingState, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> statusReceiver) { + Objects.requireNonNull(callbackExecutor); + if (mRemoteProcessingService == null) { + throw new IllegalStateException("Remote processing service is unavailable."); + } + try { + mRemoteProcessingService.updateProcessingState(processingState, + new IProcessingUpdateStatusCallback.Stub() { + @Override + public void onSuccess(PersistableBundle result) { + Binder.withCleanCallingIdentity(() -> { + callbackExecutor.execute( + () -> statusReceiver.onResult(result)); + }); + } + + @Override + public void onFailure(int errorCode, String errorMessage) { + Binder.withCleanCallingIdentity(() -> callbackExecutor.execute( + () -> statusReceiver.onError( + new OnDeviceIntelligenceException( + errorCode, errorMessage)))); + } + }); + } catch (RemoteException e) { + Slog.e(TAG, "Error in updateProcessingState: " + e); + throw new RuntimeException(e); + } + } + + private OutcomeReceiver<Feature, + OnDeviceIntelligenceException> wrapFeatureCallback( + IFeatureCallback featureCallback) { + return new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull Feature feature) { + try { + featureCallback.onSuccess(feature); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending feature: " + e); + } + } + + @Override + public void onError( + @NonNull OnDeviceIntelligenceException exception) { + try { + featureCallback.onFailure(exception.getErrorCode(), exception.getMessage(), + exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download feature: " + e); + } + } + }; + } + + private OutcomeReceiver<List<Feature>, + OnDeviceIntelligenceException> wrapListFeaturesCallback( + IListFeaturesCallback listFeaturesCallback) { + return new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull List<Feature> features) { + try { + listFeaturesCallback.onSuccess(features); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending feature: " + e); + } + } + + @Override + public void onError( + @NonNull OnDeviceIntelligenceException exception) { + try { + listFeaturesCallback.onFailure(exception.getErrorCode(), exception.getMessage(), + exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download feature: " + e); + } + } + }; + } + + private OutcomeReceiver<FeatureDetails, + OnDeviceIntelligenceException> wrapFeatureDetailsCallback( + IFeatureDetailsCallback featureStatusCallback) { + return new OutcomeReceiver<>() { + @Override + public void onResult(FeatureDetails result) { + try { + featureStatusCallback.onSuccess(result); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending feature status: " + e); + } + } + + @Override + public void onError( + @NonNull OnDeviceIntelligenceException exception) { + try { + featureStatusCallback.onFailure(exception.getErrorCode(), + exception.getMessage(), exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending feature status: " + e); + } + } + }; + } + + + private DownloadCallback wrapDownloadCallback(IDownloadCallback downloadCallback) { + return new DownloadCallback() { + @Override + public void onDownloadStarted(long bytesToDownload) { + try { + downloadCallback.onDownloadStarted(bytesToDownload); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download status: " + e); + } + } + + @Override + public void onDownloadFailed(int failureStatus, + String errorMessage, @NonNull PersistableBundle errorParams) { + try { + downloadCallback.onDownloadFailed(failureStatus, errorMessage, errorParams); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download status: " + e); + } + } + + @Override + public void onDownloadProgress(long totalBytesDownloaded) { + try { + downloadCallback.onDownloadProgress(totalBytesDownloaded); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download status: " + e); + } + } + + @Override + public void onDownloadCompleted(@NonNull PersistableBundle persistableBundle) { + try { + downloadCallback.onDownloadCompleted(persistableBundle); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending download status: " + e); + } + } + }; + } + + private static void tryClosePfds(Collection<ParcelFileDescriptor> pfds) { + pfds.forEach(pfd -> { + try { + pfd.close(); + } catch (Exception e) { + Log.w(TAG, "Error closing FD", e); + } + }); + } + + private void onGetReadOnlyFileDescriptor(@NonNull String fileName, + @NonNull AndroidFuture<ParcelFileDescriptor> future) { + Slog.v(TAG, "onGetReadOnlyFileDescriptor " + fileName); + Binder.withCleanCallingIdentity(() -> { + Slog.v(TAG, + "onGetReadOnlyFileDescriptor: " + fileName + " under internal app storage."); + File f = new File(getBaseContext().getFilesDir(), fileName); + if (!f.exists()) { + f = new File(fileName); + } + ParcelFileDescriptor pfd = null; + try { + pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); + Slog.d(TAG, "Successfully opened a file with ParcelFileDescriptor."); + } catch (FileNotFoundException e) { + Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned."); + future.completeExceptionally(e); + } finally { + future.complete(pfd); + if (pfd != null) { + pfd.close(); + } + } + }); + } + + /** + * Provide implementation for a scenario when caller wants to get all feature related + * file-descriptors that might be required for processing a request for the corresponding the + * feature. + * + * @param feature the feature for which files need to be opened. + * @param fileDescriptorMapConsumer callback to be populated with a map of file-path and + * corresponding ParcelDescriptor to be used in a remote + * service. + */ + public abstract void onGetReadOnlyFeatureFileDescriptorMap( + @NonNull Feature feature, + @NonNull Consumer<Map<String, ParcelFileDescriptor>> fileDescriptorMapConsumer); + + /** + * Request download for feature that is requested and listen to download progress updates. If + * the download completes successfully, success callback should be populated. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param feature the feature for which files need to be downlaoded. + * process. + * @param cancellationSignal signal to attach a listener to, and receive cancellation signals + * from thw client. + * @param downloadCallback callback to populate download updates for clients to listen on.. + */ + public abstract void onDownloadFeature( + int callerUid, @NonNull Feature feature, + @Nullable CancellationSignal cancellationSignal, + @NonNull DownloadCallback downloadCallback); + + /** + * Provide feature details for the passed in feature. Usually the client and remote + * implementation use the {@link Feature#getFeatureParams()} as a hint to communicate what + * details the client is looking for. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param feature the feature for which status needs to be known. + * @param featureDetailsCallback callback to populate the resulting feature status. + */ + public abstract void onGetFeatureDetails(int callerUid, @NonNull Feature feature, + @NonNull OutcomeReceiver<FeatureDetails, + OnDeviceIntelligenceException> featureDetailsCallback); + + + /** + * Get feature using the provided identifier to the remote implementation. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param featureCallback callback to populate the features list. + */ + public abstract void onGetFeature(int callerUid, int featureId, + @NonNull OutcomeReceiver<Feature, + OnDeviceIntelligenceException> featureCallback); + + /** + * List all features which are available in the remote implementation. The implementation might + * choose to provide only a certain list of features based on the caller. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param listFeaturesCallback callback to populate the features list. + */ + public abstract void onListFeatures(int callerUid, @NonNull OutcomeReceiver<List<Feature>, + OnDeviceIntelligenceException> listFeaturesCallback); + + /** + * Provides a long value representing the version of the remote implementation processing + * requests. + * + * @param versionConsumer consumer to populate the version. + */ + public abstract void onGetVersion(@NonNull LongConsumer versionConsumer); +} diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java new file mode 100644 index 000000000000..949fb8ddda9d --- /dev/null +++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -0,0 +1,617 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.ondeviceintelligence; + +import static android.app.ondeviceintelligence.OnDeviceIntelligenceManager.AUGMENT_REQUEST_CONTENT_BUNDLE_KEY; +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.ondeviceintelligence.Feature; +import android.app.ondeviceintelligence.IProcessingSignal; +import android.app.ondeviceintelligence.IResponseCallback; +import android.app.ondeviceintelligence.IStreamingResponseCallback; +import android.app.ondeviceintelligence.ITokenInfoCallback; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams; +import android.app.ondeviceintelligence.ProcessingCallback; +import android.app.ondeviceintelligence.ProcessingSignal; +import android.app.ondeviceintelligence.StreamingProcessingCallback; +import android.app.ondeviceintelligence.TokenInfo; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.IRemoteCallback; +import android.os.Looper; +import android.os.OutcomeReceiver; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.infra.AndroidFuture; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Abstract base class for performing inference in a isolated process. This service exposes its + * methods via {@link OnDeviceIntelligenceManager}. + * + * <p> A service that provides methods to perform on-device inference both in streaming and + * non-streaming fashion. Also, provides a way to register a storage service that will be used to + * read-only access files from the {@link OnDeviceIntelligenceService} counterpart. </p> + * + * <p> Similar to {@link OnDeviceIntelligenceManager} class, the contracts in this service are + * defined to be open-ended in general, to allow interoperability. Therefore, it is recommended + * that implementations of this system-service expose this API to the clients via a library which + * has more defined contract.</p> + * + * <pre> + * {@literal + * <service android:name=".SampleSandboxedInferenceService" + * android:permission="android.permission.BIND_ONDEVICE_SANDBOXED_INFERENCE_SERVICE" + * android:isolatedProcess="true"> + * </service>} + * </pre> + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE) +public abstract class OnDeviceSandboxedInferenceService extends Service { + private static final String TAG = OnDeviceSandboxedInferenceService.class.getSimpleName(); + + /** + * @hide + */ + public static final String INFERENCE_INFO_BUNDLE_KEY = "inference_info"; + + /** + * The {@link Intent} that must be declared as handled by the service. To be supported, the + * service must also require the + * {@link android.Manifest.permission#BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE} + * permission so that other applications can not abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService"; + + // TODO(339594686): make API + /** + * @hide + */ + public static final String REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY = + "register_model_update_callback"; + /** + * @hide + */ + public static final String MODEL_LOADED_BUNDLE_KEY = "model_loaded"; + /** + * @hide + */ + public static final String MODEL_UNLOADED_BUNDLE_KEY = "model_unloaded"; + /** + * @hide + */ + public static final String MODEL_LOADED_BROADCAST_INTENT = + "android.service.ondeviceintelligence.MODEL_LOADED"; + /** + * @hide + */ + public static final String MODEL_UNLOADED_BROADCAST_INTENT = + "android.service.ondeviceintelligence.MODEL_UNLOADED"; + + /** + * @hide + */ + public static final String DEVICE_CONFIG_UPDATE_BUNDLE_KEY = "device_config_update"; + + private IRemoteStorageService mRemoteStorageService; + private Handler mHandler; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null /* callback */, true /* async */); + } + + /** + * @hide + */ + @Nullable + @Override + public final IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return new IOnDeviceSandboxedInferenceService.Stub() { + @Override + public void registerRemoteStorageService(IRemoteStorageService storageService, + IRemoteCallback remoteCallback) throws RemoteException { + Objects.requireNonNull(storageService); + mRemoteStorageService = storageService; + remoteCallback.sendResult( + Bundle.EMPTY); //to notify caller uid to system-server. + } + + @Override + public void requestTokenInfo(int callerUid, Feature feature, Bundle request, + AndroidFuture cancellationSignalFuture, + ITokenInfoCallback tokenInfoCallback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(tokenInfoCallback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceSandboxedInferenceService::onTokenInfoRequest, + OnDeviceSandboxedInferenceService.this, + callerUid, feature, + request, + CancellationSignal.fromTransport(transport), + wrapTokenInfoCallback(tokenInfoCallback))); + } + + @Override + public void processRequestStreaming(int callerUid, Feature feature, Bundle request, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, + IStreamingResponseCallback callback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(callback); + + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } + + + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceSandboxedInferenceService::onProcessRequestStreaming, + OnDeviceSandboxedInferenceService.this, callerUid, + feature, + request, + requestType, + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), + wrapStreamingResponseCallback(callback))); + } + + @Override + public void processRequest(int callerUid, Feature feature, Bundle request, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, + IResponseCallback callback) { + Objects.requireNonNull(feature); + Objects.requireNonNull(callback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceSandboxedInferenceService::onProcessRequest, + OnDeviceSandboxedInferenceService.this, callerUid, feature, + request, requestType, + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), + wrapResponseCallback(callback))); + } + + @Override + public void updateProcessingState(Bundle processingState, + IProcessingUpdateStatusCallback callback) { + Objects.requireNonNull(processingState); + Objects.requireNonNull(callback); + mHandler.executeOrSendMessage( + obtainMessage( + OnDeviceSandboxedInferenceService::onUpdateProcessingState, + OnDeviceSandboxedInferenceService.this, processingState, + wrapOutcomeReceiver(callback))); + } + }; + } + Slog.w(TAG, "Incorrect service interface, returning null."); + return null; + } + + /** + * Invoked when caller wants to obtain token info related to the payload in the passed + * content, associated with the provided feature. + * The expectation from the implementation is that when processing is complete, it + * should provide the token info in the {@link OutcomeReceiver#onResult}. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param feature feature which is associated with the request. + * @param request request that requires processing. + * @param cancellationSignal Cancellation Signal to receive cancellation events from client and + * configure a listener to. + * @param callback callback to populate failure or the token info for the provided + * request. + */ + @NonNull + public abstract void onTokenInfoRequest( + int callerUid, @NonNull Feature feature, + @NonNull @InferenceParams Bundle request, + @Nullable CancellationSignal cancellationSignal, + @NonNull OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> callback); + + /** + * Invoked when caller provides a request for a particular feature to be processed in a + * streaming manner. The expectation from the implementation is that when processing the + * request, + * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to + * continuously + * provide partial Bundle results for the caller to utilize. Optionally the implementation can + * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon + * processing completion. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param feature feature which is associated with the request. + * @param request request that requires processing. + * @param requestType identifier representing the type of request. + * @param cancellationSignal Cancellation Signal to receive cancellation events from client and + * configure a listener to. + * @param processingSignal Signal to receive custom action instructions from client. + * @param callback callback to populate the partial responses, failure and optionally + * full response for the provided request. + */ + @NonNull + public abstract void onProcessRequestStreaming( + int callerUid, @NonNull Feature feature, + @NonNull @InferenceParams Bundle request, + @OnDeviceIntelligenceManager.RequestType int requestType, + @Nullable CancellationSignal cancellationSignal, + @Nullable ProcessingSignal processingSignal, + @NonNull StreamingProcessingCallback callback); + + /** + * Invoked when caller provides a request for a particular feature to be processed in one shot + * completely. + * The expectation from the implementation is that when processing the request is complete, it + * should + * provide the complete response in the {@link OutcomeReceiver#onResult}. + * + * @param callerUid UID of the caller that initiated this call chain. + * @param feature feature which is associated with the request. + * @param request request that requires processing. + * @param requestType identifier representing the type of request. + * @param cancellationSignal Cancellation Signal to receive cancellation events from client and + * configure a listener to. + * @param processingSignal Signal to receive custom action instructions from client. + * @param callback callback to populate failure and full response for the provided + * request. + */ + @NonNull + public abstract void onProcessRequest( + int callerUid, @NonNull Feature feature, + @NonNull @InferenceParams Bundle request, + @OnDeviceIntelligenceManager.RequestType int requestType, + @Nullable CancellationSignal cancellationSignal, + @Nullable ProcessingSignal processingSignal, + @NonNull ProcessingCallback callback); + + + /** + * Invoked when processing environment needs to be updated or refreshed with fresh + * configuration, files or state. + * + * @param processingState contains updated state and params that are to be applied to the + * processing environmment, + * @param callback callback to populate the update status and if there are params + * associated with the status. + */ + public abstract void onUpdateProcessingState(@NonNull @StateParams Bundle processingState, + @NonNull OutcomeReceiver<PersistableBundle, + OnDeviceIntelligenceException> callback); + + + /** + * Overrides {@link Context#openFileInput} to read files with the given file names under the + * internal app storage of the {@link OnDeviceIntelligenceService}, i.e., only files stored in + * {@link Context#getFilesDir()} can be opened. + */ + @Override + public final FileInputStream openFileInput(@NonNull String filename) throws + FileNotFoundException { + try { + AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>(); + mRemoteStorageService.getReadOnlyFileDescriptor(filename, future); + ParcelFileDescriptor pfd = future.get(); + return new FileInputStream(pfd.getFileDescriptor()); + } catch (RemoteException | ExecutionException | InterruptedException e) { + Log.w(TAG, "Cannot open file due to remote service failure"); + throw new FileNotFoundException(e.getMessage()); + } + } + + /** + * Provides read-only access to the internal app storage via the + * {@link OnDeviceIntelligenceService}. This is an asynchronous alternative for + * {@link #openFileInput(String)}. + * + * @param fileName File name relative to the {@link Context#getFilesDir()}. + * @param resultConsumer Consumer to populate the corresponding file descriptor in. + */ + public final void getReadOnlyFileDescriptor(@NonNull String fileName, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ParcelFileDescriptor> resultConsumer) throws FileNotFoundException { + AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>(); + try { + mRemoteStorageService.getReadOnlyFileDescriptor(fileName, future); + } catch (RemoteException e) { + Log.w(TAG, "Cannot open file due to remote service failure"); + throw new FileNotFoundException(e.getMessage()); + } + future.whenCompleteAsync((pfd, err) -> { + if (err != null) { + Log.e(TAG, "Failure when reading file: " + fileName + err); + executor.execute(() -> resultConsumer.accept(null)); + } else { + executor.execute( + () -> resultConsumer.accept(pfd)); + } + }, executor); + } + + /** + * Provides access to all file streams required for feature via the + * {@link OnDeviceIntelligenceService}. + * + * @param feature Feature for which the associated files should be fetched. + * @param executor Executor to run the consumer callback on. + * @param resultConsumer Consumer to receive a map of filePath to the corresponding file input + * stream. + */ + public final void fetchFeatureFileDescriptorMap(@NonNull Feature feature, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<Map<String, ParcelFileDescriptor>> resultConsumer) { + try { + mRemoteStorageService.getReadOnlyFeatureFileDescriptorMap(feature, + wrapAsRemoteCallback(resultConsumer, executor)); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + + /** + * Returns the {@link Executor} to use for incoming IPC from request sender into your service + * implementation. For e.g. see + * {@link ProcessingCallback#onDataAugmentRequest(Bundle, + * Consumer)} where we use the executor to populate the consumer. + * <p> + * Override this method in your {@link OnDeviceSandboxedInferenceService} implementation to + * provide the executor you want to use for incoming IPC. + * + * @return the {@link Executor} to use for incoming IPC from {@link OnDeviceIntelligenceManager} + * to {@link OnDeviceSandboxedInferenceService}. + */ + @SuppressLint("OnNameExpected") + @NonNull + public Executor getCallbackExecutor() { + return new HandlerExecutor(Handler.createAsync(getMainLooper())); + } + + + private RemoteCallback wrapAsRemoteCallback( + @NonNull Consumer<Map<String, ParcelFileDescriptor>> resultConsumer, + @NonNull Executor executor) { + return new RemoteCallback(result -> { + if (result == null) { + executor.execute(() -> resultConsumer.accept(new HashMap<>())); + } else { + Map<String, ParcelFileDescriptor> pfdMap = new HashMap<>(); + result.keySet().forEach(key -> + pfdMap.put(key, result.getParcelable(key, + ParcelFileDescriptor.class))); + executor.execute(() -> resultConsumer.accept(pfdMap)); + } + }); + } + + private ProcessingCallback wrapResponseCallback( + IResponseCallback callback) { + return new ProcessingCallback() { + @Override + public void onResult(@NonNull Bundle result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onError( + OnDeviceIntelligenceException exception) { + try { + callback.onFailure(exception.getErrorCode(), exception.getMessage(), + exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onDataAugmentRequest(@NonNull Bundle content, + @NonNull Consumer<Bundle> contentCallback) { + try { + callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback)); + + } catch (RemoteException e) { + Slog.e(TAG, "Error sending augment request: " + e); + } + } + }; + } + + private StreamingProcessingCallback wrapStreamingResponseCallback( + IStreamingResponseCallback callback) { + return new StreamingProcessingCallback() { + @Override + public void onPartialResult(@NonNull Bundle partialResult) { + try { + callback.onNewContent(partialResult); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onResult(@NonNull Bundle result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onError( + OnDeviceIntelligenceException exception) { + try { + callback.onFailure(exception.getErrorCode(), exception.getMessage(), + exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onDataAugmentRequest(@NonNull Bundle content, + @NonNull Consumer<Bundle> contentCallback) { + try { + callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback)); + + } catch (RemoteException e) { + Slog.e(TAG, "Error sending augment request: " + e); + } + } + }; + } + + private RemoteCallback wrapRemoteCallback( + @NonNull Consumer<Bundle> contentCallback) { + return new RemoteCallback( + result -> { + if (result != null) { + getCallbackExecutor().execute(() -> contentCallback.accept( + result.getParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, + Bundle.class))); + } else { + getCallbackExecutor().execute( + () -> contentCallback.accept(null)); + } + }); + } + + private OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> wrapTokenInfoCallback( + ITokenInfoCallback tokenInfoCallback) { + return new OutcomeReceiver<>() { + @Override + public void onResult(TokenInfo tokenInfo) { + try { + tokenInfoCallback.onSuccess(tokenInfo); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + } + + @Override + public void onError( + OnDeviceIntelligenceException exception) { + try { + tokenInfoCallback.onFailure(exception.getErrorCode(), exception.getMessage(), + exception.getErrorParams()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending failure: " + e); + } + } + }; + } + + @NonNull + private static OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> wrapOutcomeReceiver( + IProcessingUpdateStatusCallback callback) { + return new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull PersistableBundle result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + + } + } + + @Override + public void onError( + @NonNull OnDeviceIntelligenceException error) { + try { + callback.onFailure(error.getErrorCode(), error.getMessage()); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending exception details: " + e); + } + } + }; + } + +} diff --git a/packages/NeuralNetworks/service/Android.bp b/packages/NeuralNetworks/service/Android.bp index 05c603f5ebce..cfdc1af6857f 100644 --- a/packages/NeuralNetworks/service/Android.bp +++ b/packages/NeuralNetworks/service/Android.bp @@ -19,11 +19,20 @@ package { filegroup { name: "service-ondeviceintelligence-sources", srcs: [ - "java/**/*.java", + "module/java/**/*.java", ], - path: "java", visibility: [ "//frameworks/base:__subpackages__", "//packages/modules/NeuralNetworks:__subpackages__", ], } + +filegroup { + name: "service-ondeviceintelligence-sources-platform", + srcs: [ + "platform/java/**/*.java", + ], + visibility: [ + "//frameworks/base:__subpackages__", + ], +} diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/BundleUtil.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/BundleUtil.java index 2626cc880e09..2626cc880e09 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/BundleUtil.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/BundleUtil.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java index e8a1b322f661..e8a1b322f661 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java index 6badc53ae97e..6badc53ae97e 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index a078f7542c11..a078f7542c11 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java index c641de8b47b1..c641de8b47b1 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java index 0c43a309c456..0c43a309c456 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java index 8c5d5a7ba736..8c5d5a7ba736 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java index 249bcd37db5d..249bcd37db5d 100644 --- a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java +++ b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java new file mode 100644 index 000000000000..7dd8f2fdcecb --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import static android.system.OsConstants.F_GETFL; +import static android.system.OsConstants.O_ACCMODE; +import static android.system.OsConstants.O_RDONLY; +import static android.system.OsConstants.PROT_READ; + +import android.app.ondeviceintelligence.IResponseCallback; +import android.app.ondeviceintelligence.IStreamingResponseCallback; +import android.app.ondeviceintelligence.ITokenInfoCallback; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.ResponseParams; +import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams; +import android.app.ondeviceintelligence.TokenInfo; +import android.database.CursorWindow; +import android.graphics.Bitmap; +import android.os.BadParcelableException; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.SharedMemory; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import com.android.internal.infra.AndroidFuture; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeoutException; + +/** + * Util methods for ensuring the Bundle passed in various methods are read-only and restricted to + * some known types. + */ +public class BundleUtil { + private static final String TAG = "BundleUtil"; + + /** + * Validation of the inference request payload as described in {@link InferenceParams} + * description. + * + * @throws BadParcelableException when the bundle does not meet the read-only requirements. + */ + public static void sanitizeInferenceParams( + @InferenceParams Bundle bundle) { + ensureValidBundle(bundle); + + if (!bundle.hasFileDescriptors()) { + return; //safe to exit if there are no FDs and Binders + } + + for (String key : bundle.keySet()) { + Object obj = bundle.get(key); + if (obj == null) { + /* Null value here could also mean deserializing a custom parcelable has failed, + * and since {@link Bundle} is marked as defusable in system-server - the + * {@link ClassNotFoundException} exception is swallowed and `null` is returned + * instead. We want to ensure cleanup of null entries in such case. + */ + bundle.putObject(key, null); + continue; + } + if (canMarshall(obj) || obj instanceof CursorWindow) { + continue; + } + if (obj instanceof Bundle) { + sanitizeInferenceParams((Bundle) obj); + } else if (obj instanceof ParcelFileDescriptor) { + validatePfdReadOnly((ParcelFileDescriptor) obj); + } else if (obj instanceof SharedMemory) { + ((SharedMemory) obj).setProtect(PROT_READ); + } else if (obj instanceof Bitmap) { + validateBitmap((Bitmap) obj); + } else if (obj instanceof Parcelable[]) { + validateParcelableArray((Parcelable[]) obj); + } else { + throw new BadParcelableException( + "Unsupported Parcelable type encountered in the Bundle: " + + obj.getClass().getSimpleName()); + } + } + } + + /** + * Validation of the inference request payload as described in {@link ResponseParams} + * description. + * + * @throws BadParcelableException when the bundle does not meet the read-only requirements. + */ + public static void sanitizeResponseParams( + @ResponseParams Bundle bundle) { + ensureValidBundle(bundle); + + if (!bundle.hasFileDescriptors()) { + return; //safe to exit if there are no FDs and Binders + } + + for (String key : bundle.keySet()) { + Object obj = bundle.get(key); + if (obj == null) { + /* Null value here could also mean deserializing a custom parcelable has failed, + * and since {@link Bundle} is marked as defusable in system-server - the + * {@link ClassNotFoundException} exception is swallowed and `null` is returned + * instead. We want to ensure cleanup of null entries in such case. + */ + bundle.putObject(key, null); + continue; + } + if (canMarshall(obj)) { + continue; + } + + if (obj instanceof Bundle) { + sanitizeResponseParams((Bundle) obj); + } else if (obj instanceof ParcelFileDescriptor) { + validatePfdReadOnly((ParcelFileDescriptor) obj); + } else if (obj instanceof Bitmap) { + validateBitmap((Bitmap) obj); + } else if (obj instanceof Parcelable[]) { + validateParcelableArray((Parcelable[]) obj); + } else { + throw new BadParcelableException( + "Unsupported Parcelable type encountered in the Bundle: " + + obj.getClass().getSimpleName()); + } + } + } + + /** + * Validation of the inference request payload as described in {@link StateParams} + * description. + * + * @throws BadParcelableException when the bundle does not meet the read-only requirements. + */ + public static void sanitizeStateParams( + @StateParams Bundle bundle) { + ensureValidBundle(bundle); + + if (!bundle.hasFileDescriptors()) { + return; //safe to exit if there are no FDs and Binders + } + + for (String key : bundle.keySet()) { + Object obj = bundle.get(key); + if (obj == null) { + /* Null value here could also mean deserializing a custom parcelable has failed, + * and since {@link Bundle} is marked as defusable in system-server - the + * {@link ClassNotFoundException} exception is swallowed and `null` is returned + * instead. We want to ensure cleanup of null entries in such case. + */ + bundle.putObject(key, null); + continue; + } + if (canMarshall(obj)) { + continue; + } + + if (obj instanceof ParcelFileDescriptor) { + validatePfdReadOnly((ParcelFileDescriptor) obj); + } else { + throw new BadParcelableException( + "Unsupported Parcelable type encountered in the Bundle: " + + obj.getClass().getSimpleName()); + } + } + } + + + public static IStreamingResponseCallback wrapWithValidation( + IStreamingResponseCallback streamingResponseCallback, + Executor resourceClosingExecutor, + AndroidFuture future, + InferenceInfoStore inferenceInfoStore) { + return new IStreamingResponseCallback.Stub() { + @Override + public void onNewContent(Bundle processedResult) throws RemoteException { + try { + sanitizeResponseParams(processedResult); + streamingResponseCallback.onNewContent(processedResult); + } finally { + resourceClosingExecutor.execute(() -> tryCloseResource(processedResult)); + } + } + + @Override + public void onSuccess(Bundle resultBundle) + throws RemoteException { + try { + sanitizeResponseParams(resultBundle); + streamingResponseCallback.onSuccess(resultBundle); + } finally { + inferenceInfoStore.addInferenceInfoFromBundle(resultBundle); + resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle)); + future.complete(null); + } + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) throws RemoteException { + streamingResponseCallback.onFailure(errorCode, errorMessage, errorParams); + inferenceInfoStore.addInferenceInfoFromBundle(errorParams); + future.completeExceptionally(new TimeoutException()); + } + + @Override + public void onDataAugmentRequest(Bundle processedContent, + RemoteCallback remoteCallback) + throws RemoteException { + try { + sanitizeResponseParams(processedContent); + streamingResponseCallback.onDataAugmentRequest(processedContent, + new RemoteCallback( + augmentedData -> { + try { + sanitizeInferenceParams(augmentedData); + remoteCallback.sendResult(augmentedData); + } finally { + resourceClosingExecutor.execute( + () -> tryCloseResource(augmentedData)); + } + })); + } finally { + resourceClosingExecutor.execute(() -> tryCloseResource(processedContent)); + } + } + }; + } + + public static IResponseCallback wrapWithValidation(IResponseCallback responseCallback, + Executor resourceClosingExecutor, + AndroidFuture future, + InferenceInfoStore inferenceInfoStore) { + return new IResponseCallback.Stub() { + @Override + public void onSuccess(Bundle resultBundle) + throws RemoteException { + try { + sanitizeResponseParams(resultBundle); + responseCallback.onSuccess(resultBundle); + } finally { + inferenceInfoStore.addInferenceInfoFromBundle(resultBundle); + resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle)); + future.complete(null); + } + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) throws RemoteException { + responseCallback.onFailure(errorCode, errorMessage, errorParams); + inferenceInfoStore.addInferenceInfoFromBundle(errorParams); + future.completeExceptionally(new TimeoutException()); + } + + @Override + public void onDataAugmentRequest(Bundle processedContent, + RemoteCallback remoteCallback) + throws RemoteException { + try { + sanitizeResponseParams(processedContent); + responseCallback.onDataAugmentRequest(processedContent, new RemoteCallback( + augmentedData -> { + try { + sanitizeInferenceParams(augmentedData); + remoteCallback.sendResult(augmentedData); + } finally { + resourceClosingExecutor.execute( + () -> tryCloseResource(augmentedData)); + } + })); + } finally { + resourceClosingExecutor.execute(() -> tryCloseResource(processedContent)); + } + } + }; + } + + + public static ITokenInfoCallback wrapWithValidation(ITokenInfoCallback responseCallback, + AndroidFuture future, + InferenceInfoStore inferenceInfoStore) { + return new ITokenInfoCallback.Stub() { + @Override + public void onSuccess(TokenInfo tokenInfo) throws RemoteException { + responseCallback.onSuccess(tokenInfo); + inferenceInfoStore.addInferenceInfoFromBundle(tokenInfo.getInfoParams()); + future.complete(null); + } + + @Override + public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams) + throws RemoteException { + responseCallback.onFailure(errorCode, errorMessage, errorParams); + inferenceInfoStore.addInferenceInfoFromBundle(errorParams); + future.completeExceptionally(new TimeoutException()); + } + }; + } + + private static boolean canMarshall(Object obj) { + return obj instanceof byte[] || obj instanceof PersistableBundle + || PersistableBundle.isValidType(obj); + } + + private static void ensureValidBundle(Bundle bundle) { + if (bundle == null) { + throw new IllegalArgumentException("Request passed is expected to be non-null"); + } + + if (bundle.hasBinders() != Bundle.STATUS_BINDERS_NOT_PRESENT) { + throw new BadParcelableException("Bundle should not contain IBinder objects."); + } + } + + private static void validateParcelableArray(Parcelable[] parcelables) { + if (parcelables.length > 0 + && parcelables[0] instanceof ParcelFileDescriptor) { + // Safe to cast + validatePfdsReadOnly(parcelables); + } else if (parcelables.length > 0 + && parcelables[0] instanceof Bitmap) { + validateBitmapsImmutable(parcelables); + } else { + throw new BadParcelableException( + "Could not cast to any known parcelable array"); + } + } + + public static void validatePfdsReadOnly(Parcelable[] pfds) { + for (Parcelable pfd : pfds) { + validatePfdReadOnly((ParcelFileDescriptor) pfd); + } + } + + public static void validatePfdReadOnly(ParcelFileDescriptor pfd) { + if (pfd == null) { + return; + } + try { + int readMode = Os.fcntlInt(pfd.getFileDescriptor(), F_GETFL, 0) & O_ACCMODE; + if (readMode != O_RDONLY) { + throw new BadParcelableException( + "Bundle contains a parcel file descriptor which is not read-only."); + } + } catch (ErrnoException e) { + throw new BadParcelableException( + "Invalid File descriptor passed in the Bundle.", e); + } + } + + private static void validateBitmap(Bitmap obj) { + if (obj.isMutable()) { + throw new BadParcelableException( + "Encountered a mutable Bitmap in the Bundle at key : " + obj); + } + } + + private static void validateBitmapsImmutable(Parcelable[] bitmaps) { + for (Parcelable bitmap : bitmaps) { + validateBitmap((Bitmap) bitmap); + } + } + + public static void tryCloseResource(Bundle bundle) { + if (bundle == null || bundle.isEmpty() || !bundle.hasFileDescriptors()) { + return; + } + + for (String key : bundle.keySet()) { + Object obj = bundle.get(key); + + try { + // TODO(b/329898589) : This can be cleaned up after the flag passing is fixed. + if (obj instanceof ParcelFileDescriptor) { + ((ParcelFileDescriptor) obj).close(); + } else if (obj instanceof CursorWindow) { + ((CursorWindow) obj).close(); + } else if (obj instanceof SharedMemory) { + // TODO(b/331796886) : Shared memory should honour parcelable flags. + ((SharedMemory) obj).close(); + } + } catch (Exception e) { + Log.e(TAG, "Error closing resource with key: " + key, e); + } + } + } +} diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java new file mode 100644 index 000000000000..bef3f8048da1 --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import android.app.ondeviceintelligence.InferenceInfo; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; +import android.util.Base64; +import android.util.Slog; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.TreeSet; + +public class InferenceInfoStore { + private static final String TAG = "InferenceInfoStore"; + private final TreeSet<InferenceInfo> inferenceInfos; + private final long maxAgeMs; + + public InferenceInfoStore(long maxAgeMs) { + this.maxAgeMs = maxAgeMs; + this.inferenceInfos = new TreeSet<>( + Comparator.comparingLong(InferenceInfo::getStartTimeMillis)); + } + + public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) { + return inferenceInfos.stream().filter( + info -> info.getStartTimeMillis() > startTimeEpochMillis).toList(); + } + + public void addInferenceInfoFromBundle(PersistableBundle pb) { + if (!pb.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) { + return; + } + + try { + String infoBytesBase64String = pb.getString( + OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY); + if (infoBytesBase64String != null) { + byte[] infoBytes = Base64.decode(infoBytesBase64String, Base64.DEFAULT); + com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo = + com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom( + infoBytes); + add(inferenceInfo); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes."); + } + } + + public void addInferenceInfoFromBundle(Bundle b) { + if (!b.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) { + return; + } + + try { + byte[] infoBytes = b.getByteArray( + OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY); + if (infoBytes != null) { + com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo = + com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom( + infoBytes); + add(inferenceInfo); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes."); + } + } + + private synchronized void add(com.android.server.ondeviceintelligence.nano.InferenceInfo info) { + while (!inferenceInfos.isEmpty() + && System.currentTimeMillis() - inferenceInfos.first().getStartTimeMillis() + > maxAgeMs) { + inferenceInfos.pollFirst(); + } + inferenceInfos.add(toInferenceInfo(info)); + } + + private static InferenceInfo toInferenceInfo( + com.android.server.ondeviceintelligence.nano.InferenceInfo info) { + return new InferenceInfo.Builder(info.uid).setStartTimeMillis( + info.startTimeMs).setEndTimeMillis(info.endTimeMs).setSuspendedTimeMillis( + info.suspendedTimeMs).build(); + } +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java new file mode 100644 index 000000000000..6badc53ae97e --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; + +import android.annotation.FlaggedApi; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; + +/** + * Exposes APIs to {@code system_server} components outside of the module boundaries. + * <p> This API should be access using {@link com.android.server.LocalManagerRegistry}. </p> + * + * @hide + */ +@SystemApi(client = Client.SYSTEM_SERVER) +@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) +public interface OnDeviceIntelligenceManagerLocal { + /** + * Gets the uid for the process that is currently hosting the + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} registered on + * the device. + */ + int getInferenceServiceUid(); +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java new file mode 100644 index 000000000000..0a69af67a8bf --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -0,0 +1,1096 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.DEVICE_CONFIG_UPDATE_BUNDLE_KEY; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BUNDLE_KEY; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BUNDLE_KEY; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BROADCAST_INTENT; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BROADCAST_INTENT; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY; + +import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeInferenceParams; +import static com.android.server.ondeviceintelligence.BundleUtil.validatePfdReadOnly; +import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeStateParams; +import static com.android.server.ondeviceintelligence.BundleUtil.wrapWithValidation; + + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.app.AppGlobals; +import android.app.ondeviceintelligence.DownloadCallback; +import android.app.ondeviceintelligence.Feature; +import android.app.ondeviceintelligence.FeatureDetails; +import android.app.ondeviceintelligence.IDownloadCallback; +import android.app.ondeviceintelligence.IFeatureCallback; +import android.app.ondeviceintelligence.IFeatureDetailsCallback; +import android.app.ondeviceintelligence.IListFeaturesCallback; +import android.app.ondeviceintelligence.IOnDeviceIntelligenceManager; +import android.app.ondeviceintelligence.IProcessingSignal; +import android.app.ondeviceintelligence.IResponseCallback; +import android.app.ondeviceintelligence.IStreamingResponseCallback; +import android.app.ondeviceintelligence.ITokenInfoCallback; +import android.app.ondeviceintelligence.InferenceInfo; +import android.app.ondeviceintelligence.OnDeviceIntelligenceException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.IRemoteCallback; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.service.ondeviceintelligence.IOnDeviceIntelligenceService; +import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService; +import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; +import android.service.ondeviceintelligence.IRemoteProcessingService; +import android.service.ondeviceintelligence.IRemoteStorageService; +import android.service.ondeviceintelligence.OnDeviceIntelligenceService; +import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.infra.ServiceConnector; +import com.android.internal.os.BackgroundThread; +import com.android.server.LocalManagerRegistry; +import com.android.server.SystemService; +import com.android.server.SystemService.TargetUser; +import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * This is the system service for handling calls on the + * {@link android.app.ondeviceintelligence.OnDeviceIntelligenceManager}. This + * service holds connection references to the underlying remote services i.e. the isolated service + * {@link OnDeviceSandboxedInferenceService} and a regular + * service counter part {@link OnDeviceIntelligenceService}. + * + * Note: Both the remote services run under the SYSTEM user, as we cannot have separate instance of + * the Inference service for each user, due to possible high memory footprint. + * + * @hide + */ +public class OnDeviceIntelligenceManagerService extends SystemService { + + private static final String TAG = OnDeviceIntelligenceManagerService.class.getSimpleName(); + private static final String KEY_SERVICE_ENABLED = "service_enabled"; + + /** Handler message to {@link #resetTemporaryServices()} */ + private static final int MSG_RESET_TEMPORARY_SERVICE = 0; + /** Handler message to clean up temporary broadcast keys. */ + private static final int MSG_RESET_BROADCAST_KEYS = 1; + /** Handler message to clean up temporary config namespace. */ + private static final int MSG_RESET_CONFIG_NAMESPACE = 2; + + /** Default value in absence of {@link DeviceConfig} override. */ + private static final boolean DEFAULT_SERVICE_ENABLED = true; + private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence"; + + private static final String SYSTEM_PACKAGE = "android"; + private static final long MAX_AGE_MS = TimeUnit.HOURS.toMillis(3); + + + private final Executor resourceClosingExecutor = Executors.newCachedThreadPool(); + private final Executor callbackExecutor = Executors.newCachedThreadPool(); + private final Executor broadcastExecutor = Executors.newCachedThreadPool(); + private final Executor mConfigExecutor = Executors.newCachedThreadPool(); + + + private final Context mContext; + protected final Object mLock = new Object(); + + private final InferenceInfoStore mInferenceInfoStore; + private RemoteOnDeviceSandboxedInferenceService mRemoteInferenceService; + private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService; + volatile boolean mIsServiceEnabled; + + @GuardedBy("mLock") + private int remoteInferenceServiceUid = -1; + + @GuardedBy("mLock") + private String[] mTemporaryServiceNames; + @GuardedBy("mLock") + private String[] mTemporaryBroadcastKeys; + @GuardedBy("mLock") + private String mBroadcastPackageName = SYSTEM_PACKAGE; + @GuardedBy("mLock") + private String mTemporaryConfigNamespace; + + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = + this::sendUpdatedConfig; + + + /** + * Handler used to reset the temporary service names. + */ + private Handler mTemporaryHandler; + private final @NonNull Handler mMainHandler = new Handler(Looper.getMainLooper()); + + + public OnDeviceIntelligenceManagerService(Context context) { + super(context); + mContext = context; + mTemporaryServiceNames = new String[0]; + mInferenceInfoStore = new InferenceInfoStore(MAX_AGE_MS); + } + + @Override + public void onStart() { + publishBinderService( + Context.ON_DEVICE_INTELLIGENCE_SERVICE, getOnDeviceIntelligenceManagerService(), + /* allowIsolated = */ true); + LocalManagerRegistry.addManager(OnDeviceIntelligenceManagerLocal.class, + this::getRemoteInferenceServiceUid); + } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { + DeviceConfig.addOnPropertiesChangedListener( + NAMESPACE_ON_DEVICE_INTELLIGENCE, + BackgroundThread.getExecutor(), + (properties) -> onDeviceConfigChange(properties.getKeyset())); + + mIsServiceEnabled = isServiceEnabled(); + } + } + + private void onDeviceConfigChange(@NonNull Set<String> keys) { + if (keys.contains(KEY_SERVICE_ENABLED)) { + mIsServiceEnabled = isServiceEnabled(); + } + } + + private boolean isServiceEnabled() { + return DeviceConfig.getBoolean( + NAMESPACE_ON_DEVICE_INTELLIGENCE, + KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED); + } + + private IBinder getOnDeviceIntelligenceManagerService() { + return new IOnDeviceIntelligenceManager.Stub() { + @Override + public String getRemoteServicePackageName() { + return OnDeviceIntelligenceManagerService.this.getRemoteConfiguredPackageName(); + } + + @Override + public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) { + mContext.enforceCallingPermission( + Manifest.permission.DUMP, TAG); + return OnDeviceIntelligenceManagerService.this.getLatestInferenceInfo( + startTimeEpochMillis); + } + + @Override + public void getVersion(RemoteCallback remoteCallback) { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); + Objects.requireNonNull(remoteCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + remoteCallback.sendResult(null); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getVersion(new RemoteCallback( + result -> { + remoteCallback.sendResult(result); + future.complete(null); + })); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + } + + @Override + public void getFeature(int id, IFeatureCallback featureCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); + Objects.requireNonNull(featureCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + featureCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getFeature(callerUid, id, new IFeatureCallback.Stub() { + @Override + public void onSuccess(Feature result) throws RemoteException { + featureCallback.onSuccess(result); + future.complete(null); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) throws RemoteException { + featureCallback.onFailure(errorCode, errorMessage, errorParams); + future.completeExceptionally(new TimeoutException()); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + } + + @Override + public void listFeatures(IListFeaturesCallback listFeaturesCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures"); + Objects.requireNonNull(listFeaturesCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + listFeaturesCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.listFeatures(callerUid, + new IListFeaturesCallback.Stub() { + @Override + public void onSuccess(List<Feature> result) + throws RemoteException { + listFeaturesCallback.onSuccess(result); + future.complete(null); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) + throws RemoteException { + listFeaturesCallback.onFailure(errorCode, errorMessage, + errorParams); + future.completeExceptionally(new TimeoutException()); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + } + + @Override + public void getFeatureDetails(Feature feature, + IFeatureDetailsCallback featureDetailsCallback) + throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatureStatus"); + Objects.requireNonNull(feature); + Objects.requireNonNull(featureDetailsCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + featureDetailsCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + return; + } + ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getFeatureDetails(callerUid, feature, + new IFeatureDetailsCallback.Stub() { + @Override + public void onSuccess(FeatureDetails result) + throws RemoteException { + future.complete(null); + featureDetailsCallback.onSuccess(result); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) + throws RemoteException { + future.completeExceptionally(null); + featureDetailsCallback.onFailure(errorCode, + errorMessage, errorParams); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + } + + @Override + public void requestFeatureDownload(Feature feature, + AndroidFuture cancellationSignalFuture, + IDownloadCallback downloadCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); + Objects.requireNonNull(feature); + Objects.requireNonNull(downloadCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + downloadCallback.onDownloadFailed( + DownloadCallback.DOWNLOAD_FAILURE_STATUS_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + ListenableDownloadCallback listenableDownloadCallback = + new ListenableDownloadCallback( + downloadCallback, + mMainHandler, future, getIdleTimeoutMs()); + service.requestFeatureDownload(callerUid, feature, + wrapCancellationFuture(cancellationSignalFuture), + listenableDownloadCallback); + return future; // this future has no timeout because, actual download + // might take long, but we fail early if there is no progress callbacks. + } + ); + } + + + @Override + public void requestTokenInfo(Feature feature, + Bundle request, + AndroidFuture cancellationSignalFuture, + ITokenInfoCallback tokenInfoCallback) throws RemoteException { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo"); + AndroidFuture<Void> result = null; + try { + Objects.requireNonNull(feature); + sanitizeInferenceParams(request); + Objects.requireNonNull(tokenInfoCallback); + + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + tokenInfoCallback.onFailure( + OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.requestTokenInfo(callerUid, feature, + request, + wrapCancellationFuture(cancellationSignalFuture), + wrapWithValidation(tokenInfoCallback, future, + mInferenceInfoStore)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), + resourceClosingExecutor); + } finally { + if (result == null) { + resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request)); + } + } + } + + @Override + public void processRequest(Feature feature, + Bundle request, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, + IResponseCallback responseCallback) + throws RemoteException { + AndroidFuture<Void> result = null; + try { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); + Objects.requireNonNull(feature); + sanitizeInferenceParams(request); + Objects.requireNonNull(responseCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + responseCallback.onFailure( + OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.processRequest(callerUid, feature, + request, + requestType, + wrapCancellationFuture(cancellationSignalFuture), + wrapProcessingFuture(processingSignalFuture), + wrapWithValidation(responseCallback, + resourceClosingExecutor, future, + mInferenceInfoStore)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), + resourceClosingExecutor); + } finally { + if (result == null) { + resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request)); + } + } + } + + @Override + public void processRequestStreaming(Feature feature, + Bundle request, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, + IStreamingResponseCallback streamingCallback) throws RemoteException { + AndroidFuture<Void> result = null; + try { + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); + Objects.requireNonNull(feature); + sanitizeInferenceParams(request); + Objects.requireNonNull(streamingCallback); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available"); + streamingCallback.onFailure( + OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE, + "OnDeviceIntelligenceManagerService is unavailable", + PersistableBundle.EMPTY); + } + ensureRemoteInferenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.processRequestStreaming(callerUid, + feature, + request, requestType, + wrapCancellationFuture(cancellationSignalFuture), + wrapProcessingFuture(processingSignalFuture), + wrapWithValidation(streamingCallback, + resourceClosingExecutor, future, + mInferenceInfoStore)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); + result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), + resourceClosingExecutor); + } finally { + if (result == null) { + resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request)); + } + } + } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + new OnDeviceIntelligenceShellCommand(OnDeviceIntelligenceManagerService.this).exec( + this, in, out, err, args, callback, resultReceiver); + } + }; + } + + private void ensureRemoteIntelligenceServiceInitialized() { + synchronized (mLock) { + if (mRemoteOnDeviceIntelligenceService == null) { + String serviceName = getServiceNames()[0]; + Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false)); + mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext, + ComponentName.unflattenFromString(serviceName), + UserHandle.SYSTEM.getIdentifier()); + mRemoteOnDeviceIntelligenceService.setServiceLifecycleCallbacks( + new ServiceConnector.ServiceLifecycleCallbacks<>() { + @Override + public void onConnected( + @NonNull IOnDeviceIntelligenceService service) { + try { + service.registerRemoteServices( + getRemoteProcessingService()); + service.ready(); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to send connected event", ex); + } + } + }); + } + } + } + + @NonNull + private IRemoteProcessingService.Stub getRemoteProcessingService() { + return new IRemoteProcessingService.Stub() { + @Override + public void updateProcessingState( + Bundle processingState, + IProcessingUpdateStatusCallback callback) { + callbackExecutor.execute(() -> { + AndroidFuture<Void> result = null; + try { + sanitizeStateParams(processingState); + ensureRemoteInferenceServiceInitialized(); + result = mRemoteInferenceService.post( + service -> service.updateProcessingState( + processingState, callback)); + result.whenCompleteAsync( + (c, e) -> BundleUtil.tryCloseResource(processingState), + resourceClosingExecutor); + } finally { + if (result == null) { + resourceClosingExecutor.execute( + () -> BundleUtil.tryCloseResource(processingState)); + } + } + }); + } + }; + } + + private void ensureRemoteInferenceServiceInitialized() { + synchronized (mLock) { + if (mRemoteInferenceService == null) { + String serviceName = getServiceNames()[1]; + Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true)); + mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext, + ComponentName.unflattenFromString(serviceName), + UserHandle.SYSTEM.getIdentifier()); + mRemoteInferenceService.setServiceLifecycleCallbacks( + new ServiceConnector.ServiceLifecycleCallbacks<>() { + @Override + public void onConnected( + @NonNull IOnDeviceSandboxedInferenceService service) { + try { + ensureRemoteIntelligenceServiceInitialized(); + service.registerRemoteStorageService( + getIRemoteStorageService(), new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle bundle) { + final int uid = Binder.getCallingUid(); + setRemoteInferenceServiceUid(uid); + } + }); + mRemoteOnDeviceIntelligenceService.run( + IOnDeviceIntelligenceService::notifyInferenceServiceConnected); + broadcastExecutor.execute( + () -> registerModelLoadingBroadcasts(service)); + mConfigExecutor.execute( + () -> registerDeviceConfigChangeListener()); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to send connected event", ex); + } + } + + @Override + public void onDisconnected( + @NonNull IOnDeviceSandboxedInferenceService service) { + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + IOnDeviceIntelligenceService::notifyInferenceServiceDisconnected); + } + + @Override + public void onBinderDied() { + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + IOnDeviceIntelligenceService::notifyInferenceServiceDisconnected); + } + }); + } + } + } + + private void registerModelLoadingBroadcasts(IOnDeviceSandboxedInferenceService service) { + String[] modelBroadcastKeys; + try { + modelBroadcastKeys = getBroadcastKeys(); + } catch (Resources.NotFoundException e) { + Slog.d(TAG, "Skipping model broadcasts as broadcast intents configured."); + return; + } + + Bundle bundle = new Bundle(); + bundle.putBoolean(REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY, true); + try { + service.updateProcessingState(bundle, new IProcessingUpdateStatusCallback.Stub() { + @Override + public void onSuccess(PersistableBundle statusParams) { + Binder.clearCallingIdentity(); + synchronized (mLock) { + if (statusParams.containsKey(MODEL_LOADED_BUNDLE_KEY)) { + String modelLoadedBroadcastKey = modelBroadcastKeys[0]; + if (modelLoadedBroadcastKey != null + && !modelLoadedBroadcastKey.isEmpty()) { + final Intent intent = new Intent(modelLoadedBroadcastKey); + intent.setPackage(mBroadcastPackageName); + mContext.sendBroadcast(intent, + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE); + } + } else if (statusParams.containsKey(MODEL_UNLOADED_BUNDLE_KEY)) { + String modelUnloadedBroadcastKey = modelBroadcastKeys[1]; + if (modelUnloadedBroadcastKey != null + && !modelUnloadedBroadcastKey.isEmpty()) { + final Intent intent = new Intent(modelUnloadedBroadcastKey); + intent.setPackage(mBroadcastPackageName); + mContext.sendBroadcast(intent, + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE); + } + } + } + } + + @Override + public void onFailure(int errorCode, String errorMessage) { + Slog.e(TAG, "Failed to register model loading callback with status code", + new OnDeviceIntelligenceException(errorCode, errorMessage)); + } + }); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register model loading callback with status code", e); + } + } + + private void registerDeviceConfigChangeListener() { + Log.d(TAG, "registerDeviceConfigChangeListener"); + String configNamespace = getConfigNamespace(); + if (configNamespace.isEmpty()) { + Slog.e(TAG, "config_defaultOnDeviceIntelligenceDeviceConfigNamespace is empty"); + return; + } + DeviceConfig.addOnPropertiesChangedListener( + configNamespace, + mConfigExecutor, + mOnPropertiesChangedListener); + } + + private String getConfigNamespace() { + synchronized (mLock) { + if (mTemporaryConfigNamespace != null) { + return mTemporaryConfigNamespace; + } + + return mContext.getResources().getString( + R.string.config_defaultOnDeviceIntelligenceDeviceConfigNamespace); + } + } + + private void sendUpdatedConfig( + DeviceConfig.Properties props) { + Log.d(TAG, "sendUpdatedConfig"); + + PersistableBundle persistableBundle = new PersistableBundle(); + for (String key : props.getKeyset()) { + persistableBundle.putString(key, props.getString(key, "")); + } + Bundle bundle = new Bundle(); + bundle.putParcelable(DEVICE_CONFIG_UPDATE_BUNDLE_KEY, persistableBundle); + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run(service -> service.updateProcessingState(bundle, + new IProcessingUpdateStatusCallback.Stub() { + @Override + public void onSuccess(PersistableBundle result) { + Slog.d(TAG, "Config update successful." + result); + } + + @Override + public void onFailure(int errorCode, String errorMessage) { + Slog.e(TAG, "Config update failed with code [" + + String.valueOf(errorCode) + "] and message = " + errorMessage); + } + })); + } + + @NonNull + private IRemoteStorageService.Stub getIRemoteStorageService() { + return new IRemoteStorageService.Stub() { + @Override + public void getReadOnlyFileDescriptor( + String filePath, + AndroidFuture<ParcelFileDescriptor> future) { + ensureRemoteIntelligenceServiceInitialized(); + AndroidFuture<ParcelFileDescriptor> pfdFuture = new AndroidFuture<>(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.getReadOnlyFileDescriptor( + filePath, pfdFuture)); + pfdFuture.whenCompleteAsync((pfd, error) -> { + try { + if (error != null) { + future.completeExceptionally(error); + } else { + validatePfdReadOnly(pfd); + future.complete(pfd); + } + } finally { + tryClosePfd(pfd); + } + }, callbackExecutor); + } + + @Override + public void getReadOnlyFeatureFileDescriptorMap( + Feature feature, + RemoteCallback remoteCallback) { + ensureRemoteIntelligenceServiceInitialized(); + mRemoteOnDeviceIntelligenceService.run( + service -> service.getReadOnlyFeatureFileDescriptorMap( + feature, + new RemoteCallback(result -> callbackExecutor.execute(() -> { + try { + if (result == null) { + remoteCallback.sendResult(null); + } + for (String key : result.keySet()) { + ParcelFileDescriptor pfd = result.getParcelable(key, + ParcelFileDescriptor.class); + validatePfdReadOnly(pfd); + } + remoteCallback.sendResult(result); + } finally { + resourceClosingExecutor.execute( + () -> BundleUtil.tryCloseResource(result)); + } + })))); + } + }; + } + + private void validateServiceElevated(String serviceName, boolean checkIsolated) { + try { + if (TextUtils.isEmpty(serviceName)) { + throw new IllegalStateException( + "Remote service is not configured to complete the request"); + } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceName); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + UserHandle.SYSTEM.getIdentifier()); + if (serviceInfo != null) { + if (!checkIsolated) { + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); + return; + } + + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); + if (!isIsolatedService(serviceInfo)) { + throw new SecurityException( + "Call required an isolated service, but the configured service: " + + serviceName + ", is not isolated"); + } + } else { + throw new IllegalStateException( + "Remote service is not configured to complete the request."); + } + } catch (RemoteException e) { + throw new IllegalStateException("Could not fetch service info for remote services", e); + } + } + + private static void checkServiceRequiresPermission(ServiceInfo serviceInfo, + String requiredPermission) { + final String permission = serviceInfo.permission; + if (!requiredPermission.equals(permission)) { + throw new SecurityException(String.format( + "Service %s requires %s permission. Found %s permission", + serviceInfo.getComponentName(), + requiredPermission, + serviceInfo.permission)); + } + } + + private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) { + return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0 + && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; + } + + private List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) { + return mInferenceInfoStore.getLatestInferenceInfo(startTimeEpochMillis); + } + + @Nullable + public String getRemoteConfiguredPackageName() { + try { + String[] serviceNames = getServiceNames(); + ComponentName componentName = ComponentName.unflattenFromString(serviceNames[1]); + if (componentName != null) { + return componentName.getPackageName(); + } + } catch (Resources.NotFoundException e) { + Slog.e(TAG, "Could not find resource", e); + } + + return null; + } + + + protected String[] getServiceNames() throws Resources.NotFoundException { + // TODO 329240495 : Consider a small class with explicit field names for the two services + synchronized (mLock) { + if (mTemporaryServiceNames != null && mTemporaryServiceNames.length == 2) { + return mTemporaryServiceNames; + } + } + return new String[]{mContext.getResources().getString( + R.string.config_defaultOnDeviceIntelligenceService), + mContext.getResources().getString( + R.string.config_defaultOnDeviceSandboxedInferenceService)}; + } + + protected String[] getBroadcastKeys() throws Resources.NotFoundException { + // TODO 329240495 : Consider a small class with explicit field names for the two services + synchronized (mLock) { + if (mTemporaryBroadcastKeys != null && mTemporaryBroadcastKeys.length == 2) { + return mTemporaryBroadcastKeys; + } + } + + return new String[]{ MODEL_LOADED_BROADCAST_INTENT, MODEL_UNLOADED_BROADCAST_INTENT }; + } + + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void setTemporaryServices(@NonNull String[] componentNames, int durationMs) { + Objects.requireNonNull(componentNames); + enforceShellOnly(Binder.getCallingUid(), "setTemporaryServices"); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + synchronized (mLock) { + mTemporaryServiceNames = componentNames; + if (mRemoteInferenceService != null) { + mRemoteInferenceService.unbind(); + mRemoteInferenceService = null; + } + if (mRemoteOnDeviceIntelligenceService != null) { + mRemoteOnDeviceIntelligenceService.unbind(); + mRemoteOnDeviceIntelligenceService = null; + } + + if (durationMs != -1) { + getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, + durationMs); + } + } + } + + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void setModelBroadcastKeys(@NonNull String[] broadcastKeys, String receiverPackageName, + int durationMs) { + Objects.requireNonNull(broadcastKeys); + enforceShellOnly(Binder.getCallingUid(), "setModelBroadcastKeys"); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + synchronized (mLock) { + mTemporaryBroadcastKeys = broadcastKeys; + mBroadcastPackageName = receiverPackageName; + if (durationMs != -1) { + getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_BROADCAST_KEYS, durationMs); + } + } + } + + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void setTemporaryDeviceConfigNamespace(@NonNull String configNamespace, + int durationMs) { + Objects.requireNonNull(configNamespace); + enforceShellOnly(Binder.getCallingUid(), "setTemporaryDeviceConfigNamespace"); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + synchronized (mLock) { + mTemporaryConfigNamespace = configNamespace; + if (durationMs != -1) { + getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_CONFIG_NAMESPACE, + durationMs); + } + } + } + + /** + * Reset the temporary services set in CTS tests, this method is primarily used to only revert + * the changes caused by CTS tests. + */ + public void resetTemporaryServices() { + synchronized (mLock) { + if (mTemporaryHandler != null) { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + mTemporaryHandler = null; + } + + mRemoteInferenceService = null; + mRemoteOnDeviceIntelligenceService = null; + mTemporaryServiceNames = new String[0]; + } + } + + /** + * Throws if the caller is not of a shell (or root) UID. + * + * @param callingUid pass Binder.callingUid(). + */ + public static void enforceShellOnly(int callingUid, String message) { + if (callingUid == android.os.Process.SHELL_UID + || callingUid == android.os.Process.ROOT_UID) { + return; // okay + } + + throw new SecurityException(message + ": Only shell user can call it"); + } + + private AndroidFuture<IBinder> wrapCancellationFuture( + AndroidFuture future) { + if (future == null) { + return null; + } + AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>(); + cancellationFuture.whenCompleteAsync((c, e) -> { + if (e != null) { + Log.e(TAG, "Error forwarding ICancellationSignal to manager layer", e); + future.completeExceptionally(e); + } else { + future.complete(new ICancellationSignal.Stub() { + @Override + public void cancel() throws RemoteException { + ICancellationSignal.Stub.asInterface(c).cancel(); + } + }); + } + }); + return cancellationFuture; + } + + private AndroidFuture<IBinder> wrapProcessingFuture( + AndroidFuture future) { + if (future == null) { + return null; + } + AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>(); + processingSignalFuture.whenCompleteAsync((c, e) -> { + if (e != null) { + future.completeExceptionally(e); + } else { + future.complete(new IProcessingSignal.Stub() { + @Override + public void sendSignal(PersistableBundle actionParams) throws RemoteException { + IProcessingSignal.Stub.asInterface(c).sendSignal(actionParams); + } + }); + } + }); + return processingSignalFuture; + } + + private static void tryClosePfd(ParcelFileDescriptor pfd) { + if (pfd != null) { + try { + pfd.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close parcel file descriptor ", e); + } + } + } + + private synchronized Handler getTemporaryHandler() { + if (mTemporaryHandler == null) { + mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { + resetTemporaryServices(); + } else if (msg.what == MSG_RESET_BROADCAST_KEYS) { + mTemporaryBroadcastKeys = null; + mBroadcastPackageName = SYSTEM_PACKAGE; + } else if (msg.what == MSG_RESET_CONFIG_NAMESPACE) { + mTemporaryConfigNamespace = null; + } else { + Slog.wtf(TAG, "invalid handler msg: " + msg); + } + } + } + }; + } + + return mTemporaryHandler; + } + + private long getIdleTimeoutMs() { + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, TimeUnit.HOURS.toMillis(1), + mContext.getUserId()); + } + + private int getRemoteInferenceServiceUid() { + synchronized (mLock) { + return remoteInferenceServiceUid; + } + } + + private void setRemoteInferenceServiceUid(int remoteInferenceServiceUid) { + synchronized (mLock) { + this.remoteInferenceServiceUid = remoteInferenceServiceUid; + } + } +} diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java new file mode 100644 index 000000000000..d2c84fa1b18a --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import android.annotation.NonNull; +import android.os.Binder; +import android.os.ShellCommand; + +import java.io.PrintWriter; +import java.util.Objects; + +final class OnDeviceIntelligenceShellCommand extends ShellCommand { + private static final String TAG = OnDeviceIntelligenceShellCommand.class.getSimpleName(); + + @NonNull + private final OnDeviceIntelligenceManagerService mService; + + OnDeviceIntelligenceShellCommand(@NonNull OnDeviceIntelligenceManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + switch (cmd) { + case "set-temporary-services": + return setTemporaryServices(); + case "get-services": + return getConfiguredServices(); + case "set-model-broadcasts": + return setBroadcastKeys(); + case "set-deviceconfig-namespace": + return setDeviceConfigNamespace(); + default: + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("OnDeviceIntelligenceShellCommand commands: "); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(); + pw.println( + " set-temporary-services [IntelligenceServiceComponentName] " + + "[InferenceServiceComponentName] [DURATION]"); + pw.println(" Temporarily (for DURATION ms) changes the service implementations."); + pw.println(" To reset, call without any arguments."); + + pw.println(" get-services To get the names of services that are currently being used."); + pw.println( + " set-model-broadcasts [ModelLoadedBroadcastKey] [ModelUnloadedBroadcastKey] " + + "[ReceiverPackageName] " + + "[DURATION] To set the names of broadcast intent keys that are to be " + + "emitted for cts tests."); + pw.println( + " set-deviceconfig-namespace [DeviceConfigNamespace] " + + "[DURATION] To set the device config namespace " + + "to use for cts tests."); + } + + private int setTemporaryServices() { + final PrintWriter out = getOutPrintWriter(); + final String intelligenceServiceName = getNextArg(); + final String inferenceServiceName = getNextArg(); + + if (getRemainingArgsCount() == 0 && intelligenceServiceName == null + && inferenceServiceName == null) { + OnDeviceIntelligenceManagerService.enforceShellOnly(Binder.getCallingUid(), + "resetTemporaryServices"); + mService.resetTemporaryServices(); + out.println("OnDeviceIntelligenceManagerService temporary reset. "); + return 0; + } + + Objects.requireNonNull(intelligenceServiceName); + Objects.requireNonNull(inferenceServiceName); + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryServices( + new String[]{intelligenceServiceName, inferenceServiceName}, + duration); + out.println("OnDeviceIntelligenceService temporarily set to " + intelligenceServiceName + + " \n and \n OnDeviceTrustedInferenceService set to " + inferenceServiceName + + " for " + duration + "ms"); + return 0; + } + + private int getConfiguredServices() { + final PrintWriter out = getOutPrintWriter(); + String[] services = mService.getServiceNames(); + out.println("OnDeviceIntelligenceService set to : " + services[0] + + " \n and \n OnDeviceTrustedInferenceService set to : " + services[1]); + return 0; + } + + private int setBroadcastKeys() { + final PrintWriter out = getOutPrintWriter(); + final String modelLoadedKey = getNextArgRequired(); + final String modelUnloadedKey = getNextArgRequired(); + final String receiverPackageName = getNextArg(); + + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setModelBroadcastKeys( + new String[]{modelLoadedKey, modelUnloadedKey}, receiverPackageName, duration); + out.println("OnDeviceIntelligence Model Loading broadcast keys temporarily set to " + + modelLoadedKey + + " \n and \n OnDeviceTrustedInferenceService set to " + modelUnloadedKey + + "\n and Package name set to : " + receiverPackageName + + " for " + duration + "ms"); + return 0; + } + + private int setDeviceConfigNamespace() { + final PrintWriter out = getOutPrintWriter(); + final String configNamespace = getNextArg(); + + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryDeviceConfigNamespace(configNamespace, duration); + out.println("OnDeviceIntelligence DeviceConfig Namespace temporarily set to " + + configNamespace + + " for " + duration + "ms"); + return 0; + } + +}
\ No newline at end of file diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java new file mode 100644 index 000000000000..ac9747aa83b3 --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import static android.content.Context.BIND_FOREGROUND_SERVICE; +import static android.content.Context.BIND_INCLUDE_CAPABILITIES; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.service.ondeviceintelligence.IOnDeviceIntelligenceService; +import android.service.ondeviceintelligence.OnDeviceIntelligenceService; + +import com.android.internal.infra.ServiceConnector; + +import java.util.concurrent.TimeUnit; + +/** + * Manages the connection to the remote on-device intelligence service. Also, handles unbinding + * logic set by the service implementation via a Secure Settings flag. + */ +public class RemoteOnDeviceIntelligenceService extends + ServiceConnector.Impl<IOnDeviceIntelligenceService> { + private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(4); + private static final String TAG = + RemoteOnDeviceIntelligenceService.class.getSimpleName(); + + RemoteOnDeviceIntelligenceService(Context context, ComponentName serviceName, + int userId) { + super(context, new Intent( + OnDeviceIntelligenceService.SERVICE_INTERFACE).setComponent(serviceName), + BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId, + IOnDeviceIntelligenceService.Stub::asInterface); + + // Bind right away + connect(); + } + + @Override + protected long getRequestTimeoutMs() { + return LONG_TIMEOUT; + } + + @Override + protected long getAutoDisconnectTimeoutMs() { + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, + TimeUnit.SECONDS.toMillis(30), + mContext.getUserId()); + } +} diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java new file mode 100644 index 000000000000..18b13838ea7c --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence; + +import static android.content.Context.BIND_FOREGROUND_SERVICE; +import static android.content.Context.BIND_INCLUDE_CAPABILITIES; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService; +import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; + +import com.android.internal.infra.ServiceConnector; + +import java.util.concurrent.TimeUnit; + + +/** + * Manages the connection to the remote on-device sand boxed inference service. Also, handles + * unbinding + * logic set by the service implementation via a SecureSettings flag. + */ +public class RemoteOnDeviceSandboxedInferenceService extends + ServiceConnector.Impl<IOnDeviceSandboxedInferenceService> { + private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(1); + + /** + * Creates an instance of {@link ServiceConnector} + * + * See {@code protected} methods for optional parameters you can override. + * + * @param context to be used for {@link Context#bindServiceAsUser binding} and + * {@link Context#unbindService unbinding} + * @param userId to be used for {@link Context#bindServiceAsUser binding} + */ + RemoteOnDeviceSandboxedInferenceService(Context context, ComponentName serviceName, + int userId) { + super(context, new Intent( + OnDeviceSandboxedInferenceService.SERVICE_INTERFACE).setComponent(serviceName), + BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId, + IOnDeviceSandboxedInferenceService.Stub::asInterface); + + // Bind right away + connect(); + } + + @Override + protected long getRequestTimeoutMs() { + return LONG_TIMEOUT; + } + + + @Override + protected long getAutoDisconnectTimeoutMs() { + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, + TimeUnit.SECONDS.toMillis(30), + mContext.getUserId()); + } +} diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java new file mode 100644 index 000000000000..32f0698a8f9c --- /dev/null +++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence.callbacks; + +import android.app.ondeviceintelligence.IDownloadCallback; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import com.android.internal.infra.AndroidFuture; + +import java.util.concurrent.TimeoutException; + +/** + * This class extends the {@link IDownloadCallback} and adds a timeout Runnable to the callback + * such that, in the case where the callback methods are not invoked, we do not have to wait for + * timeout based on {@link #onDownloadCompleted} which might take minutes or hours to complete in + * some cases. Instead, in such cases we rely on the remote service sending progress updates and if + * there are *no* progress callbacks in the duration of {@link #idleTimeoutMs}, we can assume the + * download will not complete and enabling faster cleanup. + */ +public class ListenableDownloadCallback extends IDownloadCallback.Stub implements Runnable { + private final IDownloadCallback callback; + private final Handler handler; + private final AndroidFuture future; + private final long idleTimeoutMs; + + /** + * Constructor to create a ListenableDownloadCallback. + * + * @param callback callback to send download updates to caller. + * @param handler handler to schedule timeout runnable. + * @param future future to complete to signal the callback has reached a terminal state. + * @param idleTimeoutMs timeout within which download updates should be received. + */ + public ListenableDownloadCallback(IDownloadCallback callback, Handler handler, + AndroidFuture future, + long idleTimeoutMs) { + this.callback = callback; + this.handler = handler; + this.future = future; + this.idleTimeoutMs = idleTimeoutMs; + handler.postDelayed(this, + idleTimeoutMs); // init the timeout runnable in case no callback is ever invoked + } + + @Override + public void onDownloadStarted(long bytesToDownload) throws RemoteException { + callback.onDownloadStarted(bytesToDownload); + handler.removeCallbacks(this); + handler.postDelayed(this, idleTimeoutMs); + } + + @Override + public void onDownloadProgress(long bytesDownloaded) throws RemoteException { + callback.onDownloadProgress(bytesDownloaded); + handler.removeCallbacks(this); // remove previously queued timeout tasks. + handler.postDelayed(this, idleTimeoutMs); // queue fresh timeout task for next update. + } + + @Override + public void onDownloadFailed(int failureStatus, + String errorMessage, PersistableBundle errorParams) throws RemoteException { + callback.onDownloadFailed(failureStatus, errorMessage, errorParams); + handler.removeCallbacks(this); + future.completeExceptionally(new TimeoutException()); + } + + @Override + public void onDownloadCompleted( + android.os.PersistableBundle downloadParams) throws RemoteException { + callback.onDownloadCompleted(downloadParams); + handler.removeCallbacks(this); + future.complete(null); + } + + @Override + public void run() { + future.completeExceptionally( + new TimeoutException()); // complete the future as we haven't received updates + // for download progress. + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java index 63fe1b509751..e173c5e996df 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java @@ -25,13 +25,15 @@ import android.widget.Spinner; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.widget.spinner.R; /** * This preference uses Spinner & SettingsSpinnerAdapter which provide default layouts for * both view and drop down view of the Spinner. */ -public class SettingsSpinnerPreference extends Preference implements OnPreferenceClickListener { +public class SettingsSpinnerPreference extends Preference + implements OnPreferenceClickListener, GroupSectionDividerMixin { private SettingsSpinnerAdapter mAdapter; private AdapterView.OnItemSelectedListener mListener; diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml index 5a4d3ce5661b..63c8929ef652 100644 --- a/packages/SettingsLib/res/values/arrays.xml +++ b/packages/SettingsLib/res/values/arrays.xml @@ -667,4 +667,25 @@ <item>3</item> </string-array> + <!-- Options for showing shade on external display for developers --> + <string-array name="shade_display_awareness_entries" > + <item>Device display only (Default)</item> + <item>External display</item> + <item>Focus-based</item> + </string-array> + + <!-- Options for showing shade on external display for developers --> + <string-array name="shade_display_awareness_summaries" > + <item>Show shade on device display only </item> + <item>Show device on single external display</item> + <item>Show device on last focused display</item> + </string-array> + + <!-- Values for showing shade on external display for developers --> + <string-array name="shade_display_awareness_values" > + <item>device-display</item> + <item>external-display</item> + <item>focus-based</item> + </string-array> + </resources> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index eaf155df4785..e1929b725a58 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -990,6 +990,9 @@ <!-- UI debug setting: simulate secondary display devices using overlays [CHAR LIMIT=45] --> <string name="overlay_display_devices_title">Simulate secondary displays</string> + <!-- UI debug setting: shade display awareness title [CHAR LIMIT=45] --> + <string name="shade_display_awareness_title">Shade display position</string> + <!-- Preference category for application debugging development settings. [CHAR LIMIT=25] --> <string name="debug_applications_category">Apps</string> diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9004488c2e12..c88a7fd834d6 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -182,6 +182,7 @@ public class SettingsBackupTest { Settings.Global.DEVELOPMENT_FORCE_RTL, Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, Settings.Global.DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, + Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS, Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH, Settings.Global.DEVICE_DEMO_MODE, Settings.Global.DEVICE_IDLE_CONSTANTS, diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 0694b6123c11..c6555041164d 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -257,6 +257,9 @@ public class BugreportProgressService extends Service { /** Always keep remote bugreport files created in the last day. */ private static final long REMOTE_MIN_KEEP_AGE = DateUtils.DAY_IN_MILLIS; + /** Minimum delay for sending last update notification */ + private static final int DELAY_NOTIFICATION_MS = 250; + private final Object mLock = new Object(); /** Managed bugreport info (keyed by id) */ @@ -927,6 +930,7 @@ public class BugreportProgressService extends Service { Log.d(TAG, "Progress #" + info.id + ": " + percentageText); } info.lastProgress.set(progress); + info.lastUpdate.set(System.currentTimeMillis()); sendForegroundabledNotification(info.id, builder.build()); } @@ -1455,6 +1459,16 @@ public class BugreportProgressService extends Service { */ private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) { + final long lastUpdate = System.currentTimeMillis() - info.lastUpdate.longValue(); + if (lastUpdate < DELAY_NOTIFICATION_MS) { + Log.d(TAG, "Delaying final notification for " + + (DELAY_NOTIFICATION_MS - lastUpdate) + " ms "); + mMainThreadHandler.postDelayed(() -> { + sendBugreportNotification(info, takingScreenshot); + }, DELAY_NOTIFICATION_MS - lastUpdate); + return; + } + // Since adding the details can take a while, do it before notifying user. addDetailsToZipFile(info); @@ -1475,6 +1489,7 @@ public class BugreportProgressService extends Service { final Notification.Builder builder = newBaseNotification(mContext) .setContentTitle(title) .setTicker(title) + .setProgress(100 /* max value of progress percentage */, 100, false) .setOnlyAlertOnce(false) .setContentText(content); @@ -2743,7 +2758,6 @@ public class BugreportProgressService extends Service { } } info.progress.set(progress); - info.lastUpdate.set(System.currentTimeMillis()); updateProgress(info); } diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 050a3704df1f..7bda2ea790b0 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -193,7 +193,7 @@ public class BugreportReceiverTest { mService.mScreenshotDelaySec = SCREENSHOT_DELAY_SECONDS; // Dup the fds which are passing to startBugreport function. Mockito.doAnswer(invocation -> { - final boolean isScreenshotRequested = invocation.getArgument(6); + final boolean isScreenshotRequested = invocation.getArgument(7); if (isScreenshotRequested) { mScreenshotFd = ParcelFileDescriptor.dup(invocation.getArgument(3)); } @@ -250,7 +250,22 @@ public class BugreportReceiverTest { mIDumpstateListener.onProgress(300); assertProgressNotification(mProgressTitle, 99); - Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId); + Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 1); + assertActionSendMultiple(extras); + + assertServiceNotRunning(); + } + + @Test + public void testStressProgress() throws Exception { + sendBugreportStarted(); + waitForScreenshotButtonEnabled(true); + + for (int i = 0; i <= 1000; i++) { + mIDumpstateListener.onProgress(i); + } + sendBugreportFinished(); + Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); assertActionSendMultiple(extras); assertServiceNotRunning(); @@ -277,7 +292,7 @@ public class BugreportReceiverTest { assertScreenshotButtonEnabled(false); waitForScreenshotButtonEnabled(true); - Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId); + Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 2); assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1); assertServiceNotRunning(); @@ -294,7 +309,7 @@ public class BugreportReceiverTest { // There's no indication in the UI about the screenshot finish, so just sleep like a baby... sleep(SAFE_SCREENSHOT_DELAY * DateUtils.SECOND_IN_MILLIS); - Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId); + Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 2); assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1); assertServiceNotRunning(); @@ -328,7 +343,7 @@ public class BugreportReceiverTest { assertProgressNotification(NEW_NAME, 00.00f); - Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE); + Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE, 1); assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0); assertServiceNotRunning(); @@ -363,7 +378,7 @@ public class BugreportReceiverTest { assertProgressNotification(NEW_NAME, 00.00f); - Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE); + Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE, 1); assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0); assertServiceNotRunning(); @@ -390,7 +405,7 @@ public class BugreportReceiverTest { detailsUi.descField.setText(mDescription); detailsUi.clickOk(); - Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId); + Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 1); assertActionSendMultiple(extras, NO_NAME, NO_TITLE, mDescription, 0); assertServiceNotRunning(); @@ -441,7 +456,7 @@ public class BugreportReceiverTest { detailsUi.clickOk(); // Finally, share bugreport. - Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId); + Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); assertActionSendMultiple(extras, NO_NAME, TITLE, mDescription, 0); assertServiceNotRunning(); @@ -504,7 +519,7 @@ public class BugreportReceiverTest { mUiBot.click(ok, "ok"); // Share the bugreport. - mUiBot.chooseActivity(UI_NAME); + mUiBot.chooseActivity(UI_NAME, mContext, 1); Bundle extras = mListener.getExtras(); assertActionSendMultiple(extras); @@ -531,7 +546,7 @@ public class BugreportReceiverTest { sendBugreportFinished(); killService(); assertServiceNotRunning(); - Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId); + Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); assertActionSendMultiple(extras); } @@ -618,45 +633,49 @@ public class BugreportReceiverTest { * Sends a "bugreport finished" event and waits for the result. * * @param id The bugreport id for finished notification string title substitution. + * @param count Number of files to be shared * @return extras sent in the shared intent. */ - private Bundle sendBugreportFinishedAndGetSharedIntent(int id) throws Exception { + private Bundle sendBugreportFinishedAndGetSharedIntent(int id, int count) throws Exception { sendBugreportFinished(); - return acceptBugreportAndGetSharedIntent(id); + return acceptBugreportAndGetSharedIntent(id, count); } /** * Sends a "bugreport finished" event and waits for the result. * * @param notificationTitle The title of finished notification. + * @param count Number of files to be shared * @return extras sent in the shared intent. */ - private Bundle sendBugreportFinishedAndGetSharedIntent(String notificationTitle) + private Bundle sendBugreportFinishedAndGetSharedIntent(String notificationTitle, int count) throws Exception { sendBugreportFinished(); - return acceptBugreportAndGetSharedIntent(notificationTitle); + return acceptBugreportAndGetSharedIntent(notificationTitle, count); } /** * Accepts the notification to share the finished bugreport and waits for the result. * * @param id The bugreport id for finished notification string title substitution. + * @param count Number of files to be shared * @return extras sent in the shared intent. */ - private Bundle acceptBugreportAndGetSharedIntent(int id) { + private Bundle acceptBugreportAndGetSharedIntent(int id, int count) { final String notificationTitle = mContext.getString(R.string.bugreport_finished_title, id); - return acceptBugreportAndGetSharedIntent(notificationTitle); + return acceptBugreportAndGetSharedIntent(notificationTitle, count); } /** * Accepts the notification to share the finished bugreport and waits for the result. * * @param notificationTitle The title of finished notification. + * @param count Number of files to be shared * @return extras sent in the shared intent. */ - private Bundle acceptBugreportAndGetSharedIntent(String notificationTitle) { + private Bundle acceptBugreportAndGetSharedIntent(String notificationTitle, int count) { mUiBot.clickOnNotification(notificationTitle); - mUiBot.chooseActivity(UI_NAME); + mUiBot.chooseActivity(UI_NAME, mContext, count); return mListener.getExtras(); } diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java index ce9f70d8b977..60008a353d81 100644 --- a/packages/Shell/tests/src/com/android/shell/UiBot.java +++ b/packages/Shell/tests/src/com/android/shell/UiBot.java @@ -18,9 +18,12 @@ package com.android.shell; import android.app.Instrumentation; import android.app.StatusBarManager; +import android.content.Context; +import android.content.res.Resources; import android.os.SystemClock; import android.text.format.DateUtils; import android.util.Log; +import android.util.PluralsMessageFormatter; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiDevice; @@ -34,7 +37,9 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A helper class for UI-related testing tasks. @@ -206,11 +211,26 @@ final class UiBot { * * @param name name of the activity as displayed in the UI (typically the value set by * {@code android:label} in the manifest). + * @param context Context of the target application + * @param count Number of files to be shared */ - public void chooseActivity(String name) { + public void chooseActivity(String name, Context context, int count) { // It uses an intent chooser now, so just getting the activity by text is enough... - final String share = mInstrumentation.getContext().getString( - com.android.internal.R.string.share); + Resources res = null; + try { + res = context.getPackageManager() + .getResourcesForApplication("com.android.intentresolver"); + } catch (Exception e) { + assertNotNull("could not get resources for com.android.intentresolver", res); + } + /* Resource read is defined as a string which contains a plural + * which needs some formatting */ + Map<String, Object> arguments = new HashMap<>(); + arguments.put("count", count); + final String share = PluralsMessageFormatter.format( + res, + arguments, + res.getIdentifier("sharing_files", "string", "com.android.intentresolver")); boolean gotIt = mDevice.wait(Until.hasObject(By.text(share)), mTimeout); assertTrue("could not get share activity (" + share + ")", gotIt); swipeUp(); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 777f6d35c1de..8cfae2bcc547 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1871,6 +1871,13 @@ flag { } flag { + name: "hub_edit_mode_touch_adjustments" + namespace: "systemui" + description: "Makes selected widget toggleable in edit mode and modifier buttons mutually exclusive." + bug: "383160667" +} + +flag { name: "glanceable_hub_direct_edit_mode" namespace: "systemui" description: "Invokes edit mode directly from long press in glanceable hub" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 85f549d43a11..55b42931b1fa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.composable import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.overscroll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -101,6 +102,7 @@ private fun SceneScope.BouncerScene( viewModel, dialogFactory, Modifier.element(Bouncer.Elements.Content) + .overscroll(verticalOverscrollEffect) .sysuiResTag(Bouncer.TestTags.Root) .fillMaxSize(), ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 315dc342dcd0..d5eaf829b746 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -296,9 +296,19 @@ fun CommunalHub( offset.y, ) - contentOffset val index = firstIndexAtOffset(gridState, adjustedOffset) - val key = + val tappedKey = index?.let { keyAtIndexIfEditable(contentListState.list, index) } - viewModel.setSelectedKey(key) + + viewModel.setSelectedKey( + if ( + Flags.hubEditModeTouchAdjustments() && + selectedKey.value == tappedKey + ) { + null + } else { + tappedKey + } + ) } } } @@ -1080,17 +1090,27 @@ private fun Toolbar( .onSizeChanged { setToolbarSize(it) } ) { val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text) - ToolbarButton( - isPrimary = !removeEnabled, - modifier = Modifier.align(Alignment.CenterStart), - onClick = onOpenWidgetPicker, - ) { - Icon(Icons.Default.Add, null) - Text(text = addWidgetText) + + if (!(Flags.hubEditModeTouchAdjustments() && removeEnabled)) { + ToolbarButton( + isPrimary = !removeEnabled, + modifier = Modifier.align(Alignment.CenterStart), + onClick = onOpenWidgetPicker, + ) { + Icon(Icons.Default.Add, null) + Text(text = addWidgetText) + } } AnimatedVisibility( - modifier = Modifier.align(Alignment.Center), + modifier = + Modifier.align( + if (Flags.hubEditModeTouchAdjustments()) { + Alignment.CenterStart + } else { + Alignment.Center + } + ), visible = removeEnabled, enter = fadeIn(), exit = fadeOut(), @@ -1113,7 +1133,11 @@ private fun Toolbar( horizontalArrangement = Arrangement.spacedBy( ButtonDefaults.IconSpacing, - Alignment.CenterHorizontally, + if (Flags.hubEditModeTouchAdjustments()) { + Alignment.Start + } else { + Alignment.CenterHorizontally + }, ), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt index 364adcaffd77..5e9ade163ac2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -82,20 +82,15 @@ constructor( Modifier.shortcutPadding() } else { Modifier - } + }, ) } } } @Composable - fun SceneScope.IndicationArea( - modifier: Modifier = Modifier, - ) { - Element( - key = IndicationAreaElementKey, - modifier = modifier.indicationAreaPadding(), - ) { + fun SceneScope.IndicationArea(modifier: Modifier = Modifier) { + Element(key = IndicationAreaElementKey, modifier = modifier.indicationAreaPadding()) { content { IndicationArea( indicationAreaViewModel = indicationAreaViewModel, @@ -138,24 +133,20 @@ constructor( ResourcesCompat.getDrawable( context.resources, R.drawable.keyguard_bottom_affordance_bg, - context.theme + context.theme, ) foreground = ResourcesCompat.getDrawable( context.resources, R.drawable.keyguard_bottom_affordance_selected_border, - context.theme + context.theme, ) visibility = View.INVISIBLE setPadding(padding, padding, padding, padding) } setBinding( - binder.bind( - view, - viewModel, - transitionAlpha, - ) { + binder.bind(view, viewModel, transitionAlpha) { indicationController.showTransientIndication(it) } ) @@ -164,10 +155,7 @@ constructor( }, onRelease = { binding?.destroy() }, modifier = - modifier.size( - width = shortcutSizeDp().width, - height = shortcutSizeDp().height, - ) + modifier.size(width = shortcutSizeDp().width, height = shortcutSizeDp().height), ) } @@ -182,6 +170,8 @@ constructor( AndroidView( factory = { context -> val view = KeyguardIndicationArea(context, null) + view.isFocusable = true + view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES setDisposable( KeyguardIndicationAreaBinder.bind( view = view, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt index 48067ce3c4a0..ef8911dae566 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.content.state.TransitionState import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.model.ShadeMode @@ -42,7 +42,7 @@ import kotlinx.coroutines.launch * transition. */ @Composable -fun SceneScope.NotificationLockscreenScrim( +fun ContentScope.NotificationLockscreenScrim( viewModel: NotificationLockscreenScrimViewModel, modifier: Modifier = Modifier, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt index 94c18cdbef5a..cb87f0e7cf1c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt @@ -62,7 +62,6 @@ fun NotificationScrimNestedScrollConnection( canStartPostScroll = { offsetAvailable, _, _ -> offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll()) }, - canStartPostFling = { false }, onStart = { firstScroll -> onStart(firstScroll) object : ScrollController { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt index d8abfd7a4b94..e1ee59ba0626 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -25,11 +25,13 @@ import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtLeast import com.android.compose.nestedscroll.OnStopScope @@ -80,9 +82,29 @@ fun Modifier.stackVerticalOverscroll( } return this.then( - Modifier.nestedScroll(stackNestedScrollConnection).offset { - IntOffset(x = 0, y = overscrollOffset.value.roundToInt()) - } + Modifier.nestedScroll( + remember { + object : NestedScrollConnection { + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + return if (available.y < 0f && !canScrollForward()) { + overscrollOffset.animateTo( + targetValue = 0f, + initialVelocity = available.y, + animationSpec = tween(), + ) + available + } else { + Velocity.Zero + } + } + } + } + ) + .nestedScroll(stackNestedScrollConnection) + .offset { IntOffset(x = 0, y = overscrollOffset.value.roundToInt()) } ) } @@ -100,7 +122,6 @@ fun NotificationStackNestedScrollConnection( canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ -> offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward() }, - canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() }, onStart = { firstScroll -> onStart(firstScroll) object : ScrollController { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index ae273d8e2ad9..b54de784a202 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -45,6 +45,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.overscroll import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme @@ -84,10 +85,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.NestedScrollBehavior -import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayoutState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf @@ -134,7 +135,7 @@ private val quickSettingsShadeContentKey: ContentKey * entire size of the scene. */ @Composable -fun SceneScope.HeadsUpNotificationSpace( +fun ContentScope.HeadsUpNotificationSpace( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, useHunBounds: () -> Boolean = { true }, @@ -176,7 +177,7 @@ fun SceneScope.HeadsUpNotificationSpace( * the user. When swiped up, the heads up notification is snoozed. */ @Composable -fun SceneScope.SnoozeableHeadsUpNotificationSpace( +fun ContentScope.SnoozeableHeadsUpNotificationSpace( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, ) { @@ -246,7 +247,7 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( /** Adds the space where notification stack should appear in the scene. */ @Composable -fun SceneScope.ConstrainedNotificationStack( +fun ContentScope.ConstrainedNotificationStack( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, modifier: Modifier = Modifier, @@ -281,7 +282,7 @@ fun SceneScope.ConstrainedNotificationStack( */ @OptIn(ExperimentalLayoutApi::class) @Composable -fun SceneScope.NotificationScrollingStack( +fun ContentScope.NotificationScrollingStack( shadeSession: SaveableSession, stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, @@ -480,6 +481,7 @@ fun SceneScope.NotificationScrollingStack( modifier = modifier .element(Notifications.Elements.NotificationScrim) + .overscroll(verticalOverscrollEffect) .offset { // if scrim is expanded while transitioning to Gone or QS scene, increase the // offset in step with the corresponding transition so that it is 0 when it @@ -622,7 +624,7 @@ fun SceneScope.NotificationScrollingStack( * the notification contents (stack, footer, shelf) should be drawn. */ @Composable -fun SceneScope.NotificationStackCutoffGuideline( +fun ContentScope.NotificationStackCutoffGuideline( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, modifier: Modifier = Modifier, @@ -642,7 +644,7 @@ fun SceneScope.NotificationStackCutoffGuideline( } @Composable -private fun SceneScope.NotificationPlaceholder( +private fun ContentScope.NotificationPlaceholder( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, useStackBounds: () -> Boolean, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 5fb9416cf35b..e4f4df386583 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -37,6 +37,7 @@ import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotific import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.GoneUserActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -70,18 +71,22 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { - val isIdle by remember { - derivedStateOf { layoutState.transitionState is TransitionState.Idle } + val isIdleAndNotOccluded by remember { + derivedStateOf { + layoutState.transitionState is TransitionState.Idle && + Overlays.NotificationsShade !in layoutState.transitionState.currentOverlays + } } - LaunchedEffect(isIdle) { + LaunchedEffect(isIdleAndNotOccluded) { // Wait for being Idle on this Scene, otherwise LaunchedEffect would fire too soon, // and another transition could override the NSSL stack bounds. - if (isIdle) { + if (isIdleAndNotOccluded) { // Reset the stack bounds to avoid caching these values from the previous Scenes, // and not to confuse the StackScrollAlgorithm when it displays a HUN over GONE. notificationStackScrolLView.get().apply { - setStackTop(0f) + // use -headsUpInset to allow HUN translation outside bounds for snoozing + setStackTop(-getHeadsUpInset().toFloat()) setStackCutoff(0f) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 9de7a5d659ae..55fafd5cfeca 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -33,7 +33,6 @@ import com.android.systemui.scene.ui.composable.transitions.notificationsShadeTo import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.toNotificationsShadeTransition import com.android.systemui.scene.ui.composable.transitions.toQuickSettingsShadeTransition -import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade /** @@ -134,27 +133,11 @@ val SceneContainerTransitions = transitions { } // Scene overscroll - + // TODO(b/382477212) Remove STL Overscroll DSL overscrollDisabled(Scenes.Gone, Orientation.Vertical) overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical) - overscroll(Scenes.Bouncer, Orientation.Vertical) { - translate(Bouncer.Elements.Content, y = { absoluteDistance }) - } - overscroll(Scenes.Shade, Orientation.Vertical) { - translate( - Notifications.Elements.NotificationScrim, - y = Shade.Dimensions.ScrimOverscrollLimit, - ) - translate(Shade.Elements.SplitShadeStartColumn, y = Shade.Dimensions.ScrimOverscrollLimit) - translate( - Notifications.Elements.NotificationStackPlaceholder, - y = Shade.Dimensions.ScrimOverscrollLimit, - ) - } - overscroll(Overlays.NotificationsShade, Orientation.Vertical) { - translate(OverlayShade.Elements.Panel, y = OverlayShade.Dimensions.OverscrollLimit) - } - overscroll(Overlays.QuickSettingsShade, Orientation.Vertical) { - translate(OverlayShade.Elements.Panel, y = OverlayShade.Dimensions.OverscrollLimit) - } + overscrollDisabled(Scenes.Bouncer, Orientation.Vertical) + overscrollDisabled(Scenes.Shade, Orientation.Vertical) + overscrollDisabled(Overlays.NotificationsShade, Orientation.Vertical) + overscrollDisabled(Overlays.QuickSettingsShade, Orientation.Vertical) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 46f5ecd99301..8a5c96da5ac6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsIgnoringVisibility import androidx.compose.foundation.layout.waterfall import androidx.compose.foundation.layout.width +import androidx.compose.foundation.overscroll import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -65,7 +66,10 @@ fun SceneScope.OverlayShade( Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = Alignment.TopEnd) { Panel( - modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(), + modifier = + Modifier.element(OverlayShade.Elements.Panel) + .overscroll(verticalOverscrollEffect) + .panelSize(), content = content, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 22b6dbcf41ec..79fd1d7ddd8f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.overscroll import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -527,6 +528,7 @@ private fun SceneScope.SplitShade( Box( modifier = Modifier.element(Shade.Elements.SplitShadeStartColumn) + .overscroll(verticalOverscrollEffect) .weight(1f) .graphicsLayer { translationX = unfoldTranslationXForStartSide } ) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 2d589f37f3cb..974442494181 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.content.Content -import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified +import com.android.compose.animation.scene.content.state.TransitionState.DirectionProperties.Companion.DistanceUnspecified import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController @@ -108,7 +108,7 @@ internal class DraggableHandlerImpl( swipes.updateSwipesResults(fromContent) val result = - swipes.findUserActionResult(overSlop) + (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult) // As we were unable to locate a valid target scene, the initial SwipeAnimation // cannot be defined. Consequently, a simple NoOp Controller will be returned. ?: return NoOpDragController @@ -448,27 +448,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol this.upOrLeftResult = upOrLeftResult this.downOrRightResult = downOrRightResult } - - /** - * Returns the [UserActionResult] in the direction of [directionOffset]. - * - * @param directionOffset signed float that indicates the direction. Positive is down or right - * negative is up or left. - * @return null when there are no targets in either direction. If one direction is null and you - * drag into the null direction this function will return the opposite direction, assuming - * that the users intention is to start the drag into the other direction eventually. If - * [directionOffset] is 0f and both direction are available, it will default to - * [upOrLeftResult]. - */ - fun findUserActionResult(directionOffset: Float): UserActionResult? { - return when { - upOrLeftResult == null && downOrRightResult == null -> null - (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> - upOrLeftResult - - else -> downOrRightResult - } - } } internal class NestedScrollHandlerImpl( @@ -536,31 +515,6 @@ internal class NestedScrollHandlerImpl( } } }, - canStartPostFling = { velocityAvailable -> - val behavior: NestedScrollBehavior = - when { - velocityAvailable > 0f -> topOrLeftBehavior - velocityAvailable < 0f -> bottomOrRightBehavior - else -> return@PriorityNestedScrollConnection false - } - - // We could start an overscroll animation - canChangeScene = false - - val pointersDown: PointersInfo.PointersDown? = - when (val info = pointersInfoOwner.pointersInfo()) { - PointersInfo.MouseWheel -> { - // Do not support mouse wheel interactions - return@PriorityNestedScrollConnection false - } - - is PointersInfo.PointersDown -> info - null -> null - } - lastPointersDown = pointersDown - - behavior.canStartOnPostFling && shouldEnableSwipes() - }, onStart = { firstScroll -> scrollController( dragController = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index e819bfd18578..07a19d83c995 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -1257,7 +1257,7 @@ private inline fun <T> computeValue( } val currentContent = currentContentState.content - if (transition is TransitionState.HasOverscrollProperties) { + if (transition is TransitionState.DirectionProperties) { val overscroll = transition.currentOverscrollSpec if (overscroll?.content == currentContent) { val elementSpec = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index 955be603efaf..9622fc151bb7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.platform.InspectorInfo * not consumed by the [SceneTransitionLayout] unless specifically requested via * [nestedScrollToScene]. */ -enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { +enum class NestedScrollBehavior { /** * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the * gesture begins at the edge of the scrollable component (so that a scroll in that direction @@ -42,7 +42,7 @@ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { * In addition, during scene transitions, scroll events are consumed by the * [SceneTransitionLayout] instead of the scrollable component. */ - EdgeNoPreview(canStartOnPostFling = false), + EdgeNoPreview, /** * Overscroll will only be used by the [SceneTransitionLayout] to move to the next scene if the @@ -52,7 +52,7 @@ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { * In addition, during scene transitions, scroll events are consumed by the * [SceneTransitionLayout] instead of the scrollable component. */ - EdgeWithPreview(canStartOnPostFling = true), + @Deprecated("This will be removed, see b/378470603") EdgeWithPreview, /** * Any overscroll will be used by the [SceneTransitionLayout] to move to the next scene. @@ -60,7 +60,7 @@ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { * In addition, during scene transitions, scroll events are consumed by the * [SceneTransitionLayout] instead of the scrollable component. */ - EdgeAlways(canStartOnPostFling = true); + EdgeAlways; companion object { val Default = EdgeNoPreview diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 86c5fd824d8f..e8b2b09da377 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -382,7 +382,7 @@ internal class MutableSceneTransitionLayoutStateImpl( // Compute the [TransformationSpec] when the transition starts. val fromContent = transition.fromContent val toContent = transition.toContent - val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation + val orientation = (transition as? TransitionState.DirectionProperties)?.orientation // Update the transition specs. transition.transformationSpec = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 59d0b55c1db8..5aaeda84edf0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.compose.animation.scene.content.state.TransitionState -import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified +import com.android.compose.animation.scene.content.state.TransitionState.DirectionProperties.Companion.DistanceUnspecified import kotlin.math.absoluteValue import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch @@ -197,7 +197,7 @@ internal class SwipeAnimation<T : ContentKey>( private val distance: (SwipeAnimation<T>) -> Float, currentContent: T = fromContent, dragOffset: Float = 0f, -) : TransitionState.HasOverscrollProperties { +) : TransitionState.DirectionProperties { /** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */ lateinit var contentTransition: TransitionState.Transition @@ -513,7 +513,7 @@ private class ChangeSceneSwipeTransition( swipeAnimation.toContent, replacedTransition, ), - TransitionState.HasOverscrollProperties by swipeAnimation { + TransitionState.DirectionProperties by swipeAnimation { constructor( other: ChangeSceneSwipeTransition @@ -575,7 +575,7 @@ private class ShowOrHideOverlaySwipeTransition( swipeAnimation.toContent, replacedTransition, ), - TransitionState.HasOverscrollProperties by swipeAnimation { + TransitionState.DirectionProperties by swipeAnimation { constructor( other: ShowOrHideOverlaySwipeTransition ) : this( @@ -634,7 +634,7 @@ private class ReplaceOverlaySwipeTransition( swipeAnimation.toContent, replacedTransition, ), - TransitionState.HasOverscrollProperties by swipeAnimation { + TransitionState.DirectionProperties by swipeAnimation { constructor( other: ReplaceOverlaySwipeTransition ) : this( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 48f08a7086d6..952668ab49ff 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -111,6 +111,9 @@ interface SceneTransitionsBuilder { * The overscroll animation always starts from a progress of 0f, and reaches 1f when moving the * [distance] down/right, -1f when moving in the opposite direction. */ + @Deprecated( + "Use verticalOverscrollEffect (or horizontalOverscrollEffect) directly from SceneScope." + ) fun overscroll( content: ContentKey, orientation: Orientation, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index d66fe42084de..29be445e82bb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -273,7 +273,7 @@ sealed interface TransitionState { * every time progress is changed. */ private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? = - if (this !is HasOverscrollProperties) { + if (this !is DirectionProperties) { null } else { derivedStateOf { @@ -406,7 +406,7 @@ sealed interface TransitionState { /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */ internal fun isWithinProgressRange(progress: Float): Boolean { // If the properties are missing we assume that every [Transition] can overscroll - if (this !is HasOverscrollProperties) return true + if (this !is DirectionProperties) return true // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet. val specForCurrentScene = when { @@ -444,7 +444,7 @@ sealed interface TransitionState { } } - interface HasOverscrollProperties { + interface DirectionProperties { /** * The position of the [Transition.toContent]. * diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt index bfb5ca733d90..944bd85991c9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -157,7 +157,7 @@ private class VerticalContainerRevealSizeTransformation( val idleSize = checkNotNull(element.targetSize(content)) val userActionDistance = idleSize.height val progress = - when ((transition as? TransitionState.HasOverscrollProperties)?.bouncingContent) { + when ((transition as? TransitionState.DirectionProperties)?.bouncingContent) { null -> transition.progressTo(content) content -> 1f else -> 0f @@ -256,7 +256,7 @@ private class ContainerRevealAlphaTransformation( private fun targetAlpha(transition: TransitionState.Transition, content: ContentKey): Float { if (transition.isUserInputOngoing) { - if (transition !is TransitionState.HasOverscrollProperties) { + if (transition !is TransitionState.DirectionProperties) { error( "Unsupported transition driven by user input but that does not have " + "overscroll properties: $transition" diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 2f4d5bff8b41..432add38385a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -60,7 +60,7 @@ private constructor( // As this object is created by OverscrollBuilderImpl and we retrieve the current // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume // that this method was invoked after performing this check. - val overscrollProperties = transition as TransitionState.HasOverscrollProperties + val overscrollProperties = transition as TransitionState.DirectionProperties val overscrollScope = cachedOverscrollScope.getFromCacheOrCompute(density = this, overscrollProperties) @@ -77,17 +77,17 @@ private constructor( /** * A helper class to cache a [OverscrollScope] given a [Density] and - * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame - * whenever an overscroll transition is computed. + * [TransitionState.DirectionProperties]. This helps avoid recreating a scope every frame whenever + * an overscroll transition is computed. */ private class CachedOverscrollScope { private var previousScope: OverscrollScope? = null private var previousDensity: Density? = null - private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null + private var previousOverscrollProperties: TransitionState.DirectionProperties? = null fun getFromCacheOrCompute( density: Density, - overscrollProperties: TransitionState.HasOverscrollProperties, + overscrollProperties: TransitionState.DirectionProperties, ): OverscrollScope { if ( previousScope == null || diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt index 98a00173f1d7..b26bf55c85ec 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt @@ -56,7 +56,6 @@ fun LargeTopAppBarNestedScrollConnection( canStartPostScroll = { offsetAvailable, _, _ -> offsetAvailable > 0 && height() < maxHeight() }, - canStartPostFling = { false }, onStart = { LargeTopAppBarScrollController( height = height, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index 3f182363e20c..3d0f182fffee 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Velocity import com.android.compose.ui.util.SpaceVectorConverter -import kotlin.math.sign import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -102,8 +101,8 @@ interface OnStopScope { * over the default nested scrolling logic. * * When started, this connection intercepts scroll events *before* they reach child composables. - * This "priority mode" is activated activated when either [canStartPreScroll], [canStartPostScroll] - * or [canStartPostFling] returns `true`. + * This "priority mode" is activated when either [canStartPreScroll] or [canStartPostScroll] returns + * `true`. * * Once started, the [onStart] lambda provides a [ScrollController] to manage the scrolling. This * controller allows you to directly manipulate the scroll state and define how scroll events are @@ -123,8 +122,6 @@ interface OnStopScope { * @param canStartPostScroll A lambda that returns `true` if the connection should enter priority * mode during the post-scroll phase. This is called after child connections have consumed the * scroll. - * @param canStartPostFling A lambda that returns `true` if the connection should enter priority - * mode during the post-fling phase. This is called after a fling gesture has been initiated. * @param onStart A lambda that is called when the connection enters priority mode. It should return * a [ScrollController] that will be used to control the scroll. * @sample LargeTopAppBarNestedScrollConnection @@ -136,7 +133,6 @@ class PriorityNestedScrollConnection( (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean, private val canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean, - private val canStartPostFling: (velocityAvailable: Float) -> Boolean, private val onStart: (firstScroll: Float) -> ScrollController, ) : NestedScrollConnection, SpaceVectorConverter by SpaceVectorConverter(orientation) { @@ -233,17 +229,6 @@ class PriorityNestedScrollConnection( return stop(velocity = availableFloat) } - // Check if post-fling condition is met, and start priority mode if necessary. - // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the - // overscroll behavior on the Scene level. - if (canStartPostFling(availableFloat)) { - // The offset passed to onPriorityStart() must be != 0f, so we create a small offset of - // 1px given the available velocity. - val smallOffset = availableFloat.sign - start(availableOffset = smallOffset) - return stop(availableFloat) - } - // Reset offset tracking after the fling gesture is finished. resetOffsetTracker() return Velocity.Zero diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index b20056d54de1..6985644579f6 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -19,7 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.overscroll import androidx.compose.material3.Text +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput @@ -102,7 +104,7 @@ class DraggableHandlerTest { userActions = mapOf(Swipe.Up to SceneB, Swipe.Up(fromSource = Edge.Bottom) to SceneA), ) { - Text("SceneC") + Text("SceneC", Modifier.overscroll(verticalOverscrollEffect)) } overlay( key = OverlayA, @@ -434,35 +436,12 @@ class DraggableHandlerTest { } @Test - fun onDragIntoNoAction_startTransitionToOppositeDirection() = runGestureTest { + fun onDragIntoNoAction_stayIdle() = runGestureTest { navigateToSceneC() // We are on SceneC which has no action in Down direction - val dragController = onDragStarted(overSlop = 10f) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - toScene = SceneB, - progress = -0.1f, - ) - - // Reverse drag direction, it will consume the previous drag - dragController.onDragDelta(pixels = -10f) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - toScene = SceneB, - progress = 0.0f, - ) - - // Continue reverse drag direction, it should record progress to Scene B - dragController.onDragDelta(pixels = -10f) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - toScene = SceneB, - progress = 0.1f, - ) + onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f) + assertIdle(currentScene = SceneC) } @Test @@ -942,30 +921,6 @@ class DraggableHandlerTest { } @Test - fun scrollFromIdleWithNoTargetScene_shouldUseOverscrollSpecIfAvailable() = runGestureTest { - layoutState.transitions = transitions { - overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) } - } - // Start at scene C. - navigateToSceneC() - - val scene = layoutState.transitionState.currentScene - // We should have overscroll spec for scene C - assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull() - assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull() - - val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) - nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) - - // We scrolled down, under scene C there is nothing, so we can use the overscroll spec - assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC) - val transition = layoutState.currentTransition - assertThat(transition).isNotNull() - assertThat(transition!!.progress).isEqualTo(-0.1f) - } - - @Test fun nestedScrollUseFromSourceInfo() = runGestureTest { // Start at scene C. navigateToSceneC() @@ -1229,72 +1184,6 @@ class DraggableHandlerTest { } @Test - fun overscroll_releaseAtNegativePercent_up() = runGestureTest { - // Make Scene A overscrollable. - layoutState.transitions = transitions { - from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } - overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } - } - - mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB)) - - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f)) - val transition = assertThat(transitionState).isSceneTransition() - assertThat(transition).hasFromScene(SceneA) - assertThat(transition).hasToScene(SceneB) - assertThat(transition).hasProgress(-1f) - - // Release to A. - dragController.onDragStoppedAnimateNow( - velocity = 0f, - onAnimationStart = { - assertTransition(fromScene = SceneA, toScene = SceneB, progress = -1f) - }, - expectedConsumedVelocity = 0f, - ) - - // We kept the overscroll at 100% so that the placement logic didn't change at the end of - // the animation. - assertIdle(SceneA) - assertThat(transition).hasProgress(0f) - assertThat(transition).hasOverscrollSpec() - } - - @Test - fun overscroll_releaseAtNegativePercent_down() = runGestureTest { - // Make Scene A overscrollable. - layoutState.transitions = transitions { - from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } - overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } - } - - mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC)) - - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f)) - val transition = assertThat(transitionState).isSceneTransition() - assertThat(transition).hasFromScene(SceneA) - assertThat(transition).hasToScene(SceneC) - assertThat(transition).hasProgress(-1f) - - // Release to A. - dragController.onDragStoppedAnimateNow( - velocity = 0f, - onAnimationStart = { - assertTransition(fromScene = SceneA, toScene = SceneC, progress = -1f) - }, - expectedConsumedVelocity = 0f, - ) - - // We kept the overscroll at 100% so that the placement logic didn't change at the end of - // the animation. - assertIdle(SceneA) - assertThat(transition).hasProgress(0f) - assertThat(transition).hasOverscrollSpec() - } - - @Test fun requireFullDistanceSwipe() = runGestureTest { mutableUserActionsA += Swipe.Up to UserActionResult(SceneB, requiresFullDistanceSwipe = true) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 4410e157b526..ffba63988cfc 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -1006,77 +1006,74 @@ class ElementTest { @Test fun elementTransitionDuringNestedScrollOverscroll() { + lateinit var density: Density // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is // detected as a drag event. var touchSlop = 0f - val overscrollTranslateY = 10.dp val layoutWidth = 200.dp val layoutHeight = 400.dp val state = rule.runOnUiThread { MutableSceneTransitionLayoutState( - initialScene = SceneB, - transitions = - transitions { - overscroll(SceneB, Orientation.Vertical) { - progressConverter = ProgressConverter.linear() - translate(TestElements.Foo, y = overscrollTranslateY) - } - }, + initialScene = SceneA, + transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }, ) - as MutableSceneTransitionLayoutStateImpl } rule.setContent { + density = LocalDensity.current touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout( state = state, modifier = Modifier.size(layoutWidth, layoutHeight), ) { - scene(SceneA) { Spacer(Modifier.fillMaxSize()) } - scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier // A scrollable that does not consume the scroll gesture .scrollable( - rememberScrollableState(consumeScrollDelta = { 0f }), - Orientation.Vertical, + state = rememberScrollableState(consumeScrollDelta = { 0f }), + orientation = Orientation.Vertical, ) .fillMaxSize() - ) { - Spacer(Modifier.element(TestElements.Foo).fillMaxSize()) - } + ) + } + scene(SceneB) { + Spacer( + Modifier.overscroll(verticalOverscrollEffect) + .element(TestElements.Foo) + .fillMaxSize() + ) } } } assertThat(state.transitionState).isIdle() - val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag) - fooElement.assertTopPositionInRootIsEqualTo(0.dp) + rule.onNodeWithTag(TestElements.Foo.testTag).assertDoesNotExist() // Swipe by half of verticalSwipeDistance. rule.onRoot().performTouchInput { val middleTop = Offset((layoutWidth / 2).toPx(), 0f) down(middleTop) - // Scroll 50% + // Scroll 50%. moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } val transition = assertThat(state.transitionState).isSceneTransition() - assertThat(transition).hasOverscrollSpec() - assertThat(transition).hasProgress(-0.5f) - fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) + assertThat(transition).hasProgress(0.5f) + rule.onNodeWithTag(TestElements.Foo.testTag).assertTopPositionInRootIsEqualTo(0.dp) rule.onRoot().performTouchInput { - // Scroll another 100% + // Scroll another 100%. moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } - // Scroll 150% (Scene B overscroll by 50%) - assertThat(transition).hasProgress(-1.5f) - assertThat(transition).hasOverscrollSpec() - fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) + // Scroll 150% (Scene B overscroll by 50%). + assertThat(transition).hasProgress(1f) + rule + .onNodeWithTag(TestElements.Foo.testTag) + .assertTopPositionInRootIsEqualTo(expectedOffset(layoutHeight * 0.5f, density)) } @Test @@ -2856,7 +2853,7 @@ class ElementTest { // Start an overscrollable transition driven by progress. var progress by mutableFloatStateOf(0f) val transition = transition(from = SceneA, to = SceneB, progress = { progress }) - assertThat(transition).isInstanceOf(TransitionState.HasOverscrollProperties::class.java) + assertThat(transition).isInstanceOf(TransitionState.DirectionProperties::class.java) scope.launch { state.startTransition(transition) } // Reset the counters after the first animation frame. diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt index 0adb4809dd2d..9a2af640c46f 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -168,12 +168,12 @@ abstract class BaseTransitionSubject<T : TransitionState.Transition>( fun hasBouncingContent(content: ContentKey) { val actual = actual - if (actual !is TransitionState.HasOverscrollProperties) { + if (actual !is TransitionState.DirectionProperties) { failWithActual(simpleFact("expected to be ContentState.HasOverscrollProperties")) } check("bouncingContent") - .that((actual as TransitionState.HasOverscrollProperties).bouncingContent) + .that((actual as TransitionState.DirectionProperties).bouncingContent) .isEqualTo(content) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt index 28ea2d239b54..51483a894e1e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt @@ -39,7 +39,6 @@ import org.junit.runner.RunWith class PriorityNestedScrollConnectionTest { private var canStartPreScroll = false private var canStartPostScroll = false - private var canStartPostFling = false private var canStopOnPreFling = true private var isStarted = false private var lastScroll: Float? = null @@ -63,7 +62,6 @@ class PriorityNestedScrollConnectionTest { orientation = Orientation.Vertical, canStartPreScroll = { _, _, _ -> canStartPreScroll }, canStartPostScroll = { _, _, _ -> canStartPostScroll }, - canStartPostFling = { canStartPostFling }, onStart = { _ -> isStarted = true object : ScrollController { @@ -239,36 +237,6 @@ class PriorityNestedScrollConnectionTest { } @Test - fun receive_onPostFling() = runTest { - canStartPostFling = true - - scrollConnection.onPostFling(consumed = Velocity(1f, 1f), available = Velocity(2f, 2f)) - - assertThat(lastStop).isEqualTo(2f) - } - - @Test - fun step1_priorityModeShouldStartOnlyOnPostFling() = runTest { - canStartPostFling = true - - scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput) - assertThat(isStarted).isEqualTo(false) - - scrollConnection.onPostScroll( - consumed = Offset.Zero, - available = Offset.Zero, - source = UserInput, - ) - assertThat(isStarted).isEqualTo(false) - - scrollConnection.onPreFling(available = Velocity.Zero) - assertThat(isStarted).isEqualTo(false) - - scrollConnection.onPostFling(consumed = Velocity.Zero, available = Velocity.Zero) - assertThat(isStarted).isEqualTo(true) - } - - @Test fun handleMultipleOnPreFlingCalls() = runTest { startPriorityModePostScroll() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt index 646cff8b944c..6015479d8e21 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt @@ -71,7 +71,7 @@ fun transition( ): TestOverlayTransition { return object : TestOverlayTransition(fromScene, overlay, replacedTransition), - TransitionState.HasOverscrollProperties { + TransitionState.DirectionProperties { override val isEffectivelyShown: Boolean get() = isEffectivelyShown() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt index c342f488212a..bd2118dd8395 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt @@ -68,7 +68,7 @@ fun transition( ): TestReplaceOverlayTransition { return object : TestReplaceOverlayTransition(from, to, replacedTransition), - TransitionState.HasOverscrollProperties { + TransitionState.DirectionProperties { override val effectivelyShownOverlay: OverlayKey get() = effectivelyShownOverlay() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt index d24b895c3050..1d27e3a3f191 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt @@ -62,7 +62,7 @@ fun transition( replacedTransition: Transition? = null, ): TestSceneTransition { return object : - TestSceneTransition(from, to, replacedTransition), TransitionState.HasOverscrollProperties { + TestSceneTransition(from, to, replacedTransition), TransitionState.DirectionProperties { override val currentScene: SceneKey get() = current() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java deleted file mode 100644 index ce57fe256798..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.keyguard; - -import static android.view.View.INVISIBLE; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.res.Resources; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; -import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.plugins.clocks.ClockAnimations; -import com.android.systemui.plugins.clocks.ClockController; -import com.android.systemui.plugins.clocks.ClockEvents; -import com.android.systemui.plugins.clocks.ClockFaceConfig; -import com.android.systemui.plugins.clocks.ClockFaceController; -import com.android.systemui.plugins.clocks.ClockFaceEvents; -import com.android.systemui.plugins.clocks.ClockTickRate; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.res.R; -import com.android.systemui.shared.clocks.AnimatableClockView; -import com.android.systemui.shared.clocks.ClockRegistry; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; -import com.android.systemui.statusbar.phone.NotificationIconContainer; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.settings.SecureSettings; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { - - @Mock - protected KeyguardClockSwitch mView; - @Mock - protected StatusBarStateController mStatusBarStateController; - @Mock - protected ClockRegistry mClockRegistry; - @Mock - KeyguardSliceViewController mKeyguardSliceViewController; - @Mock - LockscreenSmartspaceController mSmartspaceController; - - @Mock - Resources mResources; - @Mock - KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; - @Mock - protected ClockController mClockController; - @Mock - protected ClockFaceController mLargeClockController; - @Mock - protected ClockFaceController mSmallClockController; - @Mock - protected ClockAnimations mClockAnimations; - @Mock - protected ClockEvents mClockEvents; - @Mock - protected ClockFaceEvents mClockFaceEvents; - @Mock - DumpManager mDumpManager; - @Mock - ClockEventController mClockEventController; - - @Mock - protected NotificationIconContainer mNotificationIcons; - @Mock - protected AnimatableClockView mSmallClockView; - @Mock - protected AnimatableClockView mLargeClockView; - @Mock - protected FrameLayout mSmallClockFrame; - @Mock - protected FrameLayout mLargeClockFrame; - @Mock - protected SecureSettings mSecureSettings; - @Mock - protected LogBuffer mLogBuffer; - - @Mock - protected KeyguardClockInteractor mKeyguardClockInteractor; - - protected final View mFakeDateView = (View) (new ViewGroup(mContext) { - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) {} - }); - protected final View mFakeWeatherView = new View(mContext); - protected final View mFakeSmartspaceView = new View(mContext); - - protected KeyguardClockSwitchController mController; - protected View mSliceView; - protected LinearLayout mStatusArea; - protected FakeExecutor mExecutor; - protected FakeFeatureFlags mFakeFeatureFlags; - @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - mFakeDateView.setTag(R.id.tag_smartspace_view, new Object()); - mFakeWeatherView.setTag(R.id.tag_smartspace_view, new Object()); - mFakeSmartspaceView.setTag(R.id.tag_smartspace_view, new Object()); - - when(mView.findViewById(R.id.left_aligned_notification_icon_container)) - .thenReturn(mNotificationIcons); - when(mNotificationIcons.getLayoutParams()).thenReturn( - mock(RelativeLayout.LayoutParams.class)); - when(mView.getContext()).thenReturn(getContext()); - when(mView.getResources()).thenReturn(mResources); - when(mResources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)) - .thenReturn(100); - when(mResources.getDimensionPixelSize(com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin)) - .thenReturn(-200); - when(mResources.getInteger(com.android.internal.R.integer.config_doublelineClockDefault)) - .thenReturn(1); - when(mResources.getInteger(R.integer.keyguard_date_weather_view_invisibility)) - .thenReturn(INVISIBLE); - - when(mView - .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view_large)) - .thenReturn(mLargeClockFrame); - when(mView - .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view)) - .thenReturn(mSmallClockFrame); - when(mSmallClockView.getContext()).thenReturn(getContext()); - when(mLargeClockView.getContext()).thenReturn(getContext()); - - when(mView.isAttachedToWindow()).thenReturn(true); - when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView); - when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView); - when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView); - mExecutor = new FakeExecutor(new FakeSystemClock()); - mFakeFeatureFlags = new FakeFeatureFlags(); - mController = new KeyguardClockSwitchController( - mView, - mStatusBarStateController, - mClockRegistry, - mKeyguardSliceViewController, - mSmartspaceController, - mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class), - mKeyguardUnlockAnimationController, - mSecureSettings, - mExecutor, - mExecutor, - mDumpManager, - mClockEventController, - mLogBuffer, - KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(), - mKeyguardClockInteractor, - mock(InWindowLauncherUnlockAnimationManager.class) - ); - - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - when(mLargeClockController.getView()).thenReturn(mLargeClockView); - when(mSmallClockController.getView()).thenReturn(mSmallClockView); - when(mClockController.getLargeClock()).thenReturn(mLargeClockController); - when(mClockController.getSmallClock()).thenReturn(mSmallClockController); - when(mClockController.getEvents()).thenReturn(mClockEvents); - when(mSmallClockController.getEvents()).thenReturn(mClockFaceEvents); - when(mLargeClockController.getEvents()).thenReturn(mClockFaceEvents); - when(mLargeClockController.getAnimations()).thenReturn(mClockAnimations); - when(mSmallClockController.getAnimations()).thenReturn(mClockAnimations); - when(mClockRegistry.createCurrentClock()).thenReturn(mClockController); - when(mClockEventController.getClock()).thenReturn(mClockController); - when(mSmallClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false)); - when(mLargeClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false)); - - mSliceView = new View(getContext()); - when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); - mStatusArea = new LinearLayout(getContext()); - when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea); - } - - private void removeView(View v) { - ViewGroup group = ((ViewGroup) v.getParent()); - if (group != null) { - group.removeView(v); - } - } - - protected void init() { - mController.init(); - - verify(mView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachCaptor.getValue().onViewAttachedToWindow(mView); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java deleted file mode 100644 index 892375d002c1..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2020 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.keyguard; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.database.ContentObserver; -import android.os.UserHandle; -import android.platform.test.annotations.DisableFlags; -import android.provider.Settings; -import android.view.View; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.Flags; -import com.android.systemui.plugins.clocks.ClockFaceConfig; -import com.android.systemui.plugins.clocks.ClockTickRate; -import com.android.systemui.shared.clocks.ClockRegistry; -import com.android.systemui.statusbar.StatusBarState; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.verification.VerificationMode; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) -public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchControllerBaseTest { - @Test - public void testInit_viewAlreadyAttached() { - mController.init(); - - verifyAttachment(times(1)); - } - - @Test - public void testInit_viewNotYetAttached() { - ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - - when(mView.isAttachedToWindow()).thenReturn(false); - mController.init(); - verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture()); - - verifyAttachment(never()); - - listenerArgumentCaptor.getValue().onViewAttachedToWindow(mView); - - verifyAttachment(times(1)); - } - - @Test - public void testInitSubControllers() { - mController.init(); - verify(mKeyguardSliceViewController).init(); - } - - @Test - public void testInit_viewDetached() { - ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - mController.init(); - verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture()); - - verifyAttachment(times(1)); - - listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); - verify(mClockEventController).unregisterListeners(); - } - - @Test - public void testPluginPassesStatusBarState() { - ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); - - mController.init(); - verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture()); - - listenerArgumentCaptor.getValue().onCurrentClockChanged(); - verify(mView, times(2)).setClock(mClockController, StatusBarState.SHADE); - verify(mClockEventController, times(2)).setClock(mClockController); - } - - @Test - public void testSmartspaceEnabledRemovesKeyguardStatusArea() { - when(mSmartspaceController.isEnabled()).thenReturn(true); - mController.init(); - - assertEquals(View.GONE, mSliceView.getVisibility()); - } - - @Test - public void onLocaleListChangedRebuildsSmartspaceView() { - when(mSmartspaceController.isEnabled()).thenReturn(true); - mController.init(); - - mController.onLocaleListChanged(); - // Should be called once on initial setup, then once again for locale change - verify(mSmartspaceController, times(2)).buildAndConnectView(mView); - } - - @Test - public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() { - when(mSmartspaceController.isEnabled()).thenReturn(true); - when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true); - mController.init(); - - mController.onLocaleListChanged(); - // Should be called once on initial setup, then once again for locale change - verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView); - verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView); - verify(mSmartspaceController, times(2)).buildAndConnectView(mView); - } - - @Test - public void testSmartspaceDisabledShowsKeyguardStatusArea() { - when(mSmartspaceController.isEnabled()).thenReturn(false); - mController.init(); - - assertEquals(View.VISIBLE, mSliceView.getVisibility()); - } - - @Test - public void testRefresh() { - mController.refresh(); - - verify(mSmartspaceController).requestSmartspaceUpdate(); - } - - @Test - public void testChangeToDoubleLineClockSetsSmallClock() { - when(mSecureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1, - UserHandle.USER_CURRENT)) - .thenReturn(0); - ArgumentCaptor<ContentObserver> observerCaptor = - ArgumentCaptor.forClass(ContentObserver.class); - mController.init(); - mExecutor.runAllReady(); - verify(mSecureSettings).registerContentObserverForUserSync( - eq(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK), - anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL)); - ContentObserver observer = observerCaptor.getValue(); - mExecutor.runAllReady(); - - // When a settings change has occurred to the small clock, make sure the view is adjusted - reset(mView); - when(mView.getResources()).thenReturn(mResources); - observer.onChange(true); - mExecutor.runAllReady(); - verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true); - } - - @Test - public void testGetClock_ForwardsToClock() { - assertEquals(mClockController, mController.getClock()); - } - - @Test - public void testGetLargeClockBottom_returnsExpectedValue() { - when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE); - when(mLargeClockFrame.getHeight()).thenReturn(100); - when(mSmallClockFrame.getHeight()).thenReturn(50); - when(mLargeClockView.getHeight()).thenReturn(40); - when(mSmallClockView.getHeight()).thenReturn(20); - mController.init(); - - assertEquals(170, mController.getClockBottom(1000)); - } - - @Test - public void testGetSmallLargeClockBottom_returnsExpectedValue() { - when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE); - when(mLargeClockFrame.getHeight()).thenReturn(100); - when(mSmallClockFrame.getHeight()).thenReturn(50); - when(mLargeClockView.getHeight()).thenReturn(40); - when(mSmallClockView.getHeight()).thenReturn(20); - mController.init(); - - assertEquals(1120, mController.getClockBottom(1000)); - } - - @Test - public void testGetClockBottom_nullClock_returnsZero() { - when(mClockEventController.getClock()).thenReturn(null); - assertEquals(0, mController.getClockBottom(10)); - } - - @Test - public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() { - when(mSmartspaceController.isWeatherEnabled()).thenReturn(true); - ArgumentCaptor<ContentObserver> observerCaptor = - ArgumentCaptor.forClass(ContentObserver.class); - mController.init(); - mExecutor.runAllReady(); - verify(mSecureSettings).registerContentObserverForUserSync( - eq(Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED), anyBoolean(), - observerCaptor.capture(), eq(UserHandle.USER_ALL)); - ContentObserver observer = observerCaptor.getValue(); - mExecutor.runAllReady(); - // When a settings change has occurred, check that view is visible. - observer.onChange(true); - mExecutor.runAllReady(); - assertEquals(View.VISIBLE, mFakeWeatherView.getVisibility()); - } - - @Test - public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() { - ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); - when(mSmartspaceController.isEnabled()).thenReturn(true); - when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true); - when(mSmartspaceController.isWeatherEnabled()).thenReturn(true); - mController.init(); - mExecutor.runAllReady(); - assertEquals(View.VISIBLE, mFakeDateView.getVisibility()); - - when(mSmallClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true)); - when(mLargeClockController.getConfig()) - .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true)); - verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture()); - listenerArgumentCaptor.getValue().onCurrentClockChanged(); - - mExecutor.runAllReady(); - assertEquals(View.INVISIBLE, mFakeDateView.getVisibility()); - } - - @Test - public void testGetClock_nullClock_returnsNull() { - when(mClockEventController.getClock()).thenReturn(null); - assertNull(mController.getClock()); - } - - private void verifyAttachment(VerificationMode times) { - verify(mClockRegistry, times).registerClockChangeListener( - any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(mView); - } - - @Test - public void testSplitShadeEnabledSetToSmartspaceController() { - mController.setSplitShadeEnabled(true); - verify(mSmartspaceController, times(1)).setSplitShadeEnabled(true); - verify(mSmartspaceController, times(0)).setSplitShadeEnabled(false); - } - - @Test - public void testSplitShadeDisabledSetToSmartspaceController() { - mController.setSplitShadeEnabled(false); - verify(mSmartspaceController, times(1)).setSplitShadeEnabled(false); - verify(mSmartspaceController, times(0)).setSplitShadeEnabled(true); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java deleted file mode 100644 index 4ed5fd0a6e71..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (C) 2018 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.keyguard; - -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; - -import static com.android.keyguard.KeyguardClockSwitch.LARGE; -import static com.android.keyguard.KeyguardClockSwitch.SMALL; - -import static com.google.common.truth.Truth.assertThat; - -import static junit.framework.TestCase.assertEquals; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.platform.test.annotations.DisableFlags; -import android.testing.TestableLooper.RunWithLooper; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.TextView; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.Flags; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.clocks.ClockController; -import com.android.systemui.plugins.clocks.ClockFaceController; -import com.android.systemui.res.R; -import com.android.systemui.statusbar.StatusBarState; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -// Need to run on the main thread because KeyguardSliceView$Row init checks for -// the main thread before acquiring a wake lock. This class is constructed when -// the keyguard_clock_switch layout is inflated. -@RunWithLooper(setAsMainLooper = true) -@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) -public class KeyguardClockSwitchTest extends SysuiTestCase { - @Mock - ViewGroup mMockKeyguardSliceView; - - @Mock - ClockController mClock; - - @Mock - ClockFaceController mSmallClock; - - @Mock - ClockFaceController mLargeClock; - - private FrameLayout mSmallClockFrame; - private FrameLayout mLargeClockFrame; - private KeyguardStatusAreaView mStatusArea; - - KeyguardClockSwitch mKeyguardClockSwitch; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mMockKeyguardSliceView.getContext()).thenReturn(mContext); - when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area)) - .thenReturn(mMockKeyguardSliceView); - - when(mClock.getSmallClock()).thenReturn(mSmallClock); - when(mClock.getLargeClock()).thenReturn(mLargeClock); - - when(mSmallClock.getView()).thenReturn(new TextView(getContext())); - when(mLargeClock.getView()).thenReturn(new TextView(getContext())); - - LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - layoutInflater.setPrivateFactory(new LayoutInflater.Factory2() { - - @Override - public View onCreateView(View parent, String name, Context context, - AttributeSet attrs) { - return onCreateView(name, context, attrs); - } - - @Override - public View onCreateView(String name, Context context, AttributeSet attrs) { - if ("com.android.keyguard.KeyguardSliceView".equals(name)) { - return mMockKeyguardSliceView; - } - return null; - } - }); - mKeyguardClockSwitch = - (KeyguardClockSwitch) layoutInflater.inflate(R.layout.keyguard_clock_switch, null); - mSmallClockFrame = mKeyguardClockSwitch - .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view); - mLargeClockFrame = mKeyguardClockSwitch - .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view_large); - mStatusArea = mKeyguardClockSwitch.findViewById(R.id.keyguard_status_area); - mKeyguardClockSwitch.mChildrenAreLaidOut = true; - } - - @Test - public void noPluginConnected_showNothing() { - mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); - assertEquals(mLargeClockFrame.getChildCount(), 0); - assertEquals(mSmallClockFrame.getChildCount(), 0); - } - - @Test - public void pluginConnectedThenDisconnected_showNothing() { - mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - assertEquals(mLargeClockFrame.getChildCount(), 1); - assertEquals(mSmallClockFrame.getChildCount(), 1); - - mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); - assertEquals(mLargeClockFrame.getChildCount(), 0); - assertEquals(mSmallClockFrame.getChildCount(), 0); - } - - @Test - public void onPluginConnected_showClock() { - mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - - assertEquals(mClock.getSmallClock().getView().getParent(), mSmallClockFrame); - assertEquals(mClock.getLargeClock().getView().getParent(), mLargeClockFrame); - } - - @Test - public void onPluginConnected_showSecondPluginClock() { - // GIVEN a plugin has already connected - ClockController otherClock = mock(ClockController.class); - ClockFaceController smallClock = mock(ClockFaceController.class); - ClockFaceController largeClock = mock(ClockFaceController.class); - when(otherClock.getSmallClock()).thenReturn(smallClock); - when(otherClock.getLargeClock()).thenReturn(largeClock); - when(smallClock.getView()).thenReturn(new TextView(getContext())); - when(largeClock.getView()).thenReturn(new TextView(getContext())); - mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); - - // THEN only the view from the second plugin should be a child of KeyguardClockSwitch. - assertThat(otherClock.getSmallClock().getView().getParent()).isEqualTo(mSmallClockFrame); - assertThat(otherClock.getLargeClock().getView().getParent()).isEqualTo(mLargeClockFrame); - assertThat(mClock.getSmallClock().getView().getParent()).isNull(); - assertThat(mClock.getLargeClock().getView().getParent()).isNull(); - } - - @Test - public void onPluginDisconnected_secondOfTwoDisconnected() { - // GIVEN two plugins are connected - ClockController otherClock = mock(ClockController.class); - ClockFaceController smallClock = mock(ClockFaceController.class); - ClockFaceController largeClock = mock(ClockFaceController.class); - when(otherClock.getSmallClock()).thenReturn(smallClock); - when(otherClock.getLargeClock()).thenReturn(largeClock); - when(smallClock.getView()).thenReturn(new TextView(getContext())); - when(largeClock.getView()).thenReturn(new TextView(getContext())); - mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); - mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - // WHEN the second plugin is disconnected - mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); - // THEN nothing should be shown - assertThat(otherClock.getSmallClock().getView().getParent()).isNull(); - assertThat(otherClock.getLargeClock().getView().getParent()).isNull(); - assertThat(mClock.getSmallClock().getView().getParent()).isNull(); - assertThat(mClock.getLargeClock().getView().getParent()).isNull(); - } - - @Test - public void switchingToBigClockWithAnimation_makesSmallClockDisappear() { - mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true); - - mKeyguardClockSwitch.mClockInAnim.end(); - mKeyguardClockSwitch.mClockOutAnim.end(); - mKeyguardClockSwitch.mStatusAreaAnim.end(); - - assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); - assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); - assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); - assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); - } - - @Test - public void switchingToBigClockNoAnimation_makesSmallClockDisappear() { - mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ false); - - assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); - assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); - assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); - assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); - } - - @Test - public void switchingToSmallClockWithAnimation_makesBigClockDisappear() { - mKeyguardClockSwitch.switchToClock(SMALL, /* animate */ true); - - mKeyguardClockSwitch.mClockInAnim.end(); - mKeyguardClockSwitch.mClockOutAnim.end(); - mKeyguardClockSwitch.mStatusAreaAnim.end(); - - assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1); - assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE); - // only big clock is removed at switch - assertThat(mLargeClockFrame.getParent()).isNull(); - assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); - assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); - } - - @Test - public void switchingToSmallClockNoAnimation_makesBigClockDisappear() { - mKeyguardClockSwitch.switchToClock(SMALL, false); - - assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1); - assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE); - // only big clock is removed at switch - assertThat(mLargeClockFrame.getParent()).isNull(); - assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); - assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); - } - - @Test - public void switchingToSmallClockAnimation_resetsStatusArea() { - mKeyguardClockSwitch.switchToClock(SMALL, true); - - mKeyguardClockSwitch.mClockInAnim.end(); - mKeyguardClockSwitch.mClockOutAnim.end(); - mKeyguardClockSwitch.mStatusAreaAnim.end(); - - assertThat(mStatusArea.getTranslationX()).isEqualTo(0); - assertThat(mStatusArea.getTranslationY()).isEqualTo(0); - assertThat(mStatusArea.getScaleX()).isEqualTo(1); - assertThat(mStatusArea.getScaleY()).isEqualTo(1); - } - - @Test - public void switchingToSmallClockNoAnimation_resetsStatusArea() { - mKeyguardClockSwitch.switchToClock(SMALL, false); - - assertThat(mStatusArea.getTranslationX()).isEqualTo(0); - assertThat(mStatusArea.getTranslationY()).isEqualTo(0); - assertThat(mStatusArea.getScaleX()).isEqualTo(1); - assertThat(mStatusArea.getScaleY()).isEqualTo(1); - } - - - @Test - public void switchingToBigClock_returnsTrueOnlyWhenItWasNotVisibleBefore() { - assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isTrue(); - assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isFalse(); - } -} 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 e5f0d7c6bf37..68f4acde7609 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.flags.andSceneContainer import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -494,6 +495,7 @@ class CommunalSceneStartableTest(flags: FlagsParameterization) : SysuiTestCase() // Start dreaming. updateDreaming(true) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) // Hub times out immediately. assertThat(scene).isEqualTo(CommunalScenes.Blank) @@ -650,6 +652,7 @@ class CommunalSceneStartableTest(flags: FlagsParameterization) : SysuiTestCase() // Start dreaming. updateDreaming(true) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) // Hub times out immediately. assertThat(scene).isEqualTo(Scenes.Dream) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index 21019875f51e..9d5bf4dbdc3f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -55,7 +55,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -887,7 +886,6 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { @Test @EnableSceneContainer - @Ignore("b/378766637") fun lockscreenVisibilityWithScenes() = testScope.runTest { val isDeviceUnlocked by @@ -896,6 +894,7 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { ) assertThat(isDeviceUnlocked).isFalse() + kosmos.setSceneTransition(Idle(Scenes.Lockscreen)) val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 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 9edd62a8a784..6a2aae175d80 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 @@ -16,21 +16,23 @@ package com.android.systemui.media.controls.ui.viewmodel -import android.R import android.content.packageManager import android.content.pm.ApplicationInfo import android.media.MediaMetadata import android.media.session.MediaSession import android.media.session.PlaybackState +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter +import com.android.systemui.media.controls.shared.model.MediaButton 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.res.R import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -132,6 +134,31 @@ class MediaControlViewModelTest : SysuiTestCase() { assertThat(underTest.setPlayer(playerModel!!)).isTrue() } + @Test + fun reservedButtons_showScrubbingTimes() = + testScope.runTest { + val playerModel by collectLastValue(underTest.player) + val mediaData = + initMediaData(ARTIST, TITLE) + .copy(semanticActions = MediaButton(reserveNext = true, reservePrev = true)) + + mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData) + + assertThat(playerModel?.actionButtons).isNotNull() + assertThat(playerModel!!.useSemanticActions).isTrue() + assertThat(playerModel!!.canShowTime).isTrue() + + val buttons = playerModel!!.actionButtons + + val prevButton = buttons.find { it.buttonId == R.id.actionPrev }!! + assertThat(prevButton.notVisibleValue).isEqualTo(ConstraintSet.GONE) + assertThat(prevButton.isVisibleWhenScrubbing).isEqualTo(false) + + val nextButton = buttons.find { it.buttonId == R.id.actionNext }!! + assertThat(nextButton.notVisibleValue).isEqualTo(ConstraintSet.GONE) + assertThat(nextButton.isVisibleWhenScrubbing).isEqualTo(false) + } + private fun initMediaData(artist: String, title: String): MediaData { val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 79bb0c401e78..06dd046564df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -74,6 +74,7 @@ import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.dismissCallbackRegistry +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.dozeInteractor import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -1270,8 +1271,11 @@ class SceneContainerStartableTest : SysuiTestCase() { authenticationMethod = AuthenticationMethodModel.None, isLockscreenEnabled = false, ) - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + powerInteractor.setAsleepForTest() underTest.start() + runCurrent() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + powerInteractor.setAwakeForTest() runCurrent() @@ -2139,6 +2143,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene) val transitionStateFlow = prepareState() underTest.start() + runCurrent() emulateSceneTransition(transitionStateFlow, toScene = Scenes.Bouncer) assertThat(currentScene).isNotEqualTo(Scenes.Lockscreen) @@ -2153,6 +2158,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene) val transitionStateFlow = prepareState() underTest.start() + runCurrent() emulateSceneTransition(transitionStateFlow, toScene = Scenes.Bouncer) assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -2269,6 +2275,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isTrue() underTest.start() + runCurrent() sceneInteractor.changeScene(Scenes.Shade, "") assertThat(currentScene).isEqualTo(Scenes.Shade) assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isTrue() @@ -2350,6 +2357,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene) prepareState() underTest.start() + runCurrent() // run all pending dismiss succeeded/cancelled calls from setup: kosmos.fakeExecutor.runAllReady() @@ -2461,13 +2469,18 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchFromDreamToLockscreen_whenLockedAndDreamStopped() = testScope.runTest { - keyguardInteractor.setDreaming(true) val currentScene by collectLastValue(sceneInteractor.currentScene) prepareState(initialSceneKey = Scenes.Dream) - assertThat(currentScene).isEqualTo(Scenes.Dream) underTest.start() + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) + runCurrent() + keyguardInteractor.setDreaming(true) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Dream) keyguardInteractor.setDreaming(false) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) } @@ -2475,13 +2488,18 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchFromDreamToGone_whenUnlockedAndDreamStopped() = testScope.runTest { - keyguardInteractor.setDreaming(true) val currentScene by collectLastValue(sceneInteractor.currentScene) prepareState(initialSceneKey = Scenes.Dream, isDeviceUnlocked = true) - assertThat(currentScene).isEqualTo(Scenes.Dream) underTest.start() + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) + runCurrent() + keyguardInteractor.setDreaming(true) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Dream) keyguardInteractor.setDreaming(false) + advanceTimeBy(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) } @@ -2684,6 +2702,7 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + runCurrent() sceneInteractor.changeScene(Scenes.Shade, "reason") sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason") assertThat(currentScene).isEqualTo(Scenes.Shade) @@ -2835,8 +2854,15 @@ class SceneContainerStartableTest : SysuiTestCase() { ) sceneInteractor.setTransitionState(transitionStateFlow) initialSceneKey?.let { + if (isDeviceUnlocked && initialSceneKey != Scenes.Gone) { + // Pass through the Gone scene to populate device entry state properly. + transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) + sceneInteractor.changeScene(Scenes.Gone, "prepareState, passing through Gone scene") + runCurrent() + } + transitionStateFlow.value = ObservableTransitionState.Idle(it) - sceneInteractor.changeScene(it, "reason") + sceneInteractor.changeScene(it, "prepareState, initialSceneKey isn't null") } if (startsAwake) { powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 5b0b59de47c2..4a3be4487290 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -31,6 +31,10 @@ import static android.os.UserHandle.USER_ALL; import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -280,8 +284,10 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mBackgroundExecutor.runAllReady(); - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); } @Test @@ -357,7 +363,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN current user's notification is redacted - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); } @Test @@ -368,7 +375,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN current user's notification isn't redacted - assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); } @Test @@ -385,7 +393,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { .setChannel(channel) .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build()); // THEN the notification is redacted - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); } @Test @@ -399,7 +408,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { .setChannel(null) .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build()); // THEN the notification is not redacted - assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); } @Test @@ -410,7 +420,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN work profile notification is redacted - assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mWorkProfileNotif)); assertFalse(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic()); } @@ -422,7 +433,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN work profile notification isn't redacted - assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mWorkProfileNotif)); assertTrue(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic()); } @@ -440,11 +452,14 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN the work profile notification doesn't need to be redacted - assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mWorkProfileNotif)); // THEN the current user and secondary user notifications do need to be redacted - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); } @Test @@ -461,11 +476,14 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // THEN the work profile notification needs to be redacted - assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mWorkProfileNotif)); // THEN the current user and secondary user notifications don't need to be redacted - assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); - assertFalse(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_NONE, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); } @Test @@ -481,18 +499,20 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // THEN the secondary profile notification still needs to be redacted because the current // user's setting takes precedence - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); } @Test public void testHasSensitiveContent_redacted() { // Allow private notifications for this user - mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, + mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mCurrentUser.id); changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); // Sensitive Content notifications are always redacted - assertTrue(mLockscreenUserManager.needsRedaction(mSensitiveContentNotif)); + assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT, + mLockscreenUserManager.getRedactionType(mSensitiveContentNotif)); } @Test @@ -707,9 +727,11 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { new Intent(ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED) .putExtra(EXTRA_KM_PRIVATE_NOTIFS_ALLOWED, true)); - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); // it's a global field, confirm secondary too - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mCurrentUser.id)); assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic( mSecondaryUser.id)); @@ -732,7 +754,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext, new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)); - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mCurrentUserNotif)); verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), anyInt()); } @@ -763,7 +786,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext, new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)); - assertTrue(mLockscreenUserManager.needsRedaction(notifEntry)); + assertEquals(REDACTION_TYPE_PUBLIC, mLockscreenUserManager.getRedactionType(notifEntry)); } @Test @@ -784,7 +807,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)); mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext); - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertEquals(REDACTION_TYPE_PUBLIC, + mLockscreenUserManager.getRedactionType(mSecondaryUserNotif)); verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), anyInt()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index 9e7befd3cf92..8f21ddff524c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -50,6 +50,8 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -840,7 +842,13 @@ class SensitiveContentCoordinatorTest(flags: FlagsParameterization) : SysuiTestC whenever(sbn).thenReturn(mockSbn) whenever(row).thenReturn(mockRow) } - whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) + val redactionType = + if (needsRedaction) { + REDACTION_TYPE_PUBLIC + } else { + REDACTION_TYPE_NONE + } + whenever(lockscreenUserManager.getRedactionType(mockEntry)).thenReturn(redactionType) whenever(mockEntry.rowExists()).thenReturn(true) return object : ListEntry("key", 0) { override fun getRepresentativeEntry(): NotificationEntry = mockEntry diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt index 2f77b33c96cb..3c772fdbe0b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt @@ -27,6 +27,8 @@ import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDIN import com.android.systemui.SysuiTestCase import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection @@ -42,6 +44,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue import org.junit.Before @@ -212,12 +215,12 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry)) .thenReturn(false) val oldAdjustment: NotifUiAdjustment = adjustmentProvider.calculateAdjustment(entry) - assertFalse(oldAdjustment.needsRedaction) + assertEquals(REDACTION_TYPE_NONE, oldAdjustment.redactionType) whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry)) .thenReturn(true) val newAdjustment = adjustmentProvider.calculateAdjustment(entry) - assertTrue(newAdjustment.needsRedaction) + assertEquals(REDACTION_TYPE_PUBLIC, newAdjustment.redactionType) // Then: need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) @@ -229,12 +232,12 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry)) .thenReturn(false) val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) - assertFalse(oldAdjustment.needsRedaction) + assertEquals(REDACTION_TYPE_NONE, oldAdjustment.redactionType) whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry)) .thenReturn(true) val newAdjustment = adjustmentProvider.calculateAdjustment(entry) - assertFalse(newAdjustment.needsRedaction) + assertEquals(REDACTION_TYPE_NONE, newAdjustment.redactionType) // Then: need no re-inflation assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 98bf0e6170d4..739a9c956178 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -45,7 +45,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager -import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java index 79b5cc37119f..0652a835cb7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; @@ -120,6 +121,22 @@ public class SystemUIDialogTest extends SysuiTestCase { } @Test + public void testRegisterReceiverWithoutAcsd() { + SystemUIDialog dialog = createDialogWithDelegate(mContext, mDelegate, + false /* shouldAcsdDismissDialog */); + final ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + final ArgumentCaptor<IntentFilter> intentFilterCaptor = + ArgumentCaptor.forClass(IntentFilter.class); + + dialog.show(); + verify(mBroadcastDispatcher).registerReceiver(broadcastReceiverCaptor.capture(), + intentFilterCaptor.capture(), ArgumentMatchers.eq(null), ArgumentMatchers.any()); + assertTrue(intentFilterCaptor.getValue().hasAction(Intent.ACTION_SCREEN_OFF)); + assertFalse(intentFilterCaptor.getValue().hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + @Test @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS) public void usePredictiveBackAnimFlag() { final SystemUIDialog dialog = new SystemUIDialog(mContext); @@ -163,7 +180,8 @@ public class SystemUIDialogTest extends SysuiTestCase { public void delegateIsCalled_inCorrectOrder() { Configuration configuration = new Configuration(); InOrder inOrder = Mockito.inOrder(mDelegate); - SystemUIDialog dialog = createDialogWithDelegate(); + SystemUIDialog dialog = createDialogWithDelegate(mContext, mDelegate, + true /* shouldAcsdDismissDialog */); dialog.show(); dialog.onWindowFocusChanged(/* hasFocus= */ true); @@ -178,7 +196,8 @@ public class SystemUIDialogTest extends SysuiTestCase { inOrder.verify(mDelegate).onStop(dialog); } - private SystemUIDialog createDialogWithDelegate() { + private SystemUIDialog createDialogWithDelegate(Context context, + SystemUIDialog.Delegate delegate, boolean shouldAcsdDismissDialog) { SystemUIDialog.Factory factory = new SystemUIDialog.Factory( getContext(), Dependency.get(SystemUIDialogManager.class), @@ -186,6 +205,6 @@ public class SystemUIDialogTest extends SysuiTestCase { Dependency.get(BroadcastDispatcher.class), Dependency.get(DialogTransitionAnimator.class) ); - return factory.create(mDelegate); + return factory.create(delegate, context, shouldAcsdDismissDialog); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt index c90183df9847..1cc55bf87b1a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt @@ -127,12 +127,6 @@ class KeyguardBypassInteractorTest : SysuiTestCase() { kosmos.configureKeyguardBypass(isBypassAvailable = skipIsBypassAvailableCheck) underTest = kosmos.keyguardBypassInteractor - // bouncerShowing false, !onLockscreenScene false - // !onLockscreenScene false - setScene( - bouncerShowing = !skipBouncerShowingCheck, - onLockscreenScene = skipOnLockscreenSceneCheck, - ) // alternateBouncerShowing false setAlternateBouncerShowing(!skipAlternateBouncerShowingCheck) // launchingAffordance false @@ -141,6 +135,13 @@ class KeyguardBypassInteractorTest : SysuiTestCase() { setPulseExpanding(!skipPulseExpandingCheck) // qsExpanding false setQsExpanded(!skipQsExpandedCheck) + + // bouncerShowing false, !onLockscreenScene false + // !onLockscreenScene false + setScene( + bouncerShowing = !skipBouncerShowingCheck, + onLockscreenScene = skipOnLockscreenSceneCheck, + ) } private fun setAlternateBouncerShowing(alternateBouncerVisible: Boolean) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index b19645fadbdf..8fb95e843ec1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -40,9 +41,14 @@ import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -51,6 +57,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { private val repository = kosmos.activeNotificationListRepository private val underTest = kosmos.ongoingCallInteractor + @Before + fun setUp() { + underTest.start() + } + @Test fun noNotification_emitsNoCall() = runTest { val state by collectLastValue(underTest.ongoingCallState) @@ -210,8 +221,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_setsRequiresStatusBarVisibleTrue() = kosmos.runTest { - val ongoingCallState by collectLastValue(underTest.ongoingCallState) - + val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay @@ -222,21 +232,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + postOngoingCallNotification() - assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat(isStatusBarRequired).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() } @@ -244,8 +242,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun notificationRemoved_setsRequiresStatusBarVisibleFalse() = kosmos.runTest { - val ongoingCallState by collectLastValue(underTest.ongoingCallState) - + val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay @@ -257,23 +254,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + postOngoingCallNotification() repository.activeNotifications.value = ActiveNotificationsStore() - assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.NoCall::class.java) + assertThat(isStatusBarRequired).isFalse() assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } @@ -295,19 +280,8 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = 1000L, - callType = CallType.Ongoing, - uid = UID, - ) - ) - } - .build() + + postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() @@ -321,6 +295,99 @@ class OngoingCallInteractorTest : SysuiTestCase() { assertThat(requiresStatusBarVisibleInWindowController).isFalse() } + @Test + fun gestureHandler_inCall_notFullscreen_doesNotListen() = + kosmos.runTest { + val ongoingCallState by collectLastValue(underTest.ongoingCallState) + + clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) + // Set up notification but not in fullscreen + kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false + postOngoingCallNotification() + + assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + verify(kosmos.swipeStatusBarAwayGestureHandler, never()) + .addOnGestureDetectedCallback(any(), any()) + } + + @Test + fun gestureHandler_inCall_fullscreen_addsListener() = + kosmos.runTest { + val isGestureListeningEnabled by collectLastValue(underTest.isGestureListeningEnabled) + + // Set up notification and fullscreen mode + kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true + postOngoingCallNotification() + + assertThat(isGestureListeningEnabled).isTrue() + verify(kosmos.swipeStatusBarAwayGestureHandler) + .addOnGestureDetectedCallback(any(), any()) + } + + @Test + fun gestureHandler_inCall_fullscreen_chipSwiped_removesListener() = + kosmos.runTest { + val swipeAwayState by collectLastValue(underTest.isChipSwipedAway) + + // Set up notification and fullscreen mode + kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true + postOngoingCallNotification() + + clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) + + underTest.onStatusBarSwiped() + + assertThat(swipeAwayState).isTrue() + verify(kosmos.swipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any()) + } + + @Test + fun chipSwipedAway_setsRequiresStatusBarVisibleFalse() = + kosmos.runTest { + val isStatusBarRequiredForOngoingCall by + collectLastValue(underTest.isStatusBarRequiredForOngoingCall) + val requiresStatusBarVisibleInRepository by + collectLastValue( + kosmos.fakeStatusBarModeRepository.defaultDisplay + .ongoingProcessRequiresStatusBarVisible + ) + val requiresStatusBarVisibleInWindowController by + collectLastValue( + kosmos.fakeStatusBarWindowControllerStore.defaultDisplay + .ongoingProcessRequiresStatusBarVisible + ) + + // Start with an ongoing call (which should set status bar required) + postOngoingCallNotification() + + assertThat(isStatusBarRequiredForOngoingCall).isTrue() + assertThat(requiresStatusBarVisibleInRepository).isTrue() + assertThat(requiresStatusBarVisibleInWindowController).isTrue() + + // Swipe away the chip + underTest.onStatusBarSwiped() + + // Verify status bar is no longer required + assertThat(requiresStatusBarVisibleInRepository).isFalse() + assertThat(requiresStatusBarVisibleInWindowController).isFalse() + } + + private fun postOngoingCallNotification() { + repository.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif( + activeNotificationModel( + key = "notif1", + whenTime = 1000L, + callType = CallType.Ongoing, + uid = UID, + ) + ) + } + .build() + } + companion object { private const val UID = 885 } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt new file mode 100644 index 000000000000..2ad1124d72d4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt @@ -0,0 +1,111 @@ +/* + * 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.touchpad.tutorial.ui + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted +import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState +import com.android.systemui.touchpad.tutorial.ui.composable.toTutorialActionState +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class StateTransitionsTest : SysuiTestCase() { + + companion object { + private const val START_MARKER = "startMarker" + private const val END_MARKER = "endMarker" + private const val SUCCESS_ANIMATION = 0 + } + + // needed to simulate caching last state as it's used to create new state + private var lastState: TutorialActionState = NotStarted + + private fun GestureState.toTutorialActionState(): TutorialActionState { + val newState = + this.toGestureUiState( + progressStartMarker = START_MARKER, + progressEndMarker = END_MARKER, + successAnimation = SUCCESS_ANIMATION, + ) + .toTutorialActionState(lastState) + lastState = newState + return lastState + } + + @Test + fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() { + val happyPath = + listOf( + GestureState.NotStarted, + GestureState.InProgress(0f), + GestureState.InProgress(0.5f), + GestureState.InProgress(1f), + GestureState.Finished, + ) + + val resultingStates = mutableListOf<TutorialActionState>() + happyPath.forEach { resultingStates.add(it.toTutorialActionState()) } + + assertThat(resultingStates) + .containsExactly( + NotStarted, + InProgress(0f, START_MARKER, END_MARKER), + InProgress(0.5f, START_MARKER, END_MARKER), + InProgress(1f, START_MARKER, END_MARKER), + Finished(SUCCESS_ANIMATION), + ) + .inOrder() + } + + @Test + fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() { + val errorPath = + listOf( + GestureState.NotStarted, + GestureState.InProgress(0f), + GestureState.Error, + GestureState.InProgress(0.5f), + GestureState.InProgress(1f), + GestureState.Finished, + ) + + val resultingStates = mutableListOf<TutorialActionState>() + errorPath.forEach { resultingStates.add(it.toTutorialActionState()) } + + assertThat(resultingStates) + .containsExactly( + NotStarted, + InProgress(0f, START_MARKER, END_MARKER), + Error, + InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)), + InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)), + Finished(SUCCESS_ANIMATION), + ) + .inOrder() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt index 8d0d172ada12..e23348be1590 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.testKosmos +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE @@ -56,9 +57,9 @@ class HomeGestureRecognizerTest : SysuiTestCase() { } @Test - fun doesntTriggerGestureFinished_onGestureSpeedTooSlow() { + fun triggersError_onGestureSpeedTooSlow() { velocityTracker.setVelocity(Velocity(SLOW)) - assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted) + assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Error) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt index 7a77b63a8925..2fe37aef7e0b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.testKosmos +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE @@ -57,9 +58,9 @@ class RecentAppsGestureRecognizerTest : SysuiTestCase() { } @Test - fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() { + fun triggersError_onGestureSpeedTooHigh() { velocityTracker.setVelocity(Velocity(FAST)) - assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted) + assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Error) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt index de410894d0c0..533665eaa626 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted @@ -64,12 +65,12 @@ class ThreeFingerGestureRecognizerTest( } @Test - fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { - assertStateAfterEvents(events = tooShortGesture, expectedState = NotStarted) + fun triggersGestureError_onGestureDistanceTooShort() { + assertStateAfterEvents(events = tooShortGesture, expectedState = Error) } @Test - fun doesntTriggerGestureFinished_onThreeFingersSwipeInOtherDirections() { + fun triggersGestureError_onThreeFingersSwipeInOtherDirections() { val allThreeFingerGestures = listOf( ThreeFingerGesture.swipeUp(), @@ -78,7 +79,7 @@ class ThreeFingerGestureRecognizerTest( ThreeFingerGesture.swipeRight(), ) val invalidGestures = allThreeFingerGestures.filter { it.differentFromAnyOf(validGestures) } - invalidGestures.forEach { assertStateAfterEvents(events = it, expectedState = NotStarted) } + invalidGestures.forEach { assertStateAfterEvents(events = it, expectedState = Error) } } @Test diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f927e26e0cd9..56aaf4c0c564 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3932,6 +3932,8 @@ <string name="touchpad_tutorial_recent_apps_gesture_button">View recent apps</string> <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_done_button">Done</string> + <!-- Screen title after gesture was not done correctly [CHAR LIMIT=NONE] --> + <string name="gesture_error_title">Try again!</string> <!-- BACK GESTURE --> <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_action_title">Go back</string> @@ -3941,6 +3943,8 @@ <string name="touchpad_back_gesture_success_title">Nice!</string> <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string> + <!-- Text shown to the user after back gesture was not done correctly [CHAR LIMIT=NONE] --> + <string name="touchpad_back_gesture_error_body">To go back using your touchpad, swipe left or right using three fingers</string> <!-- HOME GESTURE --> <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_home_gesture_action_title">Go home</string> @@ -3950,6 +3954,8 @@ <string name="touchpad_home_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_home_gesture_success_body">You completed the go home gesture</string> + <!-- Text shown to the user after home gesture was not done correctly [CHAR LIMIT=NONE] --> + <string name="touchpad_home_gesture_error_body">Swipe up with three fingers on your touchpad to go to your home screen</string> <!-- RECENT APPS GESTURE --> <!-- Touchpad recent apps gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_recent_apps_gesture_action_title">View recent apps</string> @@ -3959,6 +3965,8 @@ <string name="touchpad_recent_apps_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete recent apps gesture tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_recent_apps_gesture_success_body">You completed the view recent apps gesture.</string> + <!-- Text shown to the user after recent gesture was not done correctly [CHAR LIMIT=NONE] --> + <string name="touchpad_recent_gesture_error_body">To view recent apps, swipe up and hold using three fingers on your touchpad</string> <!-- KEYBOARD TUTORIAL--> <!-- Action key tutorial title [CHAR LIMIT=NONE] --> @@ -3969,6 +3977,9 @@ <string name="tutorial_action_key_success_title">Well done!</string> <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] --> <string name="tutorial_action_key_success_body">You completed the view all apps gesture</string> + <!-- Text shown to the user after action key tutorial was not done correctly [CHAR LIMIT=NONE] --> + <string name="touchpad_action_key_error_body">Press the action key on your keyboard to view all of your apps</string> + <!-- Content description for the animation playing during the tutorial. The user can click the animation to pause and unpause playback. [CHAR LIMIT=NONE] --> <string name="tutorial_animation_content_description">Tutorial animation, click to pause and resume play.</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index ec0f582dcb47..0e1eccc8231c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -55,7 +55,6 @@ import com.android.systemui.shared.regionsampling.RegionSampler; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.util.ViewController; @@ -85,7 +84,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final DumpManager mDumpManager; private final ClockEventController mClockEventController; private final LogBuffer mLogBuffer; - private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder; private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @@ -147,7 +145,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS ClockRegistry clockRegistry, KeyguardSliceViewController keyguardSliceViewController, LockscreenSmartspaceController smartspaceController, - NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder, KeyguardUnlockAnimationController keyguardUnlockAnimationController, SecureSettings secureSettings, @Main DelayableExecutor uiExecutor, @@ -163,7 +160,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockRegistry = clockRegistry; mKeyguardSliceViewController = keyguardSliceViewController; mSmartspaceController = smartspaceController; - mNicViewBinder = nicViewBinder; mSecureSettings = secureSettings; mUiExecutor = uiExecutor; mBgExecutor = bgExecutor; @@ -277,7 +273,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS hideSliceViewAndNotificationIconContainer(); return; } - updateAodIcons(); mStatusArea = mView.findViewById(R.id.keyguard_status_area); mBgExecutor.execute(() -> { @@ -569,21 +564,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return mLargeClockFrame.getVisibility() != View.VISIBLE; } - private void updateAodIcons() { - if (!MigrateClocksToBlueprint.isEnabled()) { - NotificationIconContainer nic = (NotificationIconContainer) - mView.findViewById( - com.android.systemui.res.R.id.left_aligned_notification_icon_container); - if (mAodIconsBindHandle != null) { - mAodIconsBindHandle.dispose(); - } - if (nic != null) { - mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic); - mAodIconContainer = nic; - } - } - } - private void setClock(ClockController clock) { if (MigrateClocksToBlueprint.isEnabled()) { return; diff --git a/packages/SystemUI/src/com/android/systemui/FontStyles.kt b/packages/SystemUI/src/com/android/systemui/FontStyles.kt new file mode 100644 index 000000000000..d8cd6c87a1ac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/FontStyles.kt @@ -0,0 +1,28 @@ +/* + * 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 + +/** String tokens for the different GSF font families. */ +object FontStyles { + + const val GSF_LABEL_MEDIUM = "gsf-label-medium" + const val GSF_LABEL_LARGE = "gsf-label-large" + + const val GSF_BODY_MEDIUM = "gsf-body-medium" + + const val GSF_TITLE_SMALL_EMPHASIZED = "gsf-title-small-emphasized" +} diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java index 1176cb0523c1..c17055740166 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java @@ -52,6 +52,7 @@ import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; import com.android.systemui.DualToneHandler; +import com.android.systemui.FontStyles; import com.android.systemui.battery.unified.BatteryColors; import com.android.systemui.battery.unified.BatteryDrawableState; import com.android.systemui.battery.unified.BatteryLayersDrawable; @@ -387,7 +388,8 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null); mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight); if (gsfQuickSettings()) { - mBatteryPercentView.setTypeface(Typeface.create("gsf-label-large", Typeface.NORMAL)); + mBatteryPercentView.setTypeface( + Typeface.create(FontStyles.GSF_LABEL_LARGE, Typeface.NORMAL)); } if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor); addView(mBatteryPercentView, new LayoutParams( diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index 5644e6b3b9bf..34679b08cf20 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -183,7 +183,12 @@ constructor( this@CommunalSceneStartable.isDreaming = isDreaming if (scene.isCommunal() && isDreaming && timeoutJob == null) { // If dreaming starts after timeout has expired, ex. if dream restarts under - // the hub, just close the hub immediately. + // the hub, wait for IS_ABLE_TO_DREAM_DELAY_MS and then close the hub. The + // delay is necessary so the KeyguardInteractor.isAbleToDream flow passes + // through that same amount of delay and publishes a new value which is then + // picked up by the HomeSceneFamilyResolver such that the next call to + // SceneInteractor.changeScene(Home) will resolve "Home" to "Dream". + delay(KeyguardInteractor.IS_ABLE_TO_DREAM_DELAY_MS) communalSceneInteractor.changeScene( CommunalScenes.Blank, "dream started after timeout", diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 1fc549469b55..3050cba12f09 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -61,6 +61,7 @@ import com.android.systemui.scene.SceneContainerFrameworkModule; import com.android.systemui.screenshot.ReferenceScreenshotModule; import com.android.systemui.settings.MultiUserUtilsModule; import com.android.systemui.settings.UserTracker; +import com.android.systemui.settings.brightness.dagger.BrightnessSliderModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeModule; import com.android.systemui.startable.Dependencies; @@ -124,6 +125,7 @@ import javax.inject.Named; AccessibilityRepositoryModule.class, AospPolicyModule.class, BatterySaverModule.class, + BrightnessSliderModule.class, CentralSurfacesModule.class, ClipboardOverlayOverrideModule.class, CollapsedStatusBarFragmentStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt index 058e5874ae7c..950a727aedae 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt @@ -79,6 +79,9 @@ private fun buildScreenConfig() = bodyResId = R.string.tutorial_action_key_guidance, titleSuccessResId = R.string.tutorial_action_key_success_title, bodySuccessResId = R.string.tutorial_action_key_success_body, + // error state for action key is not implemented yet so below should never appear + titleErrorResId = R.string.gesture_error_title, + bodyErrorResId = R.string.touchpad_action_key_error_body, ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.action_key_edu), ) diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index 0c1bc835517a..c40adfe6baf8 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -45,18 +45,33 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted sealed interface TutorialActionState { data object NotStarted : TutorialActionState data class InProgress( - val progress: Float = 0f, - val startMarker: String? = null, - val endMarker: String? = null, - ) : TutorialActionState + override val progress: Float = 0f, + override val startMarker: String? = null, + override val endMarker: String? = null, + ) : TutorialActionState, Progress data class Finished(@RawRes val successAnimation: Int) : TutorialActionState + + data object Error : TutorialActionState + + data class InProgressAfterError(val inProgress: InProgress) : + TutorialActionState, Progress by inProgress +} + +interface Progress { + val progress: Float + val startMarker: String? + val endMarker: String? } @Composable @@ -133,10 +148,13 @@ fun TutorialDescription( val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.requestFocus() } val (titleTextId, bodyTextId) = - if (actionState is Finished) { - config.strings.titleSuccessResId to config.strings.bodySuccessResId - } else { - config.strings.titleResId to config.strings.bodyResId + when (actionState) { + is Finished -> config.strings.titleSuccessResId to config.strings.bodySuccessResId + Error, + is InProgressAfterError -> + config.strings.titleErrorResId to config.strings.bodyErrorResId + is NotStarted, + is InProgress -> config.strings.titleResId to config.strings.bodyResId } Column(verticalArrangement = Arrangement.Top, modifier = modifier) { Text( diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt index ad18817704aa..b0816ce608a0 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt @@ -47,8 +47,10 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.LottieDynamicProperties import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted import com.android.systemui.res.R @@ -72,16 +74,18 @@ fun TutorialAnimation( }, ) { state -> when (state) { - NotStarted::class -> + NotStarted::class, + Error::class -> EducationAnimation( config.animations.educationResId, config.colors.animationColors, ) - InProgress::class -> + InProgress::class, + InProgressAfterError::class -> InProgressAnimation( // actionState can be already of different class while this composable is // transitioning to another one - actionState as? InProgress, + actionState as? Progress, config.animations.educationResId, config.colors.animationColors, ) @@ -138,14 +142,14 @@ private fun SuccessAnimation( @Composable private fun InProgressAnimation( - state: InProgress?, + state: Progress?, @RawRes inProgressAnimationId: Int, animationProperties: LottieDynamicProperties, ) { // Caching latest progress for when we're animating this view away and state is null. // Without this there's jumpcut in the animation while it's animating away. // state should never be null when composable appears, only when disappearing - val cached = remember { Ref<InProgress>() } + val cached = remember { Ref<Progress>() } cached.value = state ?: cached.value val progress = cached.value?.progress ?: 0f diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt index 60dfed3a67a4..26259912741a 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt @@ -38,6 +38,8 @@ data class TutorialScreenConfig( @StringRes val bodyResId: Int, @StringRes val titleSuccessResId: Int, @StringRes val bodySuccessResId: Int, + @StringRes val titleErrorResId: Int, + @StringRes val bodyErrorResId: Int, ) data class Animations(@RawRes val educationResId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 26c286df01d7..0d9474e07ce7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -200,7 +200,10 @@ constructor( * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means * that doze mode is not running and DREAMING is ok to commence. * - * Allow a brief moment to prevent rapidly oscillating between true/false signals. + * Allow a brief moment to prevent rapidly oscillating between true/false signals. The amount of + * time is [IS_ABLE_TO_DREAM_DELAY_MS] - consumers should consider waiting for that long before + * examining the value of this flow, to let other consumers have enough time to also see that + * same new value. */ val isAbleToDream: Flow<Boolean> = dozeTransitionModel @@ -212,7 +215,7 @@ constructor( // do not immediately process any dreaming information when exiting AOD. It // should actually be quite strange to leave AOD and then go straight to // DREAMING so this should be fine. - delay(500L) + delay(IS_ABLE_TO_DREAM_DELAY_MS) isDreaming .sample(powerInteractor.isAwake) { isDreaming, isAwake -> isDreaming && isAwake @@ -550,5 +553,11 @@ constructor( companion object { private const val TAG = "KeyguardInteractor" + /** + * Amount of time that [KeyguardInteractor.isAbleToDream] is delayed; consumers of that flow + * should consider waiting this amount of time before check the value of this flow, to let + * other consumers have enough time to see the new value. + */ + const val IS_ABLE_TO_DREAM_DELAY_MS = 500L } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 273d763a8c57..0a958e9d06a4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -27,7 +27,6 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize @@ -73,7 +72,6 @@ object KeyguardClockViewBinder { // When changing to new clock, we need to remove old views from burnInLayer var lastClock: ClockController? = null launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.currentClock.collect { currentClock -> if (lastClock != currentClock) { cleanupClockViews( @@ -99,7 +97,6 @@ object KeyguardClockViewBinder { } launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockSize.collect { clockSize -> updateBurnInLayer(keyguardRootView, viewModel, clockSize) blueprintInteractor.refreshBlueprint(Type.ClockSize) @@ -107,7 +104,6 @@ object KeyguardClockViewBinder { } launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockShouldBeCentered.collect { viewModel.currentClock.value?.let { // TODO(b/301502635): remove "!it.config.useCustomClockScene" when @@ -125,7 +121,6 @@ object KeyguardClockViewBinder { } launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch combine( viewModel.hasAodIcons, rootViewModel.isNotifIconContainerVisible.map { it.value }, @@ -143,7 +138,6 @@ object KeyguardClockViewBinder { } launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch aodBurnInViewModel.movement.collect { burnInModel -> viewModel.currentClock.value ?.largeClock @@ -159,7 +153,6 @@ object KeyguardClockViewBinder { } launch { - if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.largeClockTextSize.collect { fontSizePx -> viewModel.currentClock.value ?.largeClock 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 f121aabe795a..cd4d9dcf366c 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 @@ -31,14 +31,12 @@ import android.view.View.OnLayoutChangeListener import android.view.View.VISIBLE import android.view.ViewGroup import android.view.ViewGroup.OnHierarchyChangeListener -import android.view.ViewPropertyAnimator import android.view.WindowInsets import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD @@ -56,7 +54,6 @@ import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.KeyguardViewMediator -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState @@ -90,12 +87,9 @@ import kotlin.math.min import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import com.android.app.tracing.coroutines.launchTraced as launch /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -161,21 +155,19 @@ object KeyguardRootViewBinder { disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { - if (MigrateClocksToBlueprint.isEnabled) { - launch("$TAG#topClippingBounds") { - val clipBounds = Rect() - viewModel.topClippingBounds.collect { clipTop -> - if (clipTop == null) { - view.setClipBounds(null) - } else { - clipBounds.apply { - top = clipTop - left = view.getLeft() - right = view.getRight() - bottom = view.getBottom() - } - view.setClipBounds(clipBounds) + launch("$TAG#topClippingBounds") { + val clipBounds = Rect() + viewModel.topClippingBounds.collect { clipTop -> + if (clipTop == null) { + view.setClipBounds(null) + } else { + clipBounds.apply { + top = clipTop + left = view.getLeft() + right = view.getRight() + bottom = view.getBottom() } + view.setClipBounds(clipBounds) } } } @@ -190,40 +182,36 @@ object KeyguardRootViewBinder { } } - if (MigrateClocksToBlueprint.isEnabled) { - launch("$TAG#translationY") { - // When translation happens in burnInLayer, it won't be weather clock - // large clock isn't added to burnInLayer due to its scale transition - // so we also need to add translation to it here - // same as translationX - viewModel.translationY.collect { y -> - childViews[burnInLayerId]?.translationY = y - childViews[largeClockId]?.translationY = y - childViews[aodNotificationIconContainerId]?.translationY = y - } + launch("$TAG#translationY") { + // When translation happens in burnInLayer, it won't be weather clock large + // clock isn't added to burnInLayer due to its scale transition so we also + // need to add translation to it here same as translationX + viewModel.translationY.collect { y -> + childViews[burnInLayerId]?.translationY = y + childViews[largeClockId]?.translationY = y + childViews[aodNotificationIconContainerId]?.translationY = y } + } - launch("$TAG#translationX") { - viewModel.translationX.collect { state -> - val px = state.value ?: return@collect - when { - state.isToOrFrom(KeyguardState.AOD) -> { - // Large Clock is not translated in the x direction - childViews[burnInLayerId]?.translationX = px - childViews[aodNotificationIconContainerId]?.translationX = - px - } - state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { - for ((key, childView) in childViews.entries) { - when (key) { - indicationArea, - startButton, - endButton, - deviceEntryIcon -> { - // Do not move these views - } - else -> childView.translationX = px + launch("$TAG#translationX") { + viewModel.translationX.collect { state -> + val px = state.value ?: return@collect + when { + state.isToOrFrom(KeyguardState.AOD) -> { + // Large Clock is not translated in the x direction + childViews[burnInLayerId]?.translationX = px + childViews[aodNotificationIconContainerId]?.translationX = px + } + state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { + for ((key, childView) in childViews.entries) { + when (key) { + indicationArea, + startButton, + endButton, + deviceEntryIcon -> { + // Do not move these views } + else -> childView.translationX = px } } } @@ -263,95 +251,92 @@ object KeyguardRootViewBinder { } } - if (MigrateClocksToBlueprint.isEnabled) { - launch { - viewModel.burnInLayerVisibility.collect { visibility -> - childViews[burnInLayerId]?.visibility = visibility - } + launch { + viewModel.burnInLayerVisibility.collect { visibility -> + childViews[burnInLayerId]?.visibility = visibility } + } - launch { - viewModel.burnInLayerAlpha.collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } + launch { + viewModel.burnInLayerAlpha.collect { alpha -> + childViews[statusViewId]?.alpha = alpha } + } - launch { - viewModel.lockscreenStateAlpha(viewState).collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } + launch { + viewModel.lockscreenStateAlpha(viewState).collect { alpha -> + childViews[statusViewId]?.alpha = alpha } + } - launch { - viewModel.scale.collect { scaleViewModel -> - if (scaleViewModel.scaleClockOnly) { - // For clocks except weather clock, we have scale transition - // besides translate - childViews[largeClockId]?.let { - it.scaleX = scaleViewModel.scale - it.scaleY = scaleViewModel.scale - } + launch { + viewModel.scale.collect { scaleViewModel -> + if (scaleViewModel.scaleClockOnly) { + // For clocks except weather clock, we have scale transition besides + // translate + childViews[largeClockId]?.let { + it.scaleX = scaleViewModel.scale + it.scaleY = scaleViewModel.scale } } } + } - launch { - blueprintViewModel.currentTransition.collect { currentTransition -> - // When blueprint/clock transitions end (null), make sure NSSL is in - // the right place - if (currentTransition == null) { - childViews[nsslPlaceholderId]?.let { notificationListPlaceholder - -> - viewModel.onNotificationContainerBoundsChanged( - notificationListPlaceholder.top.toFloat(), - notificationListPlaceholder.bottom.toFloat(), - animate = true, - ) - } + launch { + blueprintViewModel.currentTransition.collect { currentTransition -> + // When blueprint/clock transitions end (null), make sure NSSL is in the + // right place + if (currentTransition == null) { + childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> + viewModel.onNotificationContainerBoundsChanged( + notificationListPlaceholder.top.toFloat(), + notificationListPlaceholder.bottom.toFloat(), + animate = true, + ) } } } + } - launch { - val iconsAppearTranslationPx = - configuration - .getDimensionPixelSize(R.dimen.shelf_appear_translation) - .stateIn(this) - viewModel.isNotifIconContainerVisible.collect { isVisible -> - if (isVisible.value) { - blueprintViewModel.refreshBlueprint() - } - childViews[aodNotificationIconContainerId] - ?.setAodNotifIconContainerIsVisible( - isVisible, - iconsAppearTranslationPx.value, - screenOffAnimationController, - ) + launch { + val iconsAppearTranslationPx = + configuration + .getDimensionPixelSize(R.dimen.shelf_appear_translation) + .stateIn(this) + viewModel.isNotifIconContainerVisible.collect { isVisible -> + if (isVisible.value) { + blueprintViewModel.refreshBlueprint() } + childViews[aodNotificationIconContainerId] + ?.setAodNotifIconContainerIsVisible( + isVisible, + iconsAppearTranslationPx.value, + screenOffAnimationController, + ) } + } - interactionJankMonitor?.let { jankMonitor -> - launch { - viewModel.goneToAodTransition.collect { - when (it.transitionState) { - TransitionState.STARTED -> { - val clockId = clockInteractor.renderedClockId - val builder = - InteractionJankMonitor.Configuration.Builder - .withView(CUJ_SCREEN_OFF_SHOW_AOD, view) - .setTag(clockId) - jankMonitor.begin(builder) - } - TransitionState.CANCELED -> - jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) - TransitionState.FINISHED -> { - if (MigrateClocksToBlueprint.isEnabled) { - keyguardViewMediator?.maybeHandlePendingLock() - } - jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) - } - TransitionState.RUNNING -> Unit + interactionJankMonitor?.let { jankMonitor -> + launch { + viewModel.goneToAodTransition.collect { + when (it.transitionState) { + TransitionState.STARTED -> { + val clockId = clockInteractor.renderedClockId + val builder = + InteractionJankMonitor.Configuration.Builder.withView( + CUJ_SCREEN_OFF_SHOW_AOD, + view, + ) + .setTag(clockId) + jankMonitor.begin(builder) + } + TransitionState.CANCELED -> + jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) + TransitionState.FINISHED -> { + keyguardViewMediator?.maybeHandlePendingLock() + jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) } + TransitionState.RUNNING -> Unit } } } @@ -406,13 +391,11 @@ object KeyguardRootViewBinder { } } - if (MigrateClocksToBlueprint.isEnabled) { - burnInParams.update { current -> - current.copy( - translationX = { childViews[burnInLayerId]?.translationX }, - translationY = { childViews[burnInLayerId]?.translationY }, - ) - } + burnInParams.update { current -> + current.copy( + translationX = { childViews[burnInLayerId]?.translationX }, + translationY = { childViews[burnInLayerId]?.translationY }, + ) } disposables += @@ -515,20 +498,16 @@ object KeyguardRootViewBinder { burnInParams.update { current -> current.copy( minViewY = - if (MigrateClocksToBlueprint.isEnabled) { - // To ensure burn-in doesn't enroach the top inset, get the min top Y - childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> - min( - currentMin, - if (!isUserVisible(view)) { - Int.MAX_VALUE - } else { - view.getTop() - }, - ) - } - } else { - childViews[statusViewId]?.top ?: 0 + // To ensure burn-in doesn't enroach the top inset, get the min top Y + childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> + min( + currentMin, + if (!isUserVisible(view)) { + Int.MAX_VALUE + } else { + view.getTop() + }, + ) } ) } @@ -542,28 +521,6 @@ object KeyguardRootViewBinder { } } - suspend fun bindAodNotifIconVisibility( - view: View, - isVisible: Flow<AnimatedValue<Boolean>>, - configuration: ConfigurationState, - screenOffAnimationController: ScreenOffAnimationController, - ) { - if (MigrateClocksToBlueprint.isEnabled) { - throw IllegalStateException("should only be called in legacy code paths") - } - coroutineScope { - val iconAppearTranslationPx = - configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this) - isVisible.collect { isVisible -> - view.setAodNotifIconContainerIsVisible( - isVisible = isVisible, - iconsAppearTranslationPx = iconAppearTranslationPx.value, - screenOffAnimationController = screenOffAnimationController, - ) - } - } - } - private fun View.setAodNotifIconContainerIsVisible( isVisible: AnimatedValue<Boolean>, iconsAppearTranslationPx: Int, @@ -578,9 +535,6 @@ object KeyguardRootViewBinder { } when { !isVisible.isAnimating -> { - if (!MigrateClocksToBlueprint.isEnabled) { - translationY = 0f - } visibility = if (isVisible.value) { alpha = 1f @@ -591,7 +545,6 @@ object KeyguardRootViewBinder { } } else -> { - animateInIconTranslation() if (isVisible.value) { CrossFadeHelper.fadeIn(this, animatorListener) } else { @@ -601,19 +554,10 @@ object KeyguardRootViewBinder { } } - private fun View.animateInIconTranslation() { - if (!MigrateClocksToBlueprint.isEnabled) { - animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() - } - } - private fun MotionEvent.isTouchscreenSource(): Boolean { return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true } - private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator = - setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f) - private val statusViewId = R.id.keyguard_status_view private val burnInLayerId = R.id.burn_in_layer private val aodNotificationIconContainerId = R.id.aod_notification_icon_container diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index de4a1b03203c..213083db71c9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.keyguard.MigrateClocksToBlueprint 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 @@ -44,13 +43,12 @@ object KeyguardSmartspaceViewBinder { return keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#clockViewModel.hasCustomWeatherDataDisplay") { - if (!MigrateClocksToBlueprint.isEnabled) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> updateDateWeatherToBurnInLayer( keyguardRootView, clockViewModel, - smartspaceViewModel + smartspaceViewModel, ) blueprintInteractor.refreshBlueprint( Config( @@ -63,7 +61,6 @@ object KeyguardSmartspaceViewBinder { } launch("$TAG#smartspaceViewModel.bcSmartspaceVisibility") { - if (!MigrateClocksToBlueprint.isEnabled) return@launch smartspaceViewModel.bcSmartspaceVisibility.collect { updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) blueprintInteractor.refreshBlueprint( @@ -100,7 +97,7 @@ object KeyguardSmartspaceViewBinder { private fun updateDateWeatherToBurnInLayer( keyguardRootView: ConstraintLayout, clockViewModel: KeyguardClockViewModel, - smartspaceViewModel: KeyguardSmartspaceViewModel + smartspaceViewModel: KeyguardSmartspaceViewModel, ) { if (clockViewModel.hasCustomWeatherDataDisplay.value) { removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel) @@ -112,7 +109,7 @@ object KeyguardSmartspaceViewBinder { private fun addDateWeatherToBurnInLayer( constraintLayout: ConstraintLayout, - smartspaceViewModel: KeyguardSmartspaceViewModel + smartspaceViewModel: KeyguardSmartspaceViewModel, ) { val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer) burnInLayer.apply { @@ -129,7 +126,7 @@ object KeyguardSmartspaceViewBinder { private fun removeDateWeatherFromBurnInLayer( constraintLayout: ConstraintLayout, - smartspaceViewModel: KeyguardSmartspaceViewModel + smartspaceViewModel: KeyguardSmartspaceViewModel, ) { val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer) burnInLayer.apply { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 4c23adfe92e8..6f7872c9cb96 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -30,7 +30,6 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.customization.R as customR -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.res.R @@ -62,9 +61,6 @@ constructor( private lateinit var nic: NotificationIconContainer override fun addViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } nic = NotificationIconContainer(context, null).apply { id = nicId @@ -81,10 +77,6 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - nicBindingDisposable?.dispose() nicBindingDisposable = NotificationIconContainerViewBinder.bindWhileAttached( @@ -98,10 +90,6 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) val isVisible = rootViewModel.isNotifIconContainerVisible.value diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 70ca82492775..dccf61d4e6c7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -1369,8 +1369,9 @@ public class MediaControlPanel { boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing; int notVisibleValue; - if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev()) - || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) { + if (!shouldBeHiddenDueToScrubbing + && ((buttonId == R.id.actionPrev && semanticActions.getReservePrev()) + || (buttonId == R.id.actionNext && semanticActions.getReserveNext()))) { notVisibleValue = ConstraintSet.INVISIBLE; mMediaViewHolder.getAction(buttonId).setFocusable(visible); mMediaViewHolder.getAction(buttonId).setClickable(visible); @@ -1408,7 +1409,9 @@ public class MediaControlPanel { // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views, // so we should only allow scrubbing times to be shown if those action views are present. return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch( - id -> semanticActions.getActionById(id) != null + id -> (semanticActions.getActionById(id) != null + || ((id == R.id.actionPrev && semanticActions.getReservePrev()) + || (id == R.id.actionNext && semanticActions.getReserveNext()))) ); } 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 4e97f2015c12..61e4d95a88e6 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 @@ -316,8 +316,11 @@ class MediaControlViewModel( isVisibleWhenScrubbing = !shouldHideWhenScrubbing, notVisibleValue = if ( - (buttonId == R.id.actionPrev && model.semanticActionButtons!!.reservePrev) || - (buttonId == R.id.actionNext && model.semanticActionButtons!!.reserveNext) + !shouldHideWhenScrubbing && + ((buttonId == R.id.actionPrev && + model.semanticActionButtons!!.reservePrev) || + (buttonId == R.id.actionNext && + model.semanticActionButtons!!.reserveNext)) ) { ConstraintSet.INVISIBLE } else { @@ -382,7 +385,9 @@ class MediaControlViewModel( // so we should only allow scrubbing times to be shown if those action views are present. return semanticActions?.let { SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch { id: Int -> - semanticActions.getActionById(id) != null + semanticActions.getActionById(id) != null || + (id == R.id.actionPrev && semanticActions.reservePrev || + id == R.id.actionNext && semanticActions.reserveNext) } } ?: false } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 1204cde19c76..2a23620839e5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -74,7 +74,7 @@ constructor( context: Context, logger: MediaTttReceiverLogger, viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, - @Main mainExecutor: DelayableExecutor, + @Main private val mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, dumpManager: DumpManager, @@ -285,6 +285,14 @@ constructor( } else { rippleController.collapseRipple(rippleView, onAnimationEnd) animateViewTranslationAndFade(iconContainerView, translationYBy, 0f) + mainExecutor.executeDelayed( + { + if (view.isAttachedToWindow) { + onAnimationEnd.run() + } + }, + ICON_TRANSLATION_ANIM_DURATION, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index b1719107fae1..037a1b2a97f1 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -26,6 +26,7 @@ import static com.android.systemui.classifier.Classifier.BACK_GESTURE; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted; import static java.util.stream.Collectors.joining; @@ -965,11 +966,14 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack return mDesktopModeExcludeRegion.contains(x, y); } - private boolean isWithinTouchRegion(int x, int y) { + private boolean isWithinTouchRegion(MotionEvent ev) { // If the point is inside the PiP or Nav bar overlay excluded bounds, then ignore the back // gesture + int x = (int) ev.getX(); + int y = (int) ev.getY(); final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y); - final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y); + final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y) + && isEdgeResizePermitted(ev); if (isInsidePip || isInDesktopExcludeRegion || mNavBarOverlayExcludedBounds.contains(x, y)) { return false; @@ -1098,8 +1102,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack && isValidTrackpadBackGesture(true /* isTrackpadEvent */); } else { mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets - && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()) - && !isButtonPressFromTrackpad(ev); + && isWithinTouchRegion(ev) && !isButtonPressFromTrackpad(ev); } if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java index dc188c24e02b..e8ee4dd8ebce 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java @@ -26,6 +26,7 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; +import com.android.systemui.FontStyles; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.dagger.QSScope; @@ -68,7 +69,7 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme mBuildText = mView.findViewById(R.id.build); if (gsfQuickSettings()) { - mBuildText.setTypeface(Typeface.create("gsf-body-medium", Typeface.NORMAL)); + mBuildText.setTypeface(Typeface.create(FontStyles.GSF_BODY_MEDIUM, Typeface.NORMAL)); } mPageIndicator = mView.findViewById(R.id.footer_page_indicator); mEditButton = mView.findViewById(android.R.id.edit); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index db778a208b1e..873059ee08db 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -47,6 +47,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.internal.logging.UiEventLogger; import com.android.systemui.FontSizeUtils; +import com.android.systemui.FontStyles; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.qs.QSEditEvent; @@ -314,7 +315,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta v.setMinimumHeight(calculateHeaderMinHeight(context)); if (gsfQuickSettings()) { ((TextView) v.findViewById(android.R.id.title)).setTypeface( - Typeface.create("gsf-label-large", Typeface.NORMAL)); + Typeface.create(FontStyles.GSF_LABEL_LARGE, Typeface.NORMAL)); } return new Holder(v); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index b7ebce247ec9..d401b6ecbfd8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -60,6 +60,7 @@ import com.android.app.tracing.traceSection import com.android.settingslib.Utils import com.android.systemui.Flags import com.android.systemui.FontSizeUtils +import com.android.systemui.FontStyles import com.android.systemui.animation.Expandable import com.android.systemui.animation.LaunchableView import com.android.systemui.animation.LaunchableViewDelegate @@ -312,9 +313,11 @@ constructor( if (Flags.gsfQuickSettings()) { label.apply { - typeface = Typeface.create("gsf-title-small-emphasized", Typeface.NORMAL) + typeface = Typeface.create(FontStyles.GSF_TITLE_SMALL_EMPHASIZED, Typeface.NORMAL) + } + secondaryLabel.apply { + typeface = Typeface.create(FontStyles.GSF_LABEL_MEDIUM, Typeface.NORMAL) } - secondaryLabel.apply { typeface = Typeface.create("gsf-label-medium", Typeface.NORMAL) } } addView(labelContainer) @@ -776,11 +779,15 @@ constructor( lastIconTint = icon.getColor(state) // Long-press effects - longPressEffect?.qsTile?.state?.handlesLongClick = state.handlesLongClick - if ( - state.handlesLongClick && - longPressEffect?.initializeEffect(longPressEffectDuration) == true - ) { + updateLongPressEffect(state.handlesLongClick) + } + + private fun updateLongPressEffect(handlesLongClick: Boolean) { + // The long press effect in the tile can't be updated if it is still running + if (longPressEffect?.state != QSLongPressEffect.State.IDLE) return + + longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick + if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) { showRippleEffect = false longPressEffect.qsTile?.state?.state = lastState // Store the tile's state longPressEffect.resetState() diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt index 2d6181aa04af..1355ba8bdfd4 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt @@ -47,7 +47,11 @@ internal constructor( } override fun createDialog(): SystemUIDialog { - return systemUIDialogFactory.create(this, rearDisplayContext) + return systemUIDialogFactory.create( + this, + rearDisplayContext, + false, /* shouldAcsdDismissDialog */ + ) } override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index d83d74e4e538..0e6fc36fb96a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -230,13 +230,7 @@ constructor( ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene - if ( - !validateSceneChange( - from = currentSceneKey, - to = resolvedScene, - loggingReason = loggingReason, - ) - ) { + if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { return } @@ -268,13 +262,7 @@ constructor( familyResolver.resolvedScene.value } } ?: toScene - if ( - !validateSceneChange( - from = currentSceneKey, - to = resolvedScene, - loggingReason = loggingReason, - ) - ) { + if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) { return } @@ -458,12 +446,11 @@ constructor( * Will throw a runtime exception for illegal states (for example, attempting to change to a * scene that's not part of the current scene framework configuration). * - * @param from The current scene being transitioned away from * @param to The desired destination scene to transition to * @param loggingReason The reason why the transition is requested, for logging purposes * @return `true` if the scene change is valid; `false` if it shouldn't happen */ - private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean { + private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean { if (to !in repository.allContentKeys) { return false } @@ -486,7 +473,7 @@ constructor( " Logging reason for scene change was: $loggingReason" } - return from != to + return true } /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt index b89eb5c762e0..2a0a22f32601 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.combine import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet @@ -34,7 +35,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** @@ -59,6 +59,7 @@ constructor( deviceEntryInteractor.isDeviceEntered, deviceEntryInteractor.isUnlocked, keyguardInteractor.isDreamingWithOverlay, + keyguardInteractor.isAbleToDream, transform = ::homeScene, ) .stateIn( @@ -71,6 +72,7 @@ constructor( isDeviceEntered = deviceEntryInteractor.isDeviceEntered.value, isUnlocked = deviceEntryInteractor.isUnlocked.value, isDreamingWithOverlay = false, + isAbleToDream = false, ), ) @@ -82,10 +84,11 @@ constructor( isDeviceEntered: Boolean, isUnlocked: Boolean, isDreamingWithOverlay: Boolean, + isAbleToDream: Boolean, ): SceneKey = when { // Dream can run even if Keyguard is disabled, thus it has the highest priority here. - isDreamingWithOverlay -> Scenes.Dream + isDreamingWithOverlay && isAbleToDream -> Scenes.Dream !isKeyguardEnabled -> Scenes.Gone canSwipeToEnter == true -> Scenes.Lockscreen !isDeviceEntered -> Scenes.Lockscreen diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 90d27f4b33e9..c65c3b854b82 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -21,7 +21,6 @@ import static com.android.settingslib.display.BrightnessUtils.convertGammaToLine import static com.android.settingslib.display.BrightnessUtils.convertLinearToGammaFloat; import android.animation.ValueAnimator; -import android.annotation.NonNull; import android.content.Context; import android.database.ContentObserver; import android.hardware.display.BrightnessInfo; @@ -42,6 +41,7 @@ import android.service.vr.IVrStateCallbacks; import android.util.Log; import android.util.MathUtils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -72,19 +72,19 @@ import java.util.concurrent.Executor; public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController { private static final String TAG = "CentralSurfaces.BrightnessController"; - private static final int SLIDER_ANIMATION_DURATION = 3000; + protected static final int SLIDER_ANIMATION_DURATION = 3000; - private static final int MSG_UPDATE_SLIDER = 1; - private static final int MSG_ATTACH_LISTENER = 2; - private static final int MSG_DETACH_LISTENER = 3; - private static final int MSG_VR_MODE_CHANGED = 4; + protected static final int MSG_UPDATE_SLIDER = 1; + protected static final int MSG_ATTACH_LISTENER = 2; + protected static final int MSG_DETACH_LISTENER = 3; + protected static final int MSG_VR_MODE_CHANGED = 4; - private static final Uri BRIGHTNESS_MODE_URI = + protected static final Uri BRIGHTNESS_MODE_URI = Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE); private final int mDisplayId; private final Context mContext; - private final ToggleSlider mControl; + protected final ToggleSlider mControl; private final DisplayManager mDisplayManager; private final UserTracker mUserTracker; private final DisplayTracker mDisplayTracker; @@ -109,10 +109,10 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private boolean mTrackingTouch = false; // Brightness adjusted via touch events. private volatile boolean mIsVrModeEnabled; private boolean mListening; - private boolean mExternalChange; + protected boolean mExternalChange; private boolean mControlValueInitialized; - private float mBrightnessMin = PowerManager.BRIGHTNESS_MIN; - private float mBrightnessMax = PowerManager.BRIGHTNESS_MAX; + protected float mBrightnessMin = PowerManager.BRIGHTNESS_MIN; + protected float mBrightnessMax = PowerManager.BRIGHTNESS_MAX; private boolean mIsBrightnessOverriddenByWindow = false; private ValueAnimator mSliderAnimator; @@ -253,10 +253,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig if (info == null) { return; } - mBrightnessMax = info.brightnessMaximum; - mBrightnessMin = info.brightnessMinimum; - mIsBrightnessOverriddenByWindow = info.isBrightnessOverrideByWindow; + updateBrightnessInfo(info); // Value is passed as intbits, since this is what the message takes. final int valueAsIntBits = Float.floatToIntBits(info.brightness); mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits, @@ -264,6 +262,12 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } }; + protected void updateBrightnessInfo(BrightnessInfo info) { + mBrightnessMax = info.brightnessMaximum; + mBrightnessMin = info.brightnessMinimum; + mIsBrightnessOverriddenByWindow = info.isBrightnessOverrideByWindow; + } + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -301,7 +305,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } }; - private final Handler mMainHandler; + protected final Handler mMainHandler; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -459,7 +463,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig return !mAutomatic && !mTrackingTouch; } - private void updateSlider(float brightnessValue, boolean inVrMode) { + protected void updateSlider(float brightnessValue, boolean inVrMode) { final float min = mBrightnessMin; final float max = mBrightnessMax; @@ -502,12 +506,17 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mSliderAnimator.start(); } - + /** Factory interface for creating a {@link BrightnessController}. */ + public interface Factory { + @NonNull + BrightnessController create(ToggleSlider toggleSlider); + } /** Factory for creating a {@link BrightnessController}. */ @AssistedFactory - public interface Factory { + public interface BrightnessControllerFactory extends Factory { /** Create a {@link BrightnessController} */ + @NonNull BrightnessController create(ToggleSlider toggleSlider); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index 503d0bfbc301..02eca74fd751 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -25,6 +25,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.SeekBar; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; @@ -42,6 +43,7 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.util.ViewController; import com.android.systemui.util.time.SystemClock; + import com.google.android.msdl.domain.MSDLPlayer; import javax.inject.Inject; @@ -89,7 +91,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV } }; - BrightnessSliderController( + protected BrightnessSliderController( BrightnessSliderView brightnessSliderView, FalsingManager falsingManager, UiEventLogger uiEventLogger, @@ -247,16 +249,20 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV return mView.isVisibleToUser(); } + protected void handleSliderProgressChange(SeekBar seekBar, int progress, boolean fromUser) { + if (mListener != null) { + mListener.onChanged(mTracking, progress, false); + if (fromUser) { + mBrightnessSliderHapticPlugin.onProgressChanged(progress, true); + } + } + } + private final SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (mListener != null) { - mListener.onChanged(mTracking, progress, false); - if (fromUser) { - mBrightnessSliderHapticPlugin.onProgressChanged(progress, true); - } - } + handleSliderProgressChange(seekBar, progress, fromUser); } @Override @@ -289,11 +295,21 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV } }; + /** Factory interface for creating a {@link BrightnessSliderController}. */ + public interface Factory { + @NonNull + BrightnessSliderController create( + Context context, + @Nullable ViewGroup viewRoot); + + int getLayout(); + } + /** * Creates a {@link BrightnessSliderController} with its associated view. */ - public static class Factory { + public static class BrightnessSliderControllerFactory implements Factory { private final FalsingManager mFalsingManager; private final UiEventLogger mUiEventLogger; private final VibratorHelper mVibratorHelper; @@ -303,7 +319,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final BrightnessWarningToast mBrightnessWarningToast; @Inject - public Factory( + public BrightnessSliderControllerFactory( FalsingManager falsingManager, UiEventLogger uiEventLogger, VibratorHelper vibratorHelper, @@ -328,6 +344,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV * @param viewRoot the {@link ViewGroup} that will contain the hierarchy. The inflated * hierarchy will not be attached */ + @Override + @NonNull public BrightnessSliderController create( Context context, @Nullable ViewGroup viewRoot) { @@ -345,7 +363,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV } /** Get the layout to inflate based on what slider to use */ - private int getLayout() { + public int getLayout() { return R.layout.quick_settings_brightness_dialog; } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java index a39d25a5ef30..550ac62c0bde 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java @@ -43,12 +43,12 @@ import java.util.Collections; public class BrightnessSliderView extends FrameLayout { @NonNull - private ToggleSeekBar mSlider; + protected ToggleSeekBar mSlider; private DispatchTouchEventListener mListener; private Gefingerpoken mOnInterceptListener; @Nullable - private Drawable mProgressDrawable; - private float mScale = 1f; + protected Drawable mProgressDrawable; + protected float mScale = 1f; private final Rect mSystemGestureExclusionRect = new Rect(); public BrightnessSliderView(Context context) { @@ -65,6 +65,10 @@ public class BrightnessSliderView extends FrameLayout { super.onFinishInflate(); setLayerType(LAYER_TYPE_HARDWARE, null); + initBrightnessViewComponents(); + } + + protected void initBrightnessViewComponents() { mSlider = requireViewById(R.id.slider); mSlider.setAccessibilityLabel(getContentDescription().toString()); setBoundaryOffset(); @@ -81,7 +85,7 @@ public class BrightnessSliderView extends FrameLayout { } } - private void setBoundaryOffset() { + protected void setBoundaryOffset() { // BrightnessSliderView uses hardware layer; if the background of its children exceed its // boundary, it'll be cropped. We need to expand its boundary so that the background of // ToggleSeekBar (i.e. the focus state) can be correctly rendered. @@ -131,7 +135,7 @@ public class BrightnessSliderView extends FrameLayout { * @param admin * @see ToggleSeekBar#setEnforcedAdmin */ - void setAdminBlocker(ToggleSeekBar.AdminBlocker blocker) { + protected void setAdminBlocker(ToggleSeekBar.AdminBlocker blocker) { mSlider.setAdminBlocker(blocker); } @@ -211,7 +215,7 @@ public class BrightnessSliderView extends FrameLayout { } } - private void applySliderScale() { + protected void applySliderScale() { if (mProgressDrawable != null) { final Rect r = mProgressDrawable.getBounds(); int height = (int) (mProgressDrawable.getIntrinsicHeight() * mScale); @@ -229,7 +233,7 @@ public class BrightnessSliderView extends FrameLayout { * Interface to attach a listener for {@link View#dispatchTouchEvent}. */ @FunctionalInterface - interface DispatchTouchEventListener { + public interface DispatchTouchEventListener { boolean onDispatchTouchEvent(MotionEvent ev); } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java index c241f2165c8f..a0985fc61b07 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java @@ -85,12 +85,12 @@ public class ToggleSeekBar extends SeekBar { } } - void setAdminBlocker(AdminBlocker blocker) { + public void setAdminBlocker(AdminBlocker blocker) { mAdminBlocker = blocker; setEnabled(blocker == null); } - interface AdminBlocker { + public interface AdminBlocker { boolean block(); } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt new file mode 100644 index 000000000000..fbe442eb4b5e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt @@ -0,0 +1,36 @@ +/* + * 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.settings.brightness.dagger + +import com.android.systemui.settings.brightness.BrightnessController +import com.android.systemui.settings.brightness.BrightnessSliderController +import dagger.Binds +import dagger.Module + +@Module +abstract class BrightnessSliderModule { + + @Binds + abstract fun bindBrightnessSliderControllerFactory( + factory: BrightnessSliderController.BrightnessSliderControllerFactory + ): BrightnessSliderController.Factory + + @Binds + abstract fun bindBrightnessControllerFactory( + factory: BrightnessController.BrightnessControllerFactory + ): BrightnessController.Factory +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java index 71efbab0f738..3180a06ae787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java @@ -14,19 +14,49 @@ package com.android.systemui.statusbar; +import android.annotation.IntDef; import android.content.pm.UserInfo; import android.util.SparseArray; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + public interface NotificationLockscreenUserManager { String PERMISSION_SELF = "com.android.systemui.permission.SELF"; String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action"; + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + prefix = {"REDACTION_TYPE_"}, + value = { + REDACTION_TYPE_NONE, + REDACTION_TYPE_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT}) + @interface RedactionType {} + + /** + * Indicates that a notification requires no redaction + */ + int REDACTION_TYPE_NONE = 0; + + /** + * Indicates that a notification should have all content redacted, showing the public view. + * Overrides all other redaction types. + */ + int REDACTION_TYPE_PUBLIC = 1; + + /** + * Indicates that a notification should have its main content redacted, due to detected + * sensitive content, such as a One-Time Password + */ + int REDACTION_TYPE_SENSITIVE_CONTENT = 1 << 1; + /** * @param userId user Id - * @return true if we re on a secure lock screen + * @return true if we're on a secure lock screen */ boolean isLockscreenPublicMode(int userId); @@ -68,7 +98,13 @@ public interface NotificationLockscreenUserManager { void updatePublicMode(); - boolean needsRedaction(NotificationEntry entry); + /** + * Determine what type of redaction is needed, if any. Returns REDACTION_TYPE_NONE if no + * redaction type is needed, REDACTION_TYPE_PUBLIC if private notifications are blocked, and + * REDACTION_TYPE_SENSITIVE_CONTENT if sensitive content is detected, and REDACTION_TYPE_PUBLIC + * doesn't apply. + */ + @RedactionType int getRedactionType(NotificationEntry entry); /** * Has the given user chosen to allow their private (full) notifications to be shown even diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index a79b78f8d5f3..239257d357f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -654,8 +654,13 @@ public class NotificationLockscreenUserManagerImpl implements } } - /** @return true if the entry needs redaction when on the lockscreen. */ - public boolean needsRedaction(NotificationEntry ent) { + /** + * Determine what type of redaction is needed, if any. Returns REDACTION_TYPE_NONE if no + * redaction type is needed, REDACTION_TYPE_PUBLIC if private notifications are blocked, and + * REDACTION_TYPE_SENSITIVE_CONTENT if sensitive content is detected, and REDACTION_TYPE_PUBLIC + * doesn't apply. + */ + public @RedactionType int getRedactionType(NotificationEntry ent) { int userId = ent.getSbn().getUserId(); boolean isCurrentUserRedactingNotifs = @@ -675,13 +680,19 @@ public class NotificationLockscreenUserManagerImpl implements ent.isNotificationVisibilityPrivate(); boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey()); - if (keyguardPrivateNotifications()) { - return !mKeyguardAllowingNotifications || isNotifSensitive - || userForcesRedaction || (notificationRequestsRedaction && isNotifRedacted); - } else { - return userForcesRedaction || isNotifSensitive - || (notificationRequestsRedaction && isNotifRedacted); + if (userForcesRedaction) { + return REDACTION_TYPE_PUBLIC; + } + if (notificationRequestsRedaction && isNotifRedacted) { + return REDACTION_TYPE_PUBLIC; + } + if (keyguardPrivateNotifications() && !mKeyguardAllowingNotifications) { + return REDACTION_TYPE_PUBLIC; + } + if (isNotifSensitive) { + return REDACTION_TYPE_SENSITIVE_CONTENT; } + return REDACTION_TYPE_NONE; } private boolean packageHasVisibilityOverride(String key) { 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 2588c7ae2363..46c84fbc19aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore @@ -101,6 +102,19 @@ interface StatusBarModule { @Provides @SysUISingleton + @IntoMap + @ClassKey(OngoingCallInteractor::class) + fun ongoingCallInteractor( + interactor: OngoingCallInteractor + ): CoreStartable = + if (StatusBarChipsModernization.isEnabled) { + interactor + } else { + CoreStartable.NOP + } + + @Provides + @SysUISingleton fun lightBarController(store: LightBarControllerStore): LightBarController { return store.defaultDisplay } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index f75163d2662b..2ecce1f02ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -416,7 +416,7 @@ public class PreparationCoordinator implements Coordinator { /* showSnooze = */ adjustment.isSnoozeEnabled(), /* isChildInGroup = */ adjustment.isChildInGroup(), /* isGroupSummary = */ adjustment.isGroupSummary(), - /* needsRedaction = */ adjustment.getNeedsRedaction() + /* needsRedaction = */ adjustment.getRedactionType() ); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index 04458f3a7168..1875e7e46693 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -30,6 +30,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.DynamicPrivacyController import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -211,7 +212,8 @@ constructor( screenshareNotificationHiding() && sensitiveNotificationProtectionController.shouldProtectNotification(entry) - val needsRedaction = lockscreenUserManager.needsRedaction(entry) + val needsRedaction = + lockscreenUserManager.getRedactionType(entry) != REDACTION_TYPE_NONE val isSensitive = userPublic && needsRedaction entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive) if (screenshareNotificationHiding()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index ff72888a5c26..ff9d533a09d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.inflation +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.render.NotifViewController @@ -61,6 +62,6 @@ interface NotifInflater { val showSnooze: Boolean, val isChildInGroup: Boolean = false, val isGroupSummary: Boolean = false, - val needsRedaction: Boolean, + @RedactionType val redactionType: Int, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt index e70fb6b0fdf3..331ef1c01596 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt @@ -20,22 +20,24 @@ import android.app.Notification import android.app.RemoteInput import android.graphics.drawable.Icon import android.text.TextUtils +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation /** * An immutable object which contains minimal state extracted from an entry that represents state - * which can change without a direct app update (e.g. with a ranking update). - * Diffing two entries determines if view re-inflation is needed. + * which can change without a direct app update (e.g. with a ranking update). Diffing two entries + * determines if view re-inflation is needed. */ -class NotifUiAdjustment internal constructor( +class NotifUiAdjustment +internal constructor( val key: String, val smartActions: List<Notification.Action>, val smartReplies: List<CharSequence>, val isConversation: Boolean, val isSnoozeEnabled: Boolean, val isMinimized: Boolean, - val needsRedaction: Boolean, + @RedactionType val redactionType: Int, val isChildInGroup: Boolean, val isGroupSummary: Boolean, ) { @@ -43,65 +45,72 @@ class NotifUiAdjustment internal constructor( @JvmStatic fun needReinflate( oldAdjustment: NotifUiAdjustment, - newAdjustment: NotifUiAdjustment - ): Boolean = when { - oldAdjustment === newAdjustment -> false - oldAdjustment.isConversation != newAdjustment.isConversation -> true - oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true - oldAdjustment.isMinimized != newAdjustment.isMinimized -> true - oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true - areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true - newAdjustment.smartReplies != oldAdjustment.smartReplies -> true - AsyncHybridViewInflation.isEnabled && - !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true - AsyncGroupHeaderViewInflation.isEnabled && - !oldAdjustment.isGroupSummary && newAdjustment.isGroupSummary -> true - else -> false - } + newAdjustment: NotifUiAdjustment, + ): Boolean = + when { + oldAdjustment === newAdjustment -> false + oldAdjustment.isConversation != newAdjustment.isConversation -> true + oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true + oldAdjustment.isMinimized != newAdjustment.isMinimized -> true + oldAdjustment.redactionType != newAdjustment.redactionType -> true + areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true + newAdjustment.smartReplies != oldAdjustment.smartReplies -> true + AsyncHybridViewInflation.isEnabled && + !oldAdjustment.isChildInGroup && + newAdjustment.isChildInGroup -> true + AsyncGroupHeaderViewInflation.isEnabled && + !oldAdjustment.isGroupSummary && + newAdjustment.isGroupSummary -> true + else -> false + } private fun areDifferent( first: List<Notification.Action>, - second: List<Notification.Action> - ): Boolean = when { - first === second -> false - first.size != second.size -> true - else -> first.asSequence().zip(second.asSequence()).any { - (!TextUtils.equals(it.first.title, it.second.title)) || - (areDifferent(it.first.getIcon(), it.second.getIcon())) || - (it.first.actionIntent != it.second.actionIntent) || - (areDifferent(it.first.remoteInputs, it.second.remoteInputs)) + second: List<Notification.Action>, + ): Boolean = + when { + first === second -> false + first.size != second.size -> true + else -> + first.asSequence().zip(second.asSequence()).any { + (!TextUtils.equals(it.first.title, it.second.title)) || + (areDifferent(it.first.getIcon(), it.second.getIcon())) || + (it.first.actionIntent != it.second.actionIntent) || + (areDifferent(it.first.remoteInputs, it.second.remoteInputs)) + } } - } - private fun areDifferent(first: Icon?, second: Icon?): Boolean = when { - first === second -> false - first == null || second == null -> true - else -> !first.sameAs(second) - } + private fun areDifferent(first: Icon?, second: Icon?): Boolean = + when { + first === second -> false + first == null || second == null -> true + else -> !first.sameAs(second) + } - private fun areDifferent( - first: Array<RemoteInput>?, - second: Array<RemoteInput>? - ): Boolean = when { - first === second -> false - first == null || second == null -> true - first.size != second.size -> true - else -> first.asSequence().zip(second.asSequence()).any { - (!TextUtils.equals(it.first.label, it.second.label)) || - (areDifferent(it.first.choices, it.second.choices)) + private fun areDifferent(first: Array<RemoteInput>?, second: Array<RemoteInput>?): Boolean = + when { + first === second -> false + first == null || second == null -> true + first.size != second.size -> true + else -> + first.asSequence().zip(second.asSequence()).any { + (!TextUtils.equals(it.first.label, it.second.label)) || + (areDifferent(it.first.choices, it.second.choices)) + } } - } private fun areDifferent( first: Array<CharSequence>?, - second: Array<CharSequence>? - ): Boolean = when { - first === second -> false - first == null || second == null -> true - first.size != second.size -> true - else -> first.asSequence().zip(second.asSequence()).any { - !TextUtils.equals(it.first, it.second) + second: Array<CharSequence>?, + ): Boolean = + when { + first === second -> false + first == null || second == null -> true + first.size != second.size -> true + else -> + first.asSequence().zip(second.asSequence()).any { + !TextUtils.equals(it.first, it.second) + } } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index 4c82bc193c77..97e55c19d2f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider @@ -79,7 +80,7 @@ constructor( secureSettings.registerContentObserverForUserSync( SHOW_NOTIFICATION_SNOOZE, settingsObserver, - UserHandle.USER_ALL + UserHandle.USER_ALL, ) } dirtyListeners.addIfAbsent(listener) @@ -140,10 +141,15 @@ constructor( isConversation = entry.ranking.isConversation, isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled, isMinimized = isEntryMinimized(entry), - needsRedaction = - lockscreenUserManager.needsRedaction(entry) || - (screenshareNotificationHiding() && - sensitiveNotifProtectionController.shouldProtectNotification(entry)), + redactionType = + if ( + screenshareNotificationHiding() && + sensitiveNotifProtectionController.shouldProtectNotification(entry) + ) { + REDACTION_TYPE_PUBLIC + } else { + lockscreenUserManager.getRedactionType(entry) + }, isChildInGroup = entry.hasEverBeenGroupChild(), isGroupSummary = entry.hasEverBeenGroupSummary(), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index e6d22b07f3ab..80e8f55b897a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.collection.inflation; -import static com.android.server.notification.Flags.screenshareNotificationHiding; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -256,9 +256,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); params.setUseMinimized(isMinimized); - boolean needsRedaction = screenshareNotificationHiding() - ? inflaterParams.getNeedsRedaction() - : mNotificationLockscreenUserManager.needsRedaction(entry); + // TODO b/358403414: use the different types of redaction + boolean needsRedaction = inflaterParams.getRedactionType() != REDACTION_TYPE_NONE; if (needsRedaction) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 6aa8d0ab3d1c..6756077e5444 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -47,6 +47,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; @@ -65,6 +66,11 @@ import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.time.SystemClock; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; +import kotlinx.coroutines.flow.StateFlowKt; + import org.jetbrains.annotations.NotNull; import java.io.PrintWriter; @@ -78,11 +84,6 @@ import java.util.stream.Stream; import javax.inject.Inject; -import kotlinx.coroutines.flow.Flow; -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlow; -import kotlinx.coroutines.flow.StateFlowKt; - /** * A manager which handles heads up notifications which is a special mode where * they simply peek from the top of the screen. @@ -93,15 +94,15 @@ public class HeadsUpManagerImpl private static final String TAG = "BaseHeadsUpManager"; private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; private static final String REASON_REORDER_ALLOWED = "mOnReorderingAllowedListener"; - protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); + private final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); - protected final Context mContext; + private final Context mContext; - protected int mTouchAcceptanceDelay; - protected int mSnoozeLengthMs; - protected boolean mHasPinnedNotification; + private final int mTouchAcceptanceDelay; + private int mSnoozeLengthMs; + private boolean mHasPinnedNotification; private PinnedStatus mPinnedNotificationStatus = PinnedStatus.NotPinned; - protected int mUser; + private int mUser; private final ArrayMap<String, Long> mSnoozedPackages; private final AccessibilityManagerWrapper mAccessibilityMgr; @@ -113,13 +114,14 @@ public class HeadsUpManagerImpl private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - protected final SystemClock mSystemClock; - protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); - protected final HeadsUpManagerLogger mLogger; - protected int mMinimumDisplayTime; - protected int mStickyForSomeTimeAutoDismissTime; - protected int mAutoDismissTime; - protected DelayableExecutor mExecutor; + private final SystemClock mSystemClock; + @VisibleForTesting + final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); + private final HeadsUpManagerLogger mLogger; + private final int mMinimumDisplayTime; + private final int mStickyForSomeTimeAutoDismissTime; + private final int mAutoDismissTime; + private final DelayableExecutor mExecutor; private final int mExtensionTime; @@ -133,7 +135,7 @@ public class HeadsUpManagerImpl private final HashSet<String> mSwipedOutKeys = new HashSet<>(); private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); @VisibleForTesting - public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed + final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed = new ArraySet<>(); private boolean mReleaseOnExpandFinish; @@ -383,9 +385,7 @@ public class HeadsUpManagerImpl HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); mLogger.logUpdateNotificationRequest(key, requestedPinnedStatus, headsUpEntry != null); - Runnable runnable = () -> { - updateNotificationInternal(key, requestedPinnedStatus); - }; + Runnable runnable = () -> updateNotificationInternal(key, requestedPinnedStatus); mAvalancheController.update(headsUpEntry, runnable, "updateNotification"); } @@ -407,9 +407,7 @@ public class HeadsUpManagerImpl headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); PinnedStatus pinnedStatus = getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus); - if (headsUpEntry != null) { - setEntryPinned(headsUpEntry, pinnedStatus, "updateNotificationInternal"); - } + setEntryPinned(headsUpEntry, pinnedStatus, "updateNotificationInternal"); } } @@ -515,8 +513,7 @@ public class HeadsUpManagerImpl } /** - * @param key - * @return When a HUN entry should be removed in milliseconds from now + * @return When a HUN entry with the given key should be removed in milliseconds from now */ @Override public long getEarliestRemovalTime(String key) { @@ -528,7 +525,10 @@ public class HeadsUpManagerImpl } @VisibleForTesting - protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) { + boolean shouldHeadsUpBecomePinned(@Nullable NotificationEntry entry) { + if (entry == null) { + return false; + } boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded; if (SceneContainerFlag.isEnabled()) { pin |= mIsQsExpanded; @@ -549,24 +549,18 @@ public class HeadsUpManagerImpl return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned; } - protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { - if (entry == null) { - return false; - } - if (entry.getSbn() == null) { - return false; - } + private boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { if (entry.getSbn().getNotification() == null) { return false; } return entry.getSbn().getNotification().fullScreenIntent != null; } - protected void setEntryPinned( + private void setEntryPinned( @NonNull HeadsUpManagerImpl.HeadsUpEntry headsUpEntry, PinnedStatus pinnedStatus, String reason) { - mLogger.logSetEntryPinned(headsUpEntry.mEntry, pinnedStatus, reason); - NotificationEntry entry = headsUpEntry.mEntry; + NotificationEntry entry = headsUpEntry.requireEntry(); + mLogger.logSetEntryPinned(entry, pinnedStatus, reason); boolean isPinned = pinnedStatus.isPinned(); if (!isPinned) { headsUpEntry.mWasUnpinned = true; @@ -574,7 +568,7 @@ public class HeadsUpManagerImpl if (headsUpEntry.getPinnedStatus().getValue() != pinnedStatus) { headsUpEntry.setRowPinnedStatus(pinnedStatus); updatePinnedMode(); - if (isPinned && entry.getSbn() != null) { + if (isPinned) { mUiEventLogger.logWithInstanceId( NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(), entry.getSbn().getPackageName(), entry.getSbn().getInstanceId()); @@ -595,8 +589,8 @@ public class HeadsUpManagerImpl * @param headsUpEntry entry added */ @VisibleForTesting - void onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) { - NotificationEntry entry = headsUpEntry.mEntry; + void onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) { + NotificationEntry entry = headsUpEntry.requireEntry(); entry.setHeadsUp(true); PinnedStatus pinnedStatus = getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus); @@ -635,7 +629,7 @@ public class HeadsUpManagerImpl * Remove a notification from the alerting entries. * @param key key of notification to remove */ - protected final void removeEntry(@NonNull String key, String reason) { + private void removeEntry(@NonNull String key, String reason) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); boolean isWaiting; if (headsUpEntry == null) { @@ -652,10 +646,10 @@ public class HeadsUpManagerImpl if (finalHeadsUpEntry == null) { return; } - NotificationEntry entry = finalHeadsUpEntry.mEntry; + NotificationEntry entry = finalHeadsUpEntry.requireEntry(); // If the notification is animating, we will remove it at the end of the animation. - if (entry != null && entry.isExpandAnimationRunning()) { + if (entry.isExpandAnimationRunning()) { return; } entry.demoteStickyHun(); @@ -677,8 +671,9 @@ public class HeadsUpManagerImpl * @param headsUpEntry entry removed * @param reason why onEntryRemoved was called */ - protected void onEntryRemoved(HeadsUpEntry headsUpEntry, String reason) { - NotificationEntry entry = headsUpEntry.mEntry; + @VisibleForTesting + void onEntryRemoved(@NonNull HeadsUpEntry headsUpEntry, String reason) { + NotificationEntry entry = headsUpEntry.requireEntry(); entry.setHeadsUp(false); setEntryPinned(headsUpEntry, PinnedStatus.NotPinned, "onEntryRemoved"); EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); @@ -700,15 +695,14 @@ public class HeadsUpManagerImpl // mEntriesToRemoveWhenReorderingAllowed, we should not remove from this list (and cause // ArrayIndexOutOfBoundsException). We don't need to in this case anyway, because we // clear mEntriesToRemoveWhenReorderingAllowed after removing these entries. - if (!reason.equals(REASON_REORDER_ALLOWED) - && mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)) { + if (!reason.equals(REASON_REORDER_ALLOWED)) { mEntriesToRemoveWhenReorderingAllowed.remove(notifEntry); } } } private void updateTopHeadsUpFlow() { - mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry()); + mTopHeadsUpRow.setValue(getTopHeadsUpEntry()); } private void updateHeadsUpFlow() { @@ -753,18 +747,7 @@ public class HeadsUpManagerImpl } } - /** - * Manager-specific logic, that should occur, when the entry is updated, and its posted time has - * changed. - * - * @param headsUpEntry entry updated - */ - protected void onEntryUpdated(HeadsUpEntry headsUpEntry) { - // no need to update the list here - updateTopHeadsUpFlow(); - } - - protected void updatePinnedMode() { + private void updatePinnedMode() { boolean hasPinnedNotification = hasPinnedNotificationInternal(); mPinnedNotificationStatus = pinnedNotificationStatusInternal(); if (hasPinnedNotification == mHasPinnedNotification) { @@ -806,7 +789,7 @@ public class HeadsUpManagerImpl keySet.addAll(mAvalancheController.getWaitingKeys()); for (String key : keySet) { HeadsUpEntry entry = getHeadsUpEntry(key); - if (entry.mEntry == null) { + if (entry == null || entry.mEntry == null) { continue; } String packageName = entry.mEntry.getSbn().getPackageName(); @@ -828,7 +811,8 @@ public class HeadsUpManagerImpl } @Nullable - protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { + @VisibleForTesting + HeadsUpEntry getHeadsUpEntry(@NonNull String key) { if (mHeadsUpEntryMap.containsKey(key)) { return mHeadsUpEntryMap.get(key); } @@ -845,7 +829,7 @@ public class HeadsUpManagerImpl } @Nullable - protected HeadsUpEntry getTopHeadsUpEntry() { + private HeadsUpEntry getTopHeadsUpEntry() { if (mHeadsUpEntryMap.isEmpty()) { return null; } @@ -941,7 +925,7 @@ public class HeadsUpManagerImpl dumpInternal(pw, args); } - protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) { + private void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) { pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); pw.print(" now="); pw.println(mSystemClock.elapsedRealtime()); @@ -978,7 +962,7 @@ public class HeadsUpManagerImpl private boolean hasPinnedNotificationInternal() { for (String key : mHeadsUpEntryMap.keySet()) { HeadsUpEntry entry = getHeadsUpEntry(key); - if (entry.mEntry != null && entry.mEntry.isRowPinned()) { + if (entry != null && entry.mEntry != null && entry.mEntry.isRowPinned()) { return true; } } @@ -1003,6 +987,10 @@ public class HeadsUpManagerImpl public void unpinAll(boolean userUnPinned) { for (String key : mHeadsUpEntryMap.keySet()) { HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + if (headsUpEntry == null) { + Log.wtf(TAG, "Couldn't find entry " + key + " in unpinAll"); + continue; + } mLogger.logUnpinEntryRequest(key); Runnable runnable = () -> { mLogger.logUnpinEntry(key); @@ -1013,10 +1001,10 @@ public class HeadsUpManagerImpl // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay // on the screen. - if (userUnPinned && headsUpEntry.mEntry != null) { - if (headsUpEntry.mEntry != null && headsUpEntry.mEntry.mustStayOnScreen()) { - headsUpEntry.mEntry.setHeadsUpIsVisible(); - } + if (userUnPinned + && headsUpEntry.mEntry != null + && headsUpEntry.mEntry.mustStayOnScreen()) { + headsUpEntry.mEntry.setHeadsUpIsVisible(); } }; mAvalancheController.delete(headsUpEntry, runnable, "unpinAll"); @@ -1037,7 +1025,7 @@ public class HeadsUpManagerImpl } else { headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); } - onEntryUpdated(headsUpEntry); + updateTopHeadsUpFlow(); } } @@ -1113,7 +1101,7 @@ public class HeadsUpManagerImpl * * @param entry the entry that might be indirectly removed by the user's action * - * @see HeadsUpCoordinator#mActionPressListener + * @see HeadsUpCoordinator.mActionPressListener * @see #canRemoveImmediately(String) */ public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { @@ -1152,8 +1140,8 @@ public class HeadsUpManagerImpl } /** - * @param key - * @return true if the entry is (pinned and expanded) or (has an active remote input) + * @return true if the entry with the given key is (pinned and expanded) or (has an active + * remote input) */ @Override public boolean isSticky(String key) { @@ -1165,7 +1153,8 @@ public class HeadsUpManagerImpl } @NonNull - protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + @VisibleForTesting + HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { if (NotificationThrottleHun.isEnabled()) { return new HeadsUpEntry(entry); } else { @@ -1189,7 +1178,7 @@ public class HeadsUpManagerImpl } @VisibleForTesting - public final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { + final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { if (NotificationThrottleHun.isEnabled()) { mAvalancheController.setEnableAtRuntime(true); if (mEntriesToRemoveWhenReorderingAllowed.isEmpty()) { @@ -1266,14 +1255,15 @@ public class HeadsUpManagerImpl public boolean mRemoteInputActive; public boolean mUserActionMayIndirectlyRemove; - protected boolean mExpanded; - protected boolean mWasUnpinned; + private boolean mExpanded; + @VisibleForTesting + boolean mWasUnpinned; @Nullable public NotificationEntry mEntry; public long mPostTime; public long mEarliestRemovalTime; - @Nullable protected Runnable mRemoveRunnable; + @Nullable private Runnable mRemoveRunnable; @Nullable private Runnable mCancelRemoveRunnable; @@ -1325,7 +1315,7 @@ public class HeadsUpManagerImpl setEntry(entry, createRemoveRunnable(entry)); } - protected void setEntry( + private void setEntry( @NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; @@ -1342,7 +1332,8 @@ public class HeadsUpManagerImpl } } - protected void setRowPinnedStatus(PinnedStatus pinnedStatus) { + @VisibleForTesting + void setRowPinnedStatus(PinnedStatus pinnedStatus) { if (mEntry != null) mEntry.setRowPinnedStatus(pinnedStatus); mPinnedStatus.setValue(pinnedStatus); } @@ -1350,7 +1341,7 @@ public class HeadsUpManagerImpl /** * An interface that returns the amount of time left this HUN should show. */ - interface FinishTimeUpdater { + private interface FinishTimeUpdater { long updateAndGetTimeRemaining(); } @@ -1370,6 +1361,10 @@ public class HeadsUpManagerImpl public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, @Nullable String reason) { Runnable runnable = () -> { + if (mEntry == null) { + Log.wtf(TAG, "#updateEntry called with null mEntry; returning early"); + return; + } mLogger.logUpdateEntry(mEntry, updatePostTime, reason); final long now = mSystemClock.elapsedRealtime(); @@ -1391,23 +1386,18 @@ public class HeadsUpManagerImpl FinishTimeUpdater finishTimeCalculator = () -> { final long finishTime = calculateFinishTime(); final long now = mSystemClock.elapsedRealtime(); - final long timeLeft = NotificationThrottleHun.isEnabled() + return NotificationThrottleHun.isEnabled() ? Math.max(finishTime, mEarliestRemovalTime) - now : Math.max(finishTime - now, mMinimumDisplayTime); - return timeLeft; }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); // Notify the manager, that the posted time has changed. - onEntryUpdated(this); + updateTopHeadsUpFlow(); - if (mEntriesToRemoveAfterExpand.contains(mEntry)) { - mEntriesToRemoveAfterExpand.remove(mEntry); - } + mEntriesToRemoveAfterExpand.remove(mEntry); if (!NotificationThrottleHun.isEnabled()) { - if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { - mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); - } + mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); } } @@ -1528,8 +1518,7 @@ public class HeadsUpManagerImpl @Override public boolean equals(@Nullable Object o) { if (this == o) return true; - if (o == null || !(o instanceof HeadsUpEntry)) return false; - HeadsUpEntry otherHeadsUpEntry = (HeadsUpEntry) o; + if (!(o instanceof HeadsUpEntry otherHeadsUpEntry)) return false; if (mEntry != null && otherHeadsUpEntry.mEntry != null) { return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey()); } @@ -1593,10 +1582,13 @@ public class HeadsUpManagerImpl } } - public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, + private void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, @NonNull String reason) { - - mLogger.logAutoRemoveRequest(this.mEntry, reason); + if (mEntry == null) { + Log.wtf(TAG, "#scheduleAutoRemovalCallback with null mEntry; returning early"); + return; + } + mLogger.logAutoRemoveRequest(mEntry, reason); Runnable runnable = () -> { long delayMs = finishTimeCalculator.updateAndGetTimeRemaining(); @@ -1636,16 +1628,14 @@ public class HeadsUpManagerImpl public void removeAsSoonAsPossible() { if (mRemoveRunnable != null) { - FinishTimeUpdater finishTimeCalculator = () -> { - final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); - return timeLeft; - }; + FinishTimeUpdater finishTimeCalculator = () -> + mEarliestRemovalTime - mSystemClock.elapsedRealtime(); scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible"); } } /** Creates a runnable to remove this notification from the alerting entries. */ - protected Runnable createRemoveRunnable(NotificationEntry entry) { + private Runnable createRemoveRunnable(NotificationEntry entry) { return () -> { if (!NotificationThrottleHun.isEnabled() && !mVisualStabilityProvider.isReorderingAllowed() @@ -1669,7 +1659,7 @@ public class HeadsUpManagerImpl * Calculate what the post time of a notification is at some current time. * @return the post time */ - protected long calculatePostTime() { + private long calculatePostTime() { // The actual post time will be just after the heads-up really slided in return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay; } @@ -1678,7 +1668,7 @@ public class HeadsUpManagerImpl * @return When the notification should auto-dismiss itself, based on * {@link SystemClock#elapsedRealtime()} */ - protected long calculateFinishTime() { + private long calculateFinishTime() { int requestedTimeOutMs; if (isStickyForSomeTime()) { requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime; @@ -1692,9 +1682,8 @@ public class HeadsUpManagerImpl /** * Get user-preferred or default timeout duration. The larger one will be returned. * @return milliseconds before auto-dismiss - * @param requestedTimeout */ - protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { + private int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { return mAccessibilityMgr.getRecommendedTimeoutMillis( requestedTimeout, AccessibilityManager.FLAG_CONTENT_CONTROLS diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt index 1ccc45b9c385..e3ca7c81582f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt @@ -156,12 +156,12 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { ) } - fun logAutoRemoveCanceled(entry: NotificationEntry, reason: String?) { + fun logAutoRemoveCanceled(entry: NotificationEntry?, reason: String?) { buffer.log( TAG, INFO, { - str1 = entry.logKey + str1 = entry?.logKey str2 = reason ?: "unknown" }, { "cancel auto remove of $str1 reason: $str2" }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt index fc432ba973ab..839028e4c3b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt @@ -16,62 +16,9 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder -import androidx.lifecycle.lifecycleScope -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.app.tracing.traceSection -import com.android.systemui.common.ui.ConfigurationState -import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore -import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel -import com.android.systemui.statusbar.phone.NotificationIconContainer -import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.statusbar.ui.SystemBarUtilsState import javax.inject.Inject -import kotlinx.coroutines.DisposableHandle - -/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */ -class NotificationIconContainerAlwaysOnDisplayViewBinder -@Inject -constructor( - private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel, - private val keyguardRootViewModel: KeyguardRootViewModel, - @ShadeDisplayAware private val configuration: ConfigurationState, - private val failureTracker: StatusBarIconViewBindingFailureTracker, - private val screenOffAnimationController: ScreenOffAnimationController, - private val systemBarUtilsState: SystemBarUtilsState, - private val viewStore: AlwaysOnDisplayNotificationIconViewStore, -) { - fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle { - return traceSection("NICAlwaysOnDisplay#bindWhileAttached") { - view.repeatWhenAttached { - lifecycleScope.launch { - launch { - NotificationIconContainerViewBinder.bind( - view = view, - viewModel = viewModel, - configuration = configuration, - systemBarUtilsState = systemBarUtilsState, - failureTracker = failureTracker, - viewStore = viewStore, - ) - } - launch { - KeyguardRootViewBinder.bindAodNotifIconVisibility( - view = view, - isVisible = keyguardRootViewModel.isNotifIconContainerVisible, - configuration = configuration, - screenOffAnimationController = screenOffAnimationController, - ) - } - } - } - } - } -} /** [IconViewStore] for the always-on display. */ class AlwaysOnDisplayNotificationIconViewStore diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 072089981cc7..7c9d850eaf07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1284,7 +1284,11 @@ public class NotificationStackScrollLayout @Override public void setStackCutoff(float stackCutoff) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; - mAmbientState.setStackCutoff(stackCutoff); + if (mAmbientState.getStackCutoff() != stackCutoff) { + mAmbientState.setStackCutoff(stackCutoff); + updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction()); + requestChildrenUpdate(); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index c5bef99f9307..ef68b4de5291 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -109,11 +109,6 @@ constructor( } } launch { - viewModel.shouldResetStackTop - .filter { it } - .collectTraced { view.setStackTop(-(view.getHeadsUpInset().toFloat())) } - } - launch { viewModel.shouldCloseGuts .filter { it } .collectTraced { view.closeGutsOnSceneTouch() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 56b335648138..1bb205cbcb61 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -192,12 +192,6 @@ constructor( /** Whether we should close any open notification guts. */ val shouldCloseGuts: Flow<Boolean> = stackAppearanceInteractor.shouldCloseGuts - val shouldResetStackTop: Flow<Boolean> = - sceneInteractor.transitionState - .mapNotNull { state -> state is Idle && state.currentScene == Scenes.Gone } - .distinctUntilChanged() - .dumpWhileCollecting("shouldResetStackTop") - /** Whether the Notification Stack is visibly on the lockscreen scene. */ val isShowingStackOnLockscreen: Flow<Boolean> = sceneInteractor.transitionState diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt index fe5a02be2fb3..153dd990820d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt @@ -67,6 +67,7 @@ class ComponentSystemUIDialog( broadcastDispatcher, dialogTransitionAnimator, delegate, + true, /* shouldAcsdDismissDialog */ ), LifecycleOwner, SavedStateRegistryOwner, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 12684fa8518a..0fac6448909c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -316,7 +316,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu .isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId()); boolean userPublic = devicePublic || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId()); - boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry); + boolean needsRedaction = mLockscreenUserManager.getRedactionType(entry) + != NotificationLockscreenUserManager.REDACTION_TYPE_NONE; if (userPublic && needsRedaction) { // TODO(b/135046837): we can probably relax this with dynamic privacy return true; @@ -369,7 +370,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu return false; } - if (!mLockscreenUserManager.needsRedaction(entry)) { + if (mLockscreenUserManager.getRedactionType(entry) + == NotificationLockscreenUserManager.REDACTION_TYPE_NONE) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 0ad1042a665f..03324d2a3e6a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -145,7 +145,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh */ public SystemUIDialog create() { return create(new DialogDelegate<>() { - }, mContext, DEFAULT_THEME); + }, mContext, DEFAULT_THEME, true /* shouldAcsdDismissDialog */); } /** @@ -155,7 +155,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh */ public SystemUIDialog create(Context context) { return create(new DialogDelegate<>() { - }, context, DEFAULT_THEME); + }, context, DEFAULT_THEME, true /* shouldAcsdDismissDialog */); } /** @@ -168,8 +168,21 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh return create(delegate, context, DEFAULT_THEME); } + /** + * Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link + * Delegate}. When you need to customize the dialog, pass it a delegate. + * + * This method allows the caller to specify if the dialog should be dismissed in response + * to the ACTION_CLOSE_SYSTEM_DIALOGS intent. + */ + public SystemUIDialog create(Delegate delegate, Context context, + boolean shouldAcsdDismissDialog) { + return create(delegate, context, DEFAULT_THEME, shouldAcsdDismissDialog); + } + public SystemUIDialog create(Delegate delegate, Context context, @StyleRes int theme) { - return create((DialogDelegate<SystemUIDialog>) delegate, context, theme); + return create((DialogDelegate<SystemUIDialog>) delegate, context, theme, + true /* shouldAcsdDismissDialog */); } public SystemUIDialog create(Delegate delegate) { @@ -177,7 +190,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh } private SystemUIDialog create(DialogDelegate<SystemUIDialog> dialogDelegate, - Context context, @StyleRes int theme) { + Context context, @StyleRes int theme, boolean shouldAcsdDismissDialog) { return new SystemUIDialog( context, theme, @@ -186,7 +199,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh mSysUiState, mBroadcastDispatcher, mDialogTransitionAnimator, - dialogDelegate); + dialogDelegate, + shouldAcsdDismissDialog); } } @@ -207,7 +221,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh broadcastDispatcher, dialogTransitionAnimator, new DialogDelegate<>() { - }); + }, + true /* shouldAcsdDismissDialog */); } public SystemUIDialog( @@ -227,7 +242,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh sysUiState, broadcastDispatcher, dialogTransitionAnimator, - (DialogDelegate<SystemUIDialog>) delegate); + (DialogDelegate<SystemUIDialog>) delegate, + true /* shouldAcsdDismissDialog */); } public SystemUIDialog( @@ -238,7 +254,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, DialogTransitionAnimator dialogTransitionAnimator, - DialogDelegate<SystemUIDialog> delegate) { + DialogDelegate<SystemUIDialog> delegate, + boolean shouldAcsdDismissDialog) { super(context, theme); mContext = context; mDelegate = delegate; @@ -249,7 +266,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh getWindow().setAttributes(attrs); mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher, - dialogTransitionAnimator) : null; + dialogTransitionAnimator, shouldAcsdDismissDialog) : null; mDialogManager = dialogManager; mSysUiState = sysUiState; } @@ -523,7 +540,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh // TODO(b/219008720): Remove those calls to Dependency.get. DismissReceiver dismissReceiver = new DismissReceiver(dialog, Dependency.get(BroadcastDispatcher.class), - Dependency.get(DialogTransitionAnimator.class)); + Dependency.get(DialogTransitionAnimator.class), + true /* shouldAcsdDismissDialog */); dialog.setOnDismissListener(d -> { dismissReceiver.unregister(); if (dismissAction != null) dismissAction.run(); @@ -595,12 +613,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh } private static class DismissReceiver extends BroadcastReceiver { - private static final IntentFilter INTENT_FILTER = new IntentFilter(); - - static { - INTENT_FILTER.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF); - } + private final IntentFilter mIntentFilter = new IntentFilter(); private final Dialog mDialog; private boolean mRegistered; @@ -608,14 +621,20 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final DialogTransitionAnimator mDialogTransitionAnimator; DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher, - DialogTransitionAnimator dialogTransitionAnimator) { + DialogTransitionAnimator dialogTransitionAnimator, + boolean shouldAcsdDismissDialog) { mDialog = dialog; mBroadcastDispatcher = broadcastDispatcher; mDialogTransitionAnimator = dialogTransitionAnimator; + + mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF); + if (shouldAcsdDismissDialog) { + mIntentFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + } } void register() { - mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT); + mBroadcastDispatcher.registerReceiver(this, mIntentFilter, null, UserHandle.CURRENT); mRegistered = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt index b7ccfa01c92c..2f7b24393ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor +import androidx.annotation.VisibleForTesting +import com.android.systemui.CoreStartable import com.android.systemui.activity.data.repository.ActivityManagerRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog @@ -31,11 +34,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -55,12 +62,19 @@ class OngoingCallInteractor @Inject constructor( private val activityManagerRepository: ActivityManagerRepository, private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore, private val statusBarWindowControllerStore: StatusBarWindowControllerStore, + private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler, activeNotificationsInteractor: ActiveNotificationsInteractor, @OngoingCallLog private val logBuffer: LogBuffer, -) { +) : CoreStartable { private val logger = Logger(logBuffer, TAG) /** + * Tracks whether the call chip has been swiped away. + */ + private val _isChipSwipedAway = MutableStateFlow(false) + val isChipSwipedAway: StateFlow<Boolean> = _isChipSwipedAway.asStateFlow() + + /** * The current state of ongoing calls. */ val ongoingCallState: StateFlow<OngoingCallModel> = @@ -70,15 +84,29 @@ class OngoingCallInteractor @Inject constructor( notification = notification ) } - .onEach { state -> - setStatusBarRequiredForOngoingCall(state) - } .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), initialValue = OngoingCallModel.NoCall ) + @VisibleForTesting + val isStatusBarRequiredForOngoingCall = combine( + ongoingCallState, + isChipSwipedAway + ) { callState, chipSwipedAway -> + callState is OngoingCallModel.InCall && !chipSwipedAway + } + + @VisibleForTesting + val isGestureListeningEnabled = combine( + ongoingCallState, + statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode, + isChipSwipedAway + ) { callState, isFullscreen, chipSwipedAway -> + callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen + } + private fun createOngoingCallStateFlow( notification: ActiveNotificationModel? ): Flow<OngoingCallModel> { @@ -99,6 +127,31 @@ class OngoingCallInteractor @Inject constructor( } } + override fun start() { + ongoingCallState + .filterIsInstance<OngoingCallModel.NoCall>() + .onEach { + _isChipSwipedAway.value = false + }.launchIn(scope) + + isStatusBarRequiredForOngoingCall.onEach { statusBarRequired -> + setStatusBarRequiredForOngoingCall(statusBarRequired) + }.launchIn(scope) + + isGestureListeningEnabled.onEach { isEnabled -> + updateGestureListening(isEnabled) + }.launchIn(scope) + } + + /** + * Callback that must run when the status bar is swiped while gesture listening is active. + */ + @VisibleForTesting + fun onStatusBarSwiped() { + logger.d("Status bar chip swiped away") + _isChipSwipedAway.value = true + } + private fun deriveOngoingCallState( model: ActiveNotificationModel, isVisible: Boolean @@ -126,8 +179,7 @@ class OngoingCallInteractor @Inject constructor( } } - private fun setStatusBarRequiredForOngoingCall(state: OngoingCallModel) { - val statusBarRequired = state is OngoingCallModel.InCall + private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) { // TODO(b/382808183): Create a single repository that can be utilized in // `statusBarModeRepositoryStore` and `statusBarWindowControllerStore` so we do not need // two separate calls to force the status bar to stay visible. @@ -138,6 +190,16 @@ class OngoingCallInteractor @Inject constructor( .setOngoingProcessRequiresStatusBarVisible(statusBarRequired) } + private fun updateGestureListening(isEnabled: Boolean) { + if (isEnabled) { + swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ -> + onStatusBarSwiped() + } + } else { + swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) + } + } + companion object { private val TAG = "OngoingCall" } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index bfdae626b5e8..e1cc11a7cfd1 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -45,6 +45,8 @@ fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni bodyResId = R.string.touchpad_back_gesture_guidance, titleSuccessResId = R.string.touchpad_back_gesture_success_title, bodySuccessResId = R.string.touchpad_back_gesture_success_body, + titleErrorResId = R.string.gesture_error_title, + bodyErrorResId = R.string.touchpad_back_gesture_error_body, ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu), ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 2332c0051c69..ed84f9c42c3a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -54,6 +54,8 @@ sealed interface GestureUiState { val progressStartMarker: String, val progressEndMarker: String, ) : GestureUiState + + data object Error : GestureUiState } fun GestureState.toGestureUiState( @@ -66,19 +68,31 @@ fun GestureState.toGestureUiState( is GestureState.InProgress -> GestureUiState.InProgress(this.progress, progressStartMarker, progressEndMarker) is GestureState.Finished -> GestureUiState.Finished(successAnimation) + GestureState.Error -> GestureUiState.Error } } -fun GestureUiState.toTutorialActionState(): TutorialActionState { +fun GestureUiState.toTutorialActionState(previousState: TutorialActionState): TutorialActionState { return when (this) { NotStarted -> TutorialActionState.NotStarted - is GestureUiState.InProgress -> - TutorialActionState.InProgress( - progress = progress, - startMarker = progressStartMarker, - endMarker = progressEndMarker, - ) + is GestureUiState.InProgress -> { + val inProgress = + TutorialActionState.InProgress( + progress = progress, + startMarker = progressStartMarker, + endMarker = progressEndMarker, + ) + if ( + previousState is TutorialActionState.InProgressAfterError || + previousState is TutorialActionState.Error + ) { + return TutorialActionState.InProgressAfterError(inProgress) + } else { + return inProgress + } + } is Finished -> TutorialActionState.Finished(successAnimation) + GestureUiState.Error -> TutorialActionState.Error } } @@ -102,11 +116,11 @@ fun GestureTutorialScreen( easterEggTriggered, resetEasterEggFlag = { easterEggTriggered = false }, ) { - ActionTutorialContent( - gestureState.toTutorialActionState(), - onDoneButtonClicked, - screenConfig, - ) + var lastState: TutorialActionState by remember { + mutableStateOf(TutorialActionState.NotStarted) + } + lastState = gestureState.toTutorialActionState(lastState) + ActionTutorialContent(lastState, onDoneButtonClicked, screenConfig) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 45fdd21e795e..26604ca6b845 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -42,6 +42,8 @@ fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni bodyResId = R.string.touchpad_home_gesture_guidance, titleSuccessResId = R.string.touchpad_home_gesture_success_title, bodySuccessResId = R.string.touchpad_home_gesture_success_body, + titleErrorResId = R.string.gesture_error_title, + bodyErrorResId = R.string.touchpad_home_gesture_error_body, ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_home_edu), ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index 680b670f4d97..6400aca57693 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -42,6 +42,8 @@ fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () bodyResId = R.string.touchpad_recent_apps_gesture_guidance, titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title, bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body, + titleErrorResId = R.string.gesture_error_title, + bodyErrorResId = R.string.touchpad_recent_gesture_error_body, ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_recent_apps_edu), diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt index f27ddb515c24..7bc7e81ceb51 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt @@ -23,6 +23,8 @@ sealed interface GestureState { data class InProgress(val progress: Float = 0f, val direction: GestureDirection? = null) : GestureState + + data object Error : GestureState } enum class GestureDirection { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt index 24f5d1f00794..69886e4a1242 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt @@ -28,7 +28,7 @@ inline fun updateGestureState( if (isFinished(gestureState)) { gestureStateChangedCallback(GestureState.Finished) } else { - gestureStateChangedCallback(GestureState.NotStarted) + gestureStateChangedCallback(GestureState.Error) } } is Moving -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt index f5616d45a3da..7fb74b3439bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt @@ -51,11 +51,11 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { private companion object { val TEST_COMPONENT = ComponentName("TestPackageName", "TestClassName") + val TEST_STRUCTURE: CharSequence = "TestStructure" val TEST_CONTROL = mock(Control::class.java, Answers.RETURNS_MOCKS)!!.apply { whenever(structure).thenReturn(TEST_STRUCTURE) } - val TEST_STRUCTURE: CharSequence = "TestStructure" val TEST_APP: CharSequence = "TestApp" private fun View.waitForPost() { @@ -158,7 +158,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { assertThat(getBooleanExtra(ControlsEditingActivity.EXTRA_FROM_FAVORITING, false)) .isTrue() assertThat(getCharSequenceExtra(ControlsEditingActivity.EXTRA_STRUCTURE)) - .isEqualTo("") + .isEqualTo(TEST_STRUCTURE) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt index 68a5d9361046..9543032ef5ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt @@ -247,7 +247,7 @@ public class MediaControlPanelTest : SysuiTestCase() { mContext, 0, intent.setPackage(mContext.packageName), - PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_MUTABLE, ) @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -294,7 +294,7 @@ public class MediaControlPanelTest : SysuiTestCase() { override fun loadAnimator( animId: Int, otionInterpolator: Interpolator, - vararg targets: View + vararg targets: View, ): AnimatorSet { return mockAnimator } @@ -323,7 +323,7 @@ public class MediaControlPanelTest : SysuiTestCase() { packageName = PACKAGE, instanceId = instanceId, recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), - cardAction = smartspaceAction + cardAction = smartspaceAction, ) } @@ -370,7 +370,7 @@ public class MediaControlPanelTest : SysuiTestCase() { packageName = PACKAGE, token = session.sessionToken, device = device, - instanceId = instanceId + instanceId = instanceId, ) } @@ -416,7 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() { action1.id, action2.id, action3.id, - action4.id + action4.id, ) } @@ -536,7 +536,7 @@ public class MediaControlPanelTest : SysuiTestCase() { playOrPause = MediaAction(icon, Runnable {}, "play", bg), nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg) + custom1 = MediaAction(icon, null, "custom 1", bg), ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -590,7 +590,7 @@ public class MediaControlPanelTest : SysuiTestCase() { custom0 = MediaAction(icon, null, "custom 0", bg), custom1 = MediaAction(icon, null, "custom 1", bg), false, - true + true, ) val state = mediaData.copy(semanticActions = semanticActions) @@ -622,7 +622,7 @@ public class MediaControlPanelTest : SysuiTestCase() { custom0 = MediaAction(icon, null, "custom 0", bg), custom1 = MediaAction(icon, null, "custom 1", bg), true, - false + false, ) val state = mediaData.copy(semanticActions = semanticActions) @@ -760,7 +760,7 @@ public class MediaControlPanelTest : SysuiTestCase() { val semanticActions = MediaButton( playOrPause = MediaAction(icon, Runnable {}, "play", null), - nextOrCustom = MediaAction(icon, Runnable {}, "next", null) + nextOrCustom = MediaAction(icon, Runnable {}, "next", null), ) val state = mediaData.copy(semanticActions = semanticActions) @@ -850,7 +850,7 @@ public class MediaControlPanelTest : SysuiTestCase() { val semanticActions = MediaButton( prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) + nextOrCustom = MediaAction(icon, {}, "next", null), ) val state = mediaData.copy(semanticActions = semanticActions) @@ -921,7 +921,7 @@ public class MediaControlPanelTest : SysuiTestCase() { val semanticActions = MediaButton( prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) + nextOrCustom = MediaAction(icon, {}, "next", null), ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -944,7 +944,7 @@ public class MediaControlPanelTest : SysuiTestCase() { val semanticActions = MediaButton( prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) + nextOrCustom = MediaAction(icon, {}, "next", null), ) val state = mediaData.copy(semanticActions = semanticActions) @@ -966,6 +966,29 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun setIsScrubbing_reservedButtonSpaces_scrubbingTimesShown() { + val semanticActions = + MediaButton( + prevOrCustom = null, + nextOrCustom = null, + reserveNext = true, + reservePrev = true, + ) + val state = mediaData.copy(semanticActions = semanticActions) + player.attachPlayer(viewHolder) + player.bindPlayer(state, PACKAGE) + reset(expandedSet) + + getScrubbingChangeListener().onScrubbingChanged(true) + mainExecutor.runAllReady() + + verify(expandedSet).setVisibility(R.id.actionPrev, View.GONE) + verify(expandedSet).setVisibility(R.id.actionNext, View.GONE) + verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.VISIBLE) + verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.VISIBLE) + } + + @Test fun bind_resumeState_withProgress() { val progress = 0.5 val state = mediaData.copy(resumption = true, resumeProgress = progress) @@ -1009,13 +1032,13 @@ public class MediaControlPanelTest : SysuiTestCase() { MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"), MediaNotificationAction(true, actionIntent = null, icon, "next"), MediaNotificationAction(true, actionIntent = null, icon, "custom 0"), - MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1") + MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1"), ) val state = mediaData.copy( actions = actions, actionsToShowInCompact = listOf(1, 2), - semanticActions = null + semanticActions = null, ) player.attachPlayer(viewHolder) @@ -1701,7 +1724,7 @@ public class MediaControlPanelTest : SysuiTestCase() { MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), - MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"), ) val data = mediaData.copy(actions = actions) @@ -1720,7 +1743,7 @@ public class MediaControlPanelTest : SysuiTestCase() { MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), - MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"), ) val data = mediaData.copy(actions = actions) @@ -1739,7 +1762,7 @@ public class MediaControlPanelTest : SysuiTestCase() { MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), - MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"), ) val data = mediaData.copy(actions = actions) @@ -2021,7 +2044,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle(subtitle3) .setIcon(icon) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) player.bindRecommendation(data) @@ -2047,7 +2070,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setIcon( Icon.createWithResource( context, - com.android.settingslib.R.drawable.ic_1x_mobiledata + com.android.settingslib.R.drawable.ic_1x_mobiledata, ) ) .setExtras(Bundle.EMPTY) @@ -2084,7 +2107,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("fake subtitle") .setIcon(icon) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) player.bindRecommendation(data) @@ -2119,7 +2142,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("") .setIcon(icon) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) player.bindRecommendation(data) @@ -2142,7 +2165,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setIcon( Icon.createWithResource( context, - com.android.settingslib.R.drawable.ic_1x_mobiledata + com.android.settingslib.R.drawable.ic_1x_mobiledata, ) ) .setExtras(Bundle.EMPTY) @@ -2157,11 +2180,11 @@ public class MediaControlPanelTest : SysuiTestCase() { .setIcon( Icon.createWithResource( context, - com.android.settingslib.R.drawable.ic_3g_mobiledata + com.android.settingslib.R.drawable.ic_3g_mobiledata, ) ) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2185,7 +2208,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setIcon( Icon.createWithResource( context, - com.android.settingslib.R.drawable.ic_1x_mobiledata + com.android.settingslib.R.drawable.ic_1x_mobiledata, ) ) .setExtras(Bundle.EMPTY) @@ -2200,11 +2223,11 @@ public class MediaControlPanelTest : SysuiTestCase() { .setIcon( Icon.createWithResource( context, - com.android.settingslib.R.drawable.ic_3g_mobiledata + com.android.settingslib.R.drawable.ic_3g_mobiledata, ) ) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2245,7 +2268,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("subtitle1") .setIcon(albumArt) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2268,7 +2291,7 @@ public class MediaControlPanelTest : SysuiTestCase() { Bundle().apply { putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED, ) putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5) } @@ -2290,7 +2313,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("subtitle1") .setIcon(albumArt) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2328,7 +2351,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("subtitle1") .setIcon(albumArt) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2381,7 +2404,7 @@ public class MediaControlPanelTest : SysuiTestCase() { .setSubtitle("subtitle1") .setIcon(albumArt) .setExtras(Bundle.EMPTY) - .build() + .build(), ) ) @@ -2444,7 +2467,7 @@ public class MediaControlPanelTest : SysuiTestCase() { icon = null, action = {}, contentDescription = "play", - background = null + background = null, ) ) val data = mediaData.copy(semanticActions = semanticActions) @@ -2465,7 +2488,7 @@ public class MediaControlPanelTest : SysuiTestCase() { icon = null, action = {}, contentDescription = "play", - background = null + background = null, ) ) val data = mediaData.copy(semanticActions = semanticActions) @@ -2498,7 +2521,7 @@ public class MediaControlPanelTest : SysuiTestCase() { icon = null, action = {}, contentDescription = "play", - background = null + background = null, ) ) val data = mediaData.copy(semanticActions = semanticActions) @@ -2530,8 +2553,8 @@ public class MediaControlPanelTest : SysuiTestCase() { icon = null, action = {}, contentDescription = "custom0", - background = null - ), + background = null, + ) ) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -2553,8 +2576,8 @@ public class MediaControlPanelTest : SysuiTestCase() { icon = null, action = {}, contentDescription = "custom0", - background = null - ), + background = null, + ) ) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt index 90ffaf19be96..67c5986fe5d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt @@ -236,7 +236,7 @@ class QSTileViewImplTest : SysuiTestCase() { context.orCreateTestableResources.addOverride( R.array.tile_states_internet, - arrayOf(unavailableString, offString, onString) + arrayOf(unavailableString, offString, onString), ) // State UNAVAILABLE @@ -341,7 +341,7 @@ class QSTileViewImplTest : SysuiTestCase() { val testA11yLabel = "TEST_LABEL" context.orCreateTestableResources.addOverride( R.string.accessibility_tile_disabled_by_policy_action_description, - testA11yLabel + testA11yLabel, ) val stateDisabledByPolicy = QSTile.State() @@ -374,7 +374,7 @@ class QSTileViewImplTest : SysuiTestCase() { context.orCreateTestableResources.addOverride( R.array.tile_states_internet, - arrayOf(unavailableString, offString, onString) + arrayOf(unavailableString, offString, onString), ) tileView.changeState(state) @@ -477,6 +477,24 @@ class QSTileViewImplTest : SysuiTestCase() { } @Test + fun onStateChange_fromLongPress_toNoLongPress_whileLongPressRuns_doesNotClearResources() { + // GIVEN that the long-press effect has been initialized + val state = QSTile.State() + state.handlesLongClick = true + tileView.changeState(state) + + // WHEN the long-press effect is running + kosmos.qsLongPressEffect.setState(QSLongPressEffect.State.RUNNING_FORWARD) + + // WHEN a state changed happens so that the tile no longer handles long-press + state.handlesLongClick = false + tileView.changeState(state) + + // THEN the long-press effect resources are not cleared + assertThat(tileView.areLongPressEffectPropertiesSet).isTrue() + } + + @Test fun onStateChange_withoutLongPressEffect_fromLongPress_to_noLongPress_neverSetsProperties() { // GIVEN a tile where the long-press effect is null tileView = FakeTileView(context, false, null) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 99467cb11282..a91fb45c9c80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -95,6 +95,7 @@ import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShade import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; +import com.android.systemui.statusbar.notification.headsup.AvalancheController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -103,7 +104,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.notification.headsup.AvalancheController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor; @@ -356,6 +356,31 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test @EnableSceneContainer + public void updateStackCutoff_updatesStackEndHeight() { + // GIVEN shade is fully open + final float stackTop = 200f; + final float stackCutoff = 1000f; + final float stackHeight = stackCutoff - stackTop; + mAmbientState.setStackTop(stackTop); + mAmbientState.setStackCutoff(stackCutoff); + mAmbientState.setStatusBarState(StatusBarState.SHADE); + mStackScroller.setMaxDisplayedNotifications(-1); // no limit on the shade + mStackScroller.setExpandFraction(1f); // shade is fully expanded + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(stackHeight); + assertThat(mAmbientState.getInterpolatedStackHeight()).isEqualTo(stackHeight); + + // WHEN stackCutoff changes + final float newStackCutoff = 800; + mStackScroller.setStackCutoff(newStackCutoff); + + // THEN stackEndHeight is updated + final float newStackHeight = newStackCutoff - stackTop; + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(newStackHeight); + assertThat(mAmbientState.getInterpolatedStackHeight()).isEqualTo(newStackHeight); + } + + @Test + @EnableSceneContainer public void updateStackEndHeightAndStackHeight_maxNotificationsSet_withSceneContainer() { float stackHeight = 300f; when(mStackSizeCalculator.computeHeight(eq(mStackScroller), anyInt(), anyFloat())) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt index 5d146fbffaca..3d45a51b9f3b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt @@ -29,7 +29,7 @@ import com.android.systemui.util.time.systemClock /** This factory creates empty mocks. */ var Kosmos.brightnessSliderControllerFactory by Kosmos.Fixture<BrightnessSliderController.Factory> { - BrightnessSliderController.Factory( + BrightnessSliderController.BrightnessSliderControllerFactory( falsingManager, uiEventLogger, vibratorHelper, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index 6944e6c14096..ab193d294b8c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -261,7 +261,7 @@ class ShadeTestUtilSceneImpl( } private fun setIdleScene(scene: SceneKey) { - sceneInteractor.changeScene(scene, "test") + sceneInteractor.changeScene(scene, "ShadeTestUtil.setIdleScene") val transitionState = MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(scene)) sceneInteractor.setTransitionState(transitionState) @@ -274,7 +274,7 @@ class ShadeTestUtilSceneImpl( progress: Float, isInitiatedByUserInput: Boolean = true, ) { - sceneInteractor.changeScene(from, "test") + sceneInteractor.changeScene(from, "ShadeTestUtil.setTransitionProgress") val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Transition( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt new file mode 100644 index 000000000000..72165c95fc55 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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.gesture + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler by +Kosmos.Fixture { + mock<SwipeStatusBarAwayGestureHandler>() +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt index 9090e02b22b6..40d91017eeef 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore @@ -32,6 +33,7 @@ val Kosmos.ongoingCallInteractor: OngoingCallInteractor by activityManagerRepository = activityManagerRepository, statusBarModeRepositoryStore = fakeStatusBarModeRepository, statusBarWindowControllerStore = fakeStatusBarWindowControllerStore, + swipeStatusBarAwayGestureHandler = swipeStatusBarAwayGestureHandler, logBuffer = logcatLogBuffer("OngoingCallInteractorKosmos"), ) } diff --git a/services/Android.bp b/services/Android.bp index 473911f08cf7..3d512f782232 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -233,8 +233,7 @@ ondeviceintelligence_module_java_defaults { libs: ["service-ondeviceintelligence.stubs.system_server"], }, release_ondevice_intelligence_platform: { - srcs: [":service-ondeviceintelligence-sources"], - static_libs: ["modules-utils-backgroundthread"], + srcs: [":service-ondeviceintelligence-sources-platform"], }, }, } diff --git a/services/autofill/bugfixes.aconfig b/services/autofill/bugfixes.aconfig index 65c446ee6fa8..7c5cfa91ab8a 100644 --- a/services/autofill/bugfixes.aconfig +++ b/services/autofill/bugfixes.aconfig @@ -96,13 +96,3 @@ flag { purpose: PURPOSE_BUGFIX } } - -flag { - name: "add_accessibility_title_for_augmented_autofill_dropdown" - namespace: "autofill" - description: "Add accessibility title for augmented autofill dropdown" - bug: "375284244" - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 36dff89d9d61..778c6864282d 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -228,20 +228,31 @@ public class BinaryTransparencyService extends SystemService { computePackageSignerSha256Digests(packageState.getSigningInfo()); AndroidPackage pkg = packageState.getAndroidPackage(); - for (AndroidPackageSplit split : pkg.getSplits()) { + if(pkg != null) { + for (AndroidPackageSplit split : pkg.getSplits()) { + var appInfo = new IBinaryTransparencyService.AppInfo(); + appInfo.packageName = packageName; + appInfo.longVersion = versionCode; + appInfo.splitName = split.getName(); // base's split name is null + // Signer digests are consistent between splits, guaranteed by Package Manager. + appInfo.signerDigests = signerDigests; + appInfo.mbaStatus = mbaStatus; + + // Only digest and split name are different between splits. + Digest digest = measureApk(split.getPath()); + appInfo.digest = digest.value(); + appInfo.digestAlgorithm = digest.algorithm(); + + results.add(appInfo); + } + } else { + Slog.w(TAG, packageName + " APK file is not physically present," + + " skipping split and digest measurement"); var appInfo = new IBinaryTransparencyService.AppInfo(); appInfo.packageName = packageName; appInfo.longVersion = versionCode; - appInfo.splitName = split.getName(); // base's split name is null - // Signer digests are consistent between splits, guaranteed by Package Manager. appInfo.signerDigests = signerDigests; appInfo.mbaStatus = mbaStatus; - - // Only digest and split name are different between splits. - Digest digest = measureApk(split.getPath()); - appInfo.digest = digest.value(); - appInfo.digestAlgorithm = digest.algorithm(); - results.add(appInfo); } diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index ccc44a41759b..bedc1308e8eb 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -16,9 +16,13 @@ package com.android.server; +import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; + import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -33,6 +37,7 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.TriggerEvent; import android.hardware.TriggerEventListener; +import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -41,10 +46,12 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.util.MutableBoolean; import android.util.Slog; import android.view.KeyEvent; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; @@ -70,7 +77,7 @@ public class GestureLauncherService extends SystemService { * Time in milliseconds in which the power button must be pressed twice so it will be considered * as a camera launch. */ - @VisibleForTesting static final long CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS = 300; + @VisibleForTesting static final long POWER_DOUBLE_TAP_MAX_TIME_MS = 300; /** @@ -100,10 +107,23 @@ public class GestureLauncherService extends SystemService { @VisibleForTesting static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000; - /** - * Number of taps required to launch camera shortcut. - */ - private static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2; + /** Indicates camera should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0; + + /** Indicates wallet should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1; + + /** Number of taps required to launch the double tap shortcut (either camera or wallet). */ + public static final int DOUBLE_POWER_TAP_COUNT_THRESHOLD = 2; + + /** Bundle to send with PendingIntent to grant background activity start privileges. */ + private static final Bundle GRANT_BACKGROUND_START_PRIVILEGES = + ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) + .toBundle(); + + private final QuickAccessWalletClient mQuickAccessWalletClient; /** The listener that receives the gesture event. */ private final GestureEventListener mGestureListener = new GestureEventListener(); @@ -158,6 +178,9 @@ public class GestureLauncherService extends SystemService { */ private boolean mCameraDoubleTapPowerEnabled; + /** Whether wallet double tap power button gesture is currently enabled. */ + private boolean mWalletDoubleTapPowerEnabled; + /** * Whether emergency gesture is currently enabled */ @@ -204,15 +227,17 @@ public class GestureLauncherService extends SystemService { } } public GestureLauncherService(Context context) { - this(context, new MetricsLogger(), new UiEventLoggerImpl()); + this(context, new MetricsLogger(), + QuickAccessWalletClient.create(context), new UiEventLoggerImpl()); } @VisibleForTesting GestureLauncherService(Context context, MetricsLogger metricsLogger, - UiEventLogger uiEventLogger) { + QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) { super(context); mContext = context; mMetricsLogger = metricsLogger; + mQuickAccessWalletClient = quickAccessWalletClient; mUiEventLogger = uiEventLogger; } @@ -237,6 +262,9 @@ public class GestureLauncherService extends SystemService { "GestureLauncherService"); updateCameraRegistered(); updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } updateEmergencyGestureEnabled(); updateEmergencyGesturePowerButtonCooldownPeriodMs(); @@ -250,12 +278,24 @@ public class GestureLauncherService extends SystemService { } private void registerContentObservers() { - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED), - false, mSettingObserver, mUserId); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED), - false, mSettingObserver, mUserId); + if (launchWalletOptionOnPowerDoubleTap()) { + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED), + false, mSettingObserver, mUserId); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE), + false, mSettingObserver, mUserId); + } else { + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED), + false, mSettingObserver, mUserId); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED), + false, mSettingObserver, mUserId); + } mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED), false, mSettingObserver, mUserId); @@ -292,6 +332,14 @@ public class GestureLauncherService extends SystemService { } @VisibleForTesting + void updateWalletDoubleTapPowerEnabled() { + boolean enabled = isWalletDoubleTapPowerSettingEnabled(mContext, mUserId); + synchronized (this) { + mWalletDoubleTapPowerEnabled = enabled; + } + } + + @VisibleForTesting void updateEmergencyGestureEnabled() { boolean enabled = isEmergencyGestureSettingEnabled(mContext, mUserId); synchronized (this) { @@ -418,10 +466,34 @@ public class GestureLauncherService extends SystemService { Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0); } + + /** Checks if camera should be launched on double press of the power button. */ public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) { - return isCameraDoubleTapPowerEnabled(context.getResources()) - && (Settings.Secure.getIntForUser(context.getContentResolver(), - Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + boolean res; + + if (launchWalletOptionOnPowerDoubleTap()) { + res = isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; + } else { + // These are legacy settings that will be deprecated once the option to launch both + // wallet and camera has been created. + res = isCameraDoubleTapPowerEnabled(context.getResources()) + && (Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + } + return res; + } + + /** Checks if wallet should be launched on double tap of the power button. */ + public static boolean isWalletDoubleTapPowerSettingEnabled(Context context, int userId) { + if (!launchWalletOptionOnPowerDoubleTap()) { + return false; + } + + return isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; } public static boolean isCameraLiftTriggerSettingEnabled(Context context, int userId) { @@ -441,6 +513,28 @@ public class GestureLauncherService extends SystemService { isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0; } + private static int getDoubleTapPowerGestureAction(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER, + userId); + } + + /** Whether the shortcut to launch app on power double press is enabled. */ + private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0, + userId) + == 1; + } + + private static boolean isDoubleTapConfigEnabled(Resources resources) { + return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled); + } + /** * Gets power button cooldown period in milliseconds after emergency gesture is triggered. The * value is capped at a maximum @@ -494,12 +588,19 @@ public class GestureLauncherService extends SystemService { * Whether GestureLauncherService should be enabled according to system properties. */ public static boolean isGestureLauncherEnabled(Resources resources) { - return isCameraLaunchEnabled(resources) - || isCameraDoubleTapPowerEnabled(resources) - || isCameraLiftTriggerEnabled(resources) - || isEmergencyGestureEnabled(resources); + boolean res = + isCameraLaunchEnabled(resources) + || isCameraLiftTriggerEnabled(resources) + || isEmergencyGestureEnabled(resources); + if (launchWalletOptionOnPowerDoubleTap()) { + res |= isDoubleTapConfigEnabled(resources); + } else { + res |= isCameraDoubleTapPowerEnabled(resources); + } + return res; } + /** * Attempts to intercept power key down event by detecting certain gesture patterns * @@ -530,6 +631,7 @@ public class GestureLauncherService extends SystemService { return false; } boolean launchCamera = false; + boolean launchWallet = false; boolean launchEmergencyGesture = false; boolean intercept = false; long powerTapInterval; @@ -541,7 +643,7 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps = 1; - } else if (powerTapInterval >= CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS) { + } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { // Tap too slow for shortcuts mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; @@ -586,10 +688,16 @@ public class GestureLauncherService extends SystemService { } } if (mCameraDoubleTapPowerEnabled - && powerTapInterval < CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - && mPowerButtonConsecutiveTaps == CAMERA_POWER_TAP_COUNT_THRESHOLD) { + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { launchCamera = true; intercept = interactive; + } else if (launchWalletOptionOnPowerDoubleTap() + && mWalletDoubleTapPowerEnabled + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { + launchWallet = true; + intercept = interactive; } } if (mPowerButtonConsecutiveTaps > 1 || mPowerButtonSlowConsecutiveTaps > 1) { @@ -608,6 +716,10 @@ public class GestureLauncherService extends SystemService { (int) powerTapInterval); mUiEventLogger.log(GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); } + } else if (launchWallet) { + Slog.i(TAG, "Power button double tap gesture detected, launching wallet. Interval=" + + powerTapInterval + "ms"); + launchWallet = sendGestureTargetActivityPendingIntent(); } else if (launchEmergencyGesture) { Slog.i(TAG, "Emergency gesture detected, launching."); launchEmergencyGesture = handleEmergencyGesture(); @@ -623,11 +735,75 @@ public class GestureLauncherService extends SystemService { mPowerButtonSlowConsecutiveTaps); mMetricsLogger.histogram("power_double_tap_interval", (int) powerTapInterval); - outLaunched.value = launchCamera || launchEmergencyGesture; + outLaunched.value = launchCamera || launchEmergencyGesture || launchWallet; // Intercept power key event if the press is part of a gesture (camera, eGesture) and the // user has completed setup. return intercept && isUserSetupComplete(); } + + /** + * Fetches and sends gestureTargetActivityPendingIntent from QuickAccessWallet, which is a + * specific activity that QuickAccessWalletService has defined to be launch on detection of the + * power button gesture. + */ + private boolean sendGestureTargetActivityPendingIntent() { + boolean userSetupComplete = isUserSetupComplete(); + if (mQuickAccessWalletClient == null + || !mQuickAccessWalletClient.isWalletServiceAvailable()) { + Slog.w(TAG, "QuickAccessWalletService is not available, ignoring wallet gesture."); + return false; + } + + if (!userSetupComplete) { + if (DBG) { + Slog.d(TAG, "userSetupComplete = false, ignoring wallet gesture."); + } + return false; + } + if (DBG) { + Slog.d(TAG, "userSetupComplete = true, performing wallet gesture."); + } + + mQuickAccessWalletClient.getGestureTargetActivityPendingIntent( + getContext().getMainExecutor(), + gesturePendingIntent -> { + if (gesturePendingIntent == null) { + Slog.d(TAG, "getGestureTargetActivityPendingIntent is null."); + sendFallbackPendingIntent(); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(gesturePendingIntent); + }); + return true; + } + + /** + * If gestureTargetActivityPendingIntent is null, this method is invoked to start the activity + * that QuickAccessWalletService has defined to host the Wallet view, which is typically the + * home screen of the Wallet application. + */ + private void sendFallbackPendingIntent() { + mQuickAccessWalletClient.getWalletPendingIntent( + getContext().getMainExecutor(), + walletPendingIntent -> { + if (walletPendingIntent == null) { + Slog.w(TAG, "getWalletPendingIntent returns null. Not launching " + + "anything for wallet."); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(walletPendingIntent); + }); + } + + private void sendPendingIntentWithBackgroundStartPrivileges(PendingIntent pendingIntent) { + try { + pendingIntent.send(GRANT_BACKGROUND_START_PRIVILEGES); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "PendingIntent was canceled", e); + } + } + + /** * @return true if camera was launched, false otherwise. */ @@ -709,6 +885,9 @@ public class GestureLauncherService extends SystemService { registerContentObservers(); updateCameraRegistered(); updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } updateEmergencyGestureEnabled(); updateEmergencyGesturePowerButtonCooldownPeriodMs(); } @@ -720,6 +899,9 @@ public class GestureLauncherService extends SystemService { if (userId == mUserId) { updateCameraRegistered(); updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } updateEmergencyGestureEnabled(); updateEmergencyGesturePowerButtonCooldownPeriodMs(); } diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index 354f281551b2..aa06b7ecf76c 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -316,8 +316,7 @@ class BroadcastController { return null; } if (callerApp.info.uid != SYSTEM_UID - && !callerApp.getPkgList().containsKey(callerPackage) - && !"android".equals(callerPackage)) { + && !callerApp.getPkgList().containsKey(callerPackage)) { throw new SecurityException("Given caller package " + callerPackage + " is not running in process " + callerApp); } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index f42641ece09b..aadf6f61956c 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2840,7 +2840,6 @@ public class OomAdjuster { return true; } } - capability |= PROCESS_CAPABILITY_CPU_TIME; } // Not doing bind OOM management, so treat // this guy more like a started service. @@ -3089,7 +3088,6 @@ public class OomAdjuster { return true; } } - capability |= PROCESS_CAPABILITY_CPU_TIME; } } if (cr.hasFlag(Context.BIND_TREAT_LIKE_ACTIVITY)) { @@ -4243,6 +4241,11 @@ public class OomAdjuster { != client.getSetCapability()) { // The connection might elevate the importance of the service's capabilities. needDryRun = true; + } else if (Flags.useCpuTimeCapability() + && (client.getSetCapability() & ~app.getSetCapability() + & PROCESS_CAPABILITY_CPU_TIME) != 0) { + // The connection might grant PROCESS_CAPABILITY_CPU_TIME to the service. + needDryRun = true; } else if (Flags.unfreezeBindPolicyFix() && cr.hasFlag(Context.BIND_WAIVE_PRIORITY | Context.BIND_ALLOW_OOM_MANAGEMENT)) { @@ -4290,6 +4293,10 @@ public class OomAdjuster { && client.mOptRecord.shouldNotFreeze()) { // Process has shouldNotFreeze and it could have gotten it from the client. return true; + } else if (Flags.useCpuTimeCapability() + && (client.getSetCapability() & app.getSetCapability() + & PROCESS_CAPABILITY_CPU_TIME) != 0) { + return true; } return false; } @@ -4309,6 +4316,11 @@ public class OomAdjuster { && client.mOptRecord.shouldNotFreeze() && !app.mOptRecord.shouldNotFreeze()) { needDryRun = true; + } else if (Flags.useCpuTimeCapability() + && (client.getSetCapability() & ~app.getSetCapability() + & PROCESS_CAPABILITY_CPU_TIME) != 0) { + // The connection might grant PROCESS_CAPABILITY_CPU_TIME to the provider. + needDryRun = true; } if (needDryRun) { @@ -4335,6 +4347,10 @@ public class OomAdjuster { && client.mOptRecord.shouldNotFreeze()) { // Process has shouldNotFreeze and it could have gotten it from the client. return true; + } else if (Flags.useCpuTimeCapability() + && (client.getSetCapability() & app.getSetCapability() + & PROCESS_CAPABILITY_CPU_TIME) != 0) { + return true; } return false; diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 83b0801ce87f..50d650855b05 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -37,7 +37,6 @@ class BrightnessRangeController { private final HdrClamper mHdrClamper; private final Runnable mModeChangeCallback; - private final boolean mUseNbmController; private final boolean mUseHdrClamper; @@ -62,11 +61,8 @@ class BrightnessRangeController { mHdrClamper = hdrClamper; mNormalBrightnessModeController = normalBrightnessModeController; mUseHdrClamper = flags.isHdrClamperEnabled() && !flags.useNewHdrBrightnessModifier(); - mUseNbmController = flags.isNbmControllerEnabled(); - if (mUseNbmController) { - mNormalBrightnessModeController.resetNbmData( - displayDeviceConfig.getLuxThrottlingData()); - } + mNormalBrightnessModeController.resetNbmData( + displayDeviceConfig.getLuxThrottlingData()); if (flags.useNewHdrBrightnessModifier()) { // HDR boost is handled by HdrBrightnessModifier and should be disabled in HbmController mHbmController.disableHdrBoost(); @@ -76,7 +72,6 @@ class BrightnessRangeController { void dump(PrintWriter pw) { pw.println("BrightnessRangeController:"); - pw.println(" mUseNormalBrightnessController=" + mUseNbmController); pw.println(" mUseHdrClamper=" + mUseHdrClamper); mHbmController.dump(pw); mNormalBrightnessModeController.dump(pw); @@ -138,9 +133,7 @@ class BrightnessRangeController { float getCurrentBrightnessMax() { // nbmController might adjust maxBrightness only if device does not support HBM or // hbm is currently not allowed - if (mUseNbmController - && (!mHbmController.deviceSupportsHbm() - || !mHbmController.isHbmCurrentlyAllowed())) { + if (!mHbmController.deviceSupportsHbm() || !mHbmController.isHbmCurrentlyAllowed()) { return Math.min(mHbmController.getCurrentBrightnessMax(), mNormalBrightnessModeController.getCurrentBrightnessMax()); } @@ -173,16 +166,12 @@ class BrightnessRangeController { } private void applyChanges(BooleanSupplier nbmChangesFunc, Runnable hbmChangesFunc) { - if (mUseNbmController) { - boolean nbmTransitionChanged = nbmChangesFunc.getAsBoolean(); - hbmChangesFunc.run(); - // if nbm transition changed - trigger callback - // HighBrightnessModeController handles sending changes itself - if (nbmTransitionChanged) { - mModeChangeCallback.run(); - } - } else { - hbmChangesFunc.run(); + boolean nbmTransitionChanged = nbmChangesFunc.getAsBoolean(); + hbmChangesFunc.run(); + // if nbm transition changed - trigger callback + // HighBrightnessModeController handles sending changes itself + if (nbmTransitionChanged) { + mModeChangeCallback.run(); } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index f48fbea64f65..9387e9ede532 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1646,6 +1646,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY) || mAutomaticBrightnessStrategy .isTemporaryAutoBrightnessAdjustmentApplied(); + float rampSpeed = 0; if (!mPendingScreenOff) { if (mSkipScreenOnBrightnessRamp) { if (state == Display.STATE_ON) { @@ -1747,7 +1748,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call customAnimationRate, /* ignoreAnimationLimits = */true); } else { boolean isIncreasing = animateValue > currentBrightness; - final float rampSpeed; final boolean idle = mAutomaticBrightnessController != null && mAutomaticBrightnessController.isInIdleMode(); if (isIncreasing && slowChange) { @@ -1832,6 +1832,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call .getDisplayBrightnessStrategyName()); mTempBrightnessEvent.setAutomaticBrightnessEnabled( displayBrightnessState.getShouldUseAutoBrightness()); + mTempBrightnessEvent.setSlowChange(slowChange); + mTempBrightnessEvent.setRampSpeed(rampSpeed); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index 9e9b899ffa7d..159c30ddf77f 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -78,6 +78,8 @@ public final class BrightnessEvent { private String mDisplayBrightnessStrategyName; @AutomaticBrightnessController.AutomaticBrightnessMode private int mAutoBrightnessMode; + private boolean mSlowChange; + private float mRampSpeed; public BrightnessEvent(BrightnessEvent that) { copyFrom(that); @@ -126,6 +128,8 @@ public final class BrightnessEvent { mAutomaticBrightnessEnabled = that.isAutomaticBrightnessEnabled(); mDisplayBrightnessStrategyName = that.getDisplayBrightnessStrategyName(); mAutoBrightnessMode = that.mAutoBrightnessMode; + mSlowChange = that.mSlowChange; + mRampSpeed = that.mRampSpeed; } /** @@ -163,6 +167,8 @@ public final class BrightnessEvent { mAutomaticBrightnessEnabled = true; mDisplayBrightnessStrategyName = ""; mAutoBrightnessMode = AUTO_BRIGHTNESS_MODE_DEFAULT; + mSlowChange = false; + mRampSpeed = 0; } /** @@ -248,7 +254,9 @@ public final class BrightnessEvent { + ", powerFactor=" + mPowerFactor // Meta + ", physDisp=" + mPhysicalDisplayName + "(" + mPhysicalDisplayId + ")" - + ", logicalId=" + mDisplayId; + + ", logicalId=" + mDisplayId + + ", slowChange=" + mSlowChange + + ", rampSpeed=" + mRampSpeed; } @Override @@ -469,8 +477,8 @@ public final class BrightnessEvent { return mDisplayBrightnessStrategyName; } - public void setAutomaticBrightnessEnabled(boolean mAutomaticBrightnessEnabled) { - this.mAutomaticBrightnessEnabled = mAutomaticBrightnessEnabled; + public void setAutomaticBrightnessEnabled(boolean automaticBrightnessEnabled) { + mAutomaticBrightnessEnabled = automaticBrightnessEnabled; } @AutomaticBrightnessController.AutomaticBrightnessMode @@ -483,6 +491,14 @@ public final class BrightnessEvent { mAutoBrightnessMode = mode; } + public void setSlowChange(boolean slowChange) { + mSlowChange = slowChange; + } + + public void setRampSpeed(float rampSpeed) { + mRampSpeed = rampSpeed; + } + /** * A utility to stringify flags from a BrightnessEvent * @return Stringified flags from BrightnessEvent diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 7892639fc8ed..52e64905c984 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -168,8 +168,7 @@ public final class ColorDisplayService extends SystemService { new NightDisplayTintController(); private final TintController mGlobalSaturationTintController = new GlobalSaturationTintController(); - private final ReduceBrightColorsTintController mReduceBrightColorsTintController = - new ReduceBrightColorsTintController(); + private final ReduceBrightColorsTintController mReduceBrightColorsTintController; @VisibleForTesting final Handler mHandler; @@ -201,7 +200,13 @@ public final class ColorDisplayService extends SystemService { private boolean mEvenDimmerActivated; public ColorDisplayService(Context context) { + this(context, new ReduceBrightColorsTintController()); + } + + @VisibleForTesting + public ColorDisplayService(Context context, ReduceBrightColorsTintController rbcController) { super(context); + mReduceBrightColorsTintController = rbcController; mHandler = new TintHandler(DisplayThread.get().getLooper()); mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); mUserManager = UserManagerService.getInstance(); @@ -571,27 +576,37 @@ public final class ColorDisplayService extends SystemService { return mColorModeCompositionColorSpaces.get(mode, Display.COLOR_MODE_INVALID); } - private void onDisplayColorModeChanged(int mode) { + @VisibleForTesting + void onDisplayColorModeChanged(int mode) { if (mode == NOT_SET) { return; } + mReduceBrightColorsTintController.cancelAnimator(); mNightDisplayTintController.cancelAnimator(); mDisplayWhiteBalanceTintController.cancelAnimator(); + final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class); + if (mNightDisplayTintController.isAvailable(getContext())) { - final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class); mNightDisplayTintController.setUp(getContext(), dtm.needsLinearColorMatrix(mode)); mNightDisplayTintController .setMatrix(mNightDisplayTintController.getColorTemperatureSetting()); } + if (mReduceBrightColorsTintController.isAvailable(getContext())) { + // Different color modes may require different coefficients to be loaded for RBC. + // Re-set up RBC so that it can recalculate its transform matrix with new values. + mReduceBrightColorsTintController.setUp(getContext(), dtm.needsLinearColorMatrix(mode)); + onReduceBrightColorsStrengthLevelChanged(); // Trigger matrix recalc + updates + } + // dtm.setColorMode() needs to be called before // updateDisplayWhiteBalanceStatus(), this is because the latter calls // DisplayTransformManager.needsLinearColorMatrix(), therefore it is dependent // on the state of DisplayTransformManager. - final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class); dtm.setColorMode(mode, mNightDisplayTintController.getMatrix(), + mReduceBrightColorsTintController.getMatrix(), getCompositionColorSpace(mode)); if (mDisplayWhiteBalanceTintController.isAvailable(getContext())) { diff --git a/services/core/java/com/android/server/display/color/DisplayTransformManager.java b/services/core/java/com/android/server/display/color/DisplayTransformManager.java index a76c427bec0e..cb7b1773e47e 100644 --- a/services/core/java/com/android/server/display/color/DisplayTransformManager.java +++ b/services/core/java/com/android/server/display/color/DisplayTransformManager.java @@ -265,7 +265,7 @@ public class DisplayTransformManager { /** * Sets color mode and updates night display transform values. */ - public boolean setColorMode(int colorMode, float[] nightDisplayMatrix, + public boolean setColorMode(int colorMode, float[] nightDisplayMatrix, float[] rbcMatrix, int compositionColorMode) { if (colorMode == ColorDisplayManager.COLOR_MODE_NATURAL) { applySaturation(COLOR_SATURATION_NATURAL); @@ -285,7 +285,11 @@ public class DisplayTransformManager { setDisplayColor(colorMode, compositionColorMode); } + // These are close to the setDisplayColor() call to reduce delay between + // setting these matrixes and updating the color mode. Without this proximity + // of calls, updates to color mode can result in flicker. setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, nightDisplayMatrix); + setColorMatrix(LEVEL_COLOR_MATRIX_REDUCE_BRIGHT_COLORS, rbcMatrix); updateConfiguration(); diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 78bd41bd2e11..85b6bbb40b91 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -46,10 +46,6 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT, Flags::enableConnectedDisplayManagement); - private final FlagState mNbmControllerFlagState = new FlagState( - Flags.FLAG_ENABLE_NBM_CONTROLLER, - Flags::enableNbmController); - private final FlagState mHdrClamperFlagState = new FlagState( Flags.FLAG_ENABLE_HDR_CLAMPER, Flags::enableHdrClamper); @@ -282,11 +278,6 @@ public class DisplayManagerFlags { return mConnectedDisplayManagementFlagState.isEnabled(); } - /** Returns whether NBM Controller is enabled or not. */ - public boolean isNbmControllerEnabled() { - return mNbmControllerFlagState.isEnabled(); - } - /** Returns whether hdr clamper is enabled on not. */ public boolean isHdrClamperEnabled() { return mHdrClamperFlagState.isEnabled(); @@ -595,7 +586,6 @@ public class DisplayManagerFlags { pw.println(" " + mExternalDisplayLimitModeState); pw.println(" " + mDisplayTopology); pw.println(" " + mHdrClamperFlagState); - pw.println(" " + mNbmControllerFlagState); pw.println(" " + mPowerThrottlingClamperFlagState); pw.println(" " + mEvenDimmerFlagState); pw.println(" " + mSmallAreaDetectionFlagState); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 123b7dfbf843..3358f723709c 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -37,14 +37,6 @@ flag { } flag { - name: "enable_nbm_controller" - namespace: "display_manager" - description: "Feature flag for Normal Brightness Mode Controller" - bug: "299527549" - is_fixed_read_only: true -} - -flag { name: "enable_hdr_clamper" namespace: "display_manager" description: "Feature flag for HDR Clamper" diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 7505c710f483..424102cbdd89 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -212,11 +212,11 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { HdmiConfig.TIMEOUT_MS); } - launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC && - reason != HdmiControlService.INITIATED_BY_BOOT_UP); resetSelectRequestBuffer(); launchDeviceDiscovery(); startQueuedActions(); + final boolean routingForBootup = reason != HdmiControlService.INITIATED_BY_ENABLE_CEC + && reason != HdmiControlService.INITIATED_BY_BOOT_UP; List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE); if (bufferedActiveSource.isEmpty()) { @@ -227,14 +227,8 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) { - if (!mService.getLocalActiveSource().isValid() - && result != HdmiControlManager.RESULT_SUCCESS) { - mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( - getDeviceInfo().getLogicalAddress(), - getDeviceInfo().getPhysicalAddress())); - updateActiveSource(getDeviceInfo().getLogicalAddress(), - getDeviceInfo().getPhysicalAddress(), - "RequestActiveSourceAction#finishWithCallback()"); + if (result != HdmiControlManager.RESULT_SUCCESS) { + launchRoutingControl(routingForBootup); } } })); @@ -1384,8 +1378,7 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } else { int activePath = mService.getPhysicalAddress(); setActivePath(activePath); - if (!routingForBootup - && !mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) { + if (!mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) { mService.sendCecCommand( HdmiCecMessageBuilder.buildActiveSource( getDeviceInfo().getLogicalAddress(), activePath)); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 78c10453cd95..4e1df769100b 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -112,9 +112,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int openSession(HubEndpointInfo destination, String serviceDescriptor) throws RemoteException { - ContextHubServiceUtil.checkPermissions(mContext); + super.openSession_enforcePermission(); if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); int sessionId = mEndpointManager.reserveSessionId(); EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination); @@ -139,8 +140,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void closeSession(int sessionId, int reason) throws RemoteException { - ContextHubServiceUtil.checkPermissions(mContext); + super.closeSession_enforcePermission(); if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); try { mContextHubProxy.closeEndpointSession(sessionId, (byte) reason); @@ -151,8 +153,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregister() { - ContextHubServiceUtil.checkPermissions(mContext); + super.unregister_enforcePermission(); mIsRegistered.set(false); try { mContextHubProxy.unregisterEndpoint(mHalEndpointInfo); @@ -174,8 +177,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void openSessionRequestComplete(int sessionId) { - ContextHubServiceUtil.checkPermissions(mContext); + super.openSessionRequestComplete_enforcePermission(); synchronized (mOpenSessionLock) { try { mContextHubProxy.endpointSessionOpenComplete(sessionId); @@ -187,9 +191,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void sendMessage( int sessionId, HubMessage message, IContextHubTransactionCallback callback) { - ContextHubServiceUtil.checkPermissions(mContext); + super.sendMessage_enforcePermission(); Message halMessage = ContextHubServiceUtil.createHalMessage(message); synchronized (mOpenSessionLock) { if (!mActiveSessionIds.contains(sessionId) @@ -227,8 +232,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode) { - ContextHubServiceUtil.checkPermissions(mContext); + super.sendMessageDeliveryStatus_enforcePermission(); MessageDeliveryStatus status = new MessageDeliveryStatus(); status.messageSequenceNumber = messageSeqNumber; status.errorCode = errorCode; diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index d8c35358102d..f09be2c15ee0 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -289,7 +289,9 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { // doesn't have any registered discovery preference, we should still be able to route their // system media. boolean bindDueToSystemMediaRoutingSupport = - mIsManagerScanning && mSupportsSystemMediaRouting; + mLastDiscoveryPreference != null + && mLastDiscoveryPreference.shouldPerformActiveScan() + && mSupportsSystemMediaRouting; if (!getSessionInfos().isEmpty() || bindDueToManagerScan || bindDueToSystemMediaRoutingSupport) { diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java index 69c460e0f19d..42303e042561 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java @@ -32,6 +32,7 @@ import android.content.pm.ServiceInfo; import android.media.MediaRoute2ProviderService; import android.os.Handler; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -162,8 +163,14 @@ final class MediaRoute2ProviderWatcher { mUserId); Slog.i( TAG, - "Enabling proxy for MediaRoute2ProviderService: " - + proxy.mComponentName); + TextUtils.formatSimple( + "Enabling proxy for MediaRoute2ProviderService: %s" + + " (isSelfScan=%b, supportsSystemMediaRouting=%b," + + " userId=%d)", + proxy.mComponentName, + isSelfScanOnlyProvider, + supportsSystemMediaRouting, + mUserId)); proxy.start(/* rebindIfDisconnected= */ false); mProxies.add(targetIndex++, proxy); mCallback.onAddProviderService(proxy); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e18ed410c045..58deffcbd4ba 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -848,7 +848,7 @@ class MediaRouter2ServiceImpl { UserRecord userRecord = getOrCreateUserRecordLocked(userId); List<RoutingSessionInfo> sessionInfos; if (hasSystemRoutingPermissions) { - if (setDeviceRouteSelected) { + if (setDeviceRouteSelected && !Flags.enableMirroringInMediaRouter2()) { // Return a fake system session that shows the device route as selected and // available bluetooth routes as transferable. return userRecord.mHandler.getSystemProvider() @@ -2733,6 +2733,15 @@ class MediaRouter2ServiceImpl { newRoutes = Collections.emptySet(); } + if (Flags.enableMirroringInMediaRouter2() + && provider instanceof MediaRoute2ProviderServiceProxy proxyProvider) { + // We notify the system provider of service updates, so that it can update the + // system routing session by adding them as transferable routes. And we remove those + // that don't support remote routing. + mSystemProvider.updateSystemMediaRoutesFromProxy(proxyProvider); + newRoutes.removeIf(it -> !it.supportsRemoteRouting()); + } + // Add new routes to the maps. ArrayList<MediaRoute2Info> addedRoutes = new ArrayList<>(); boolean hasAddedOrModifiedRoutes = false; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 8dfba39dcfd8..b93846bf9ee7 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -73,6 +73,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // For apps without MODIFYING_AUDIO_ROUTING permission. // This should be the currently selected route. MediaRoute2Info mDefaultRoute; + + @GuardedBy("mLock") + RoutingSessionInfo mSystemSessionInfo; + RoutingSessionInfo mDefaultSessionInfo; private final AudioManagerBroadcastReceiver mAudioReceiver = @@ -180,7 +184,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { if (TextUtils.equals(routeOriginalId, mSelectedRouteId)) { RoutingSessionInfo currentSessionInfo; synchronized (mLock) { - currentSessionInfo = mSessionInfos.get(0); + currentSessionInfo = + Flags.enableMirroringInMediaRouter2() + ? mSystemSessionInfo + : mSessionInfos.get(0); } mCallback.onSessionCreated(this, requestId, currentSessionInfo); return; @@ -354,7 +361,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { - RoutingSessionInfo oldSessionInfo = mSessionInfos.get(0); + var oldSessionInfo = + Flags.enableMirroringInMediaRouter2() + ? mSystemSessionInfo + : mSessionInfos.get(0); builder.setTransferReason(oldSessionInfo.getTransferReason()) .setTransferInitiator(oldSessionInfo.getTransferInitiatorUserHandle(), oldSessionInfo.getTransferInitiatorPackageName()); @@ -364,6 +374,31 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } } + /** + * Notifies the system provider of a {@link MediaRoute2ProviderServiceProxy} update. + * + * <p>To be overridden so as to generate system media routes for {@link + * MediaRoute2ProviderService} routes that {@link MediaRoute2Info#supportsSystemMediaRouting() + * support system media routing}). + * + * @param serviceProxy The proxy of the service that updated its state. + */ + public void updateSystemMediaRoutesFromProxy(MediaRoute2ProviderServiceProxy serviceProxy) { + // Do nothing. This implementation doesn't support MR2ProviderService system media routes. + // The subclass overrides this method to implement app-managed system media routing (aka + // mirroring). + } + + /** + * Called when the system provider state changes. + * + * <p>To be overridden by {@link SystemMediaRoute2Provider2}, so that app-provided system media + * routing routes are added before setting the provider state. + */ + public void onSystemProviderRoutesChanged(MediaRoute2ProviderInfo providerInfo) { + setProviderState(providerInfo); + } + protected void updateProviderState() { MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder(); @@ -373,7 +408,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { for (MediaRoute2Info route : deviceRoutes) { builder.addRoute(route); } - setProviderState(builder.build()); + if (!Flags.enableMirroringInMediaRouter2()) { + setProviderState(builder.build()); + } } else { builder.addRoute(mDeviceRouteController.getSelectedRoute()); } @@ -382,7 +419,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { builder.addRoute(route); } MediaRoute2ProviderInfo providerInfo = builder.build(); - setProviderState(providerInfo); + onSystemProviderRoutesChanged(providerInfo); if (DEBUG) { Slog.d(TAG, "Updating system provider info : " + providerInfo); } @@ -393,8 +430,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { */ boolean updateSessionInfosIfNeeded() { synchronized (mLock) { - RoutingSessionInfo oldSessionInfo = mSessionInfos.isEmpty() ? null : mSessionInfos.get( - 0); + RoutingSessionInfo oldSessionInfo; + if (Flags.enableMirroringInMediaRouter2()) { + oldSessionInfo = mSystemSessionInfo; + } else { + oldSessionInfo = mSessionInfos.isEmpty() ? null : mSessionInfos.get(0); + } RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder( SYSTEM_SESSION_ID, "" /* clientPackageName */) @@ -483,8 +524,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { if (DEBUG) { Slog.d(TAG, "Updating system routing session info : " + newSessionInfo); } - mSessionInfos.clear(); - mSessionInfos.add(newSessionInfo); + mSystemSessionInfo = newSessionInfo; + onSystemSessionInfoUpdated(); mDefaultSessionInfo = new RoutingSessionInfo.Builder( SYSTEM_SESSION_ID, "" /* clientPackageName */) @@ -501,6 +542,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } } + @GuardedBy("mLock") + protected void onSystemSessionInfoUpdated() { + mSessionInfos.clear(); + mSessionInfos.add(mSystemSessionInfo); + } + @GuardedBy("mRequestLock") private void reportPendingSessionRequestResultLockedIfNeeded( RoutingSessionInfo newSessionInfo) { @@ -587,6 +634,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { RoutingSessionInfo sessionInfo; synchronized (mLock) { sessionInfo = mSessionInfos.get(0); + if (sessionInfo == null) { + return; + } } mCallback.onSessionUpdated(this, sessionInfo); diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index 85b30ad8cadb..7dc30ab66fd2 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -16,11 +16,26 @@ package com.android.server.media; +import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; + +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.media.MediaRoute2Info; +import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.RoutingSessionInfo; import android.os.Looper; import android.os.UserHandle; +import android.util.ArraySet; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; /** * Extends {@link SystemMediaRoute2Provider} by adding system routes provided by {@link @@ -30,6 +45,15 @@ import android.os.UserHandle; */ /* package */ class SystemMediaRoute2Provider2 extends SystemMediaRoute2Provider { + private static final String ROUTE_ID_PREFIX_SYSTEM = "SYSTEM"; + private static final String ROUTE_ID_SYSTEM_SEPARATOR = "."; + + @GuardedBy("mLock") + private MediaRoute2ProviderInfo mLastSystemProviderInfo; + + @GuardedBy("mLock") + private final Map<String, ProviderProxyRecord> mProxyRecords = new HashMap<>(); + private static final ComponentName COMPONENT_NAME = new ComponentName( SystemMediaRoute2Provider2.class.getPackage().getName(), @@ -46,4 +70,117 @@ import android.os.UserHandle; private SystemMediaRoute2Provider2(Context context, UserHandle user, Looper looper) { super(context, COMPONENT_NAME, user, looper); } + + @Override + protected void onSystemSessionInfoUpdated() { + updateSessionInfo(); + } + + @Override + public void updateSystemMediaRoutesFromProxy(MediaRoute2ProviderServiceProxy serviceProxy) { + var proxyRecord = ProviderProxyRecord.createFor(serviceProxy); + synchronized (mLock) { + if (proxyRecord == null) { + mProxyRecords.remove(serviceProxy.mUniqueId); + } else { + mProxyRecords.put(serviceProxy.mUniqueId, proxyRecord); + } + setProviderState(buildProviderInfo()); + } + updateSessionInfo(); + notifyProviderState(); + notifySessionInfoUpdated(); + } + + @Override + public void onSystemProviderRoutesChanged(MediaRoute2ProviderInfo providerInfo) { + synchronized (mLock) { + mLastSystemProviderInfo = providerInfo; + setProviderState(buildProviderInfo()); + } + updateSessionInfo(); + notifySessionInfoUpdated(); + } + + /** + * Updates the {@link #mSessionInfos} by expanding the {@link SystemMediaRoute2Provider} session + * with information from the {@link MediaRoute2ProviderService provider services}. + */ + private void updateSessionInfo() { + synchronized (mLock) { + var systemSessionInfo = mSystemSessionInfo; + if (systemSessionInfo == null) { + // The system session info hasn't been initialized yet. Do nothing. + return; + } + var builder = new RoutingSessionInfo.Builder(systemSessionInfo); + mProxyRecords.values().stream() + .flatMap(ProviderProxyRecord::getRoutesStream) + .map(MediaRoute2Info::getId) + .forEach(builder::addTransferableRoute); + mSessionInfos.clear(); + mSessionInfos.add(builder.build()); + } + } + + /** + * Returns a new a provider info that includes all routes from the system provider {@link + * SystemMediaRoute2Provider}, along with system routes from {@link MediaRoute2ProviderService + * provider services}. + */ + @GuardedBy("mLock") + private MediaRoute2ProviderInfo buildProviderInfo() { + MediaRoute2ProviderInfo.Builder builder = + new MediaRoute2ProviderInfo.Builder(mLastSystemProviderInfo); + mProxyRecords.values().stream() + .flatMap(ProviderProxyRecord::getRoutesStream) + .forEach(builder::addRoute); + return builder.build(); + } + + /** + * Holds information about {@link MediaRoute2ProviderService provider services} registered in + * the system. + * + * @param mProxy The corresponding {@link MediaRoute2ProviderServiceProxy}. + * @param mSystemMediaRoutes The last snapshot of routes from the service that support system + * media routing, as defined by {@link MediaRoute2Info#supportsSystemMediaRouting()}. + */ + private record ProviderProxyRecord( + MediaRoute2ProviderServiceProxy mProxy, + Collection<MediaRoute2Info> mSystemMediaRoutes) { + + /** Returns a stream representation of the {@link #mSystemMediaRoutes}. */ + public Stream<MediaRoute2Info> getRoutesStream() { + return mSystemMediaRoutes.stream(); + } + + /** + * Returns a new instance, or null if the given {@code serviceProxy} doesn't have an + * associated {@link MediaRoute2ProviderInfo}. + */ + @Nullable + public static ProviderProxyRecord createFor(MediaRoute2ProviderServiceProxy serviceProxy) { + MediaRoute2ProviderInfo providerInfo = serviceProxy.getProviderInfo(); + if (providerInfo == null) { + return null; + } + ArraySet<MediaRoute2Info> routes = new ArraySet<>(); + providerInfo.getRoutes().stream() + .filter(MediaRoute2Info::supportsSystemMediaRouting) + .forEach( + route -> { + String id = + ROUTE_ID_PREFIX_SYSTEM + + route.getProviderId() + + ROUTE_ID_SYSTEM_SEPARATOR + + route.getOriginalId(); + routes.add( + new MediaRoute2Info.Builder(id, route.getName()) + .addFeature(FEATURE_LIVE_AUDIO) + .build()); + }); + return new ProviderProxyRecord(serviceProxy, Collections.unmodifiableSet(routes)); + } + } } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 3e488bf2207f..c81023144c0f 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -42,6 +42,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -138,7 +139,7 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - getAllMediaProfileColumns(), selection, selectionArguments) + getMediaProfileColumns(includeParams), selection, selectionArguments) ) { int count = cursor.getCount(); if (count == 0) { @@ -160,8 +161,8 @@ public class MediaQualityService extends SystemService { String packageName, boolean includeParams, UserHandle user) { String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getPictureProfilesBasedOnConditions(getAllMediaProfileColumns(), selection, - selectionArguments); + return getPictureProfilesBasedOnConditions(getMediaProfileColumns(includeParams), + selection, selectionArguments); } @Override @@ -259,7 +260,7 @@ public class MediaQualityService extends SystemService { try ( Cursor cursor = getCursorAfterQuerying( mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - getAllMediaProfileColumns(), selection, selectionArguments) + getMediaProfileColumns(includeParams), selection, selectionArguments) ) { int count = cursor.getCount(); if (count == 0) { @@ -281,8 +282,8 @@ public class MediaQualityService extends SystemService { String packageName, boolean includeParams, UserHandle user) { String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getSoundProfilesBasedOnConditions(getAllMediaProfileColumns(), selection, - selectionArguments); + return getSoundProfilesBasedOnConditions(getMediaProfileColumns(includeParams), + selection, selectionArguments); } @Override @@ -406,15 +407,18 @@ public class MediaQualityService extends SystemService { return values; } - private String[] getAllMediaProfileColumns() { - return new String[]{ + private String[] getMediaProfileColumns(boolean includeParams) { + ArrayList<String> columns = new ArrayList<>(Arrays.asList( BaseParameters.PARAMETER_ID, BaseParameters.PARAMETER_TYPE, BaseParameters.PARAMETER_NAME, BaseParameters.PARAMETER_INPUT_ID, - BaseParameters.PARAMETER_PACKAGE, - mMediaQualityDbHelper.SETTINGS - }; + BaseParameters.PARAMETER_PACKAGE) + ); + if (includeParams) { + columns.add(mMediaQualityDbHelper.SETTINGS); + } + return columns.toArray(new String[0]); } private PictureProfile getPictureProfileWithTempIdFromCursor(Cursor cursor) { diff --git a/services/core/java/com/android/server/pm/RestrictionsSet.java b/services/core/java/com/android/server/pm/RestrictionsSet.java index 08047695a42a..38075c135d2a 100644 --- a/services/core/java/com/android/server/pm/RestrictionsSet.java +++ b/services/core/java/com/android/server/pm/RestrictionsSet.java @@ -65,6 +65,7 @@ public class RestrictionsSet { throw new IllegalArgumentException("empty restriction bundle cannot be added."); } mUserRestrictions.put(userId, restrictions); + UserManager.invalidateUserRestriction(); } /** @@ -84,6 +85,7 @@ public class RestrictionsSet { } else { mUserRestrictions.delete(userId); } + UserManager.invalidateUserRestriction(); return true; } @@ -102,6 +104,9 @@ public class RestrictionsSet { removed = true; } } + if (removed) { + UserManager.invalidateUserRestriction(); + } return removed; } @@ -129,6 +134,7 @@ public class RestrictionsSet { i--; } } + UserManager.invalidateUserRestriction(); } } @@ -192,6 +198,7 @@ public class RestrictionsSet { public boolean remove(@UserIdInt int userId) { boolean hasUserRestriction = mUserRestrictions.contains(userId); mUserRestrictions.remove(userId); + UserManager.invalidateUserRestriction(); return hasUserRestriction; } @@ -200,6 +207,7 @@ public class RestrictionsSet { */ public void removeAllRestrictions() { mUserRestrictions.clear(); + UserManager.invalidateUserRestriction(); } /** diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 066fce068d61..8249d65868cd 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1113,6 +1113,7 @@ public class UserManagerService extends IUserManager.Stub { UserManager.invalidateUserPropertiesCache(); } UserManager.invalidateCacheOnUserListChange(); + UserManager.invalidateUserRestriction(); } } diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index 09feb18d07bf..6ab30595e46b 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -216,12 +216,12 @@ final class DefaultPermissionGrantPolicy { private static final Set<String> SENSORS_PERMISSIONS = new ArraySet<>(); static { + SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS); + SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS_BACKGROUND); + if (Flags.replaceBodySensorPermissionEnabled()) { SENSORS_PERMISSIONS.add(HealthPermissions.READ_HEART_RATE); SENSORS_PERMISSIONS.add(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND); - } else { - SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS); - SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS_BACKGROUND); } } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 05bc69a9f1f0..672eb4caf798 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -264,6 +264,16 @@ public class PermissionManagerService extends IPermissionManager.Stub { persistentDeviceId, mPermissionManagerServiceImpl::checkUidPermission); } + @Override + @Context.PermissionRequestState + public int getPermissionRequestState(@NonNull String packageName, + @NonNull String permissionName, int deviceId) { + Objects.requireNonNull(permissionName, "permission can't be null."); + Objects.requireNonNull(packageName, "package name can't be null."); + return mPermissionManagerServiceImpl.getPermissionRequestState(packageName, permissionName, + getPersistentDeviceId(deviceId)); + } + private String getPersistentDeviceId(int deviceId) { if (deviceId == Context.DEVICE_ID_DEFAULT) { return VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index ea71953d7cb3..ca70bddc5ac1 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -1014,6 +1014,11 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } @Override + public int getPermissionRequestState(String packageName, String permName, String deviceId) { + throw new IllegalStateException("getPermissionRequestState is not supported."); + } + + @Override public Map<String, PermissionManager.PermissionState> getAllPermissionStates( @NonNull String packageName, @NonNull String deviceId, int userId) { throw new UnsupportedOperationException( diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java index 754b141ff10d..b607832767a1 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java @@ -20,6 +20,7 @@ import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; @@ -407,6 +408,16 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte int checkUidPermission(int uid, String permName, String deviceId); /** + * Returns one of the permission state + * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_GRANTED}, + * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_REQUESTABLE}, or + * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_UNREQUESTABLE} + * for permission request permission flow. + */ + int getPermissionRequestState(@NonNull String packageName, @NonNull String permName, + @NonNull String deviceId); + + /** * Gets the permission states for requested package, persistent device and user. * <p> * <strong>Note: </strong>Default device permissions are not inherited in this API. Returns the diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java index c18f856594ed..ba5e97e7b113 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java @@ -247,6 +247,13 @@ public class PermissionManagerServiceLoggingDecorator implements PermissionManag } @Override + public int getPermissionRequestState(String packageName, String permName, String deviceId) { + Log.i(LOG_TAG, "checkUidPermissionState(permName = " + permName + ", deviceId = " + + deviceId + ", packageName = " + packageName + ")"); + return mService.getPermissionRequestState(packageName, permName, deviceId); + } + + @Override public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName, @NonNull String deviceId, int userId) { Log.i(LOG_TAG, diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java index 40139baf0e98..008c14db8b65 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java @@ -319,6 +319,11 @@ public class PermissionManagerServiceTestingShim implements PermissionManagerSer } @Override + public int getPermissionRequestState(String packageName, String permName, String deviceId) { + return mNewImplementation.getPermissionRequestState(packageName, permName, deviceId); + } + + @Override public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName, @NonNull String deviceId, int userId) { return mNewImplementation.getAllPermissionStates(packageName, deviceId, userId); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java index 981d3d92b15a..2a47f51da951 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java @@ -345,6 +345,18 @@ public class PermissionManagerServiceTracingDecorator implements PermissionManag } } + + @Override + public int getPermissionRequestState(String packageName, String permName, String deviceId) { + Trace.traceBegin(TRACE_TAG, + "TaggedTracingPermissionManagerServiceImpl#checkUidPermissionState"); + try { + return mService.getPermissionRequestState(packageName, permName, deviceId); + } finally { + Trace.traceEnd(TRACE_TAG); + } + } + @Override public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName, @NonNull String deviceId, int userId) { diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 51e0195f13fc..aae7417970eb 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -341,6 +341,8 @@ public final class HintManagerService extends SystemService { supportInfo.headroom = new SupportInfo.HeadroomSupportInfo(); supportInfo.headroom.isCpuSupported = false; supportInfo.headroom.isGpuSupported = false; + + supportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo(); if (isHintSessionSupported()) { if (mPowerHalVersion == 4) { // Assume we support the V4 hints & modes unless specified @@ -1649,6 +1651,7 @@ public final class HintManagerService extends SystemService { mSessionManager = ISessionManager.Stub.asInterface(sessionManager); } + @Override public IHintManager.HintManagerClientData registerClient(@NonNull IHintManager.IHintManagerClient clientBinder) { IHintManager.HintManagerClientData out = new IHintManager.HintManagerClientData(); diff --git a/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java b/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java index f51c25d6761c..acdea881e3d1 100644 --- a/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java +++ b/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java @@ -48,7 +48,7 @@ public final class DisallowCellular2GAdvancedProtectionHook extends AdvancedProt mTelephonyManager = context.getSystemService(TelephonyManager.class); mSubscriptionManager = context.getSystemService(SubscriptionManager.class); - setPolicy(enabled); + onAdvancedProtectionChanged(enabled); } @NonNull @@ -94,7 +94,8 @@ public final class DisallowCellular2GAdvancedProtectionHook extends AdvancedProt return false; } - private void setPolicy(boolean enabled) { + @Override + public void onAdvancedProtectionChanged(boolean enabled) { if (enabled) { Slog.d(TAG, "Setting DISALLOW_CELLULAR_2G_GLOBALLY restriction"); mDevicePolicyManager.addUserRestrictionGlobally( @@ -105,21 +106,4 @@ public final class DisallowCellular2GAdvancedProtectionHook extends AdvancedProt ADVANCED_PROTECTION_SYSTEM_ENTITY, UserManager.DISALLOW_CELLULAR_2G); } } - - @Override - public void onAdvancedProtectionChanged(boolean enabled) { - setPolicy(enabled); - - // Leave 2G disabled even if APM is disabled. - if (!enabled) { - for (TelephonyManager telephonyManager : getActiveTelephonyManagers()) { - long oldAllowedTypes = - telephonyManager.getAllowedNetworkTypesForReason( - TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G); - long newAllowedTypes = oldAllowedTypes & ~TelephonyManager.NETWORK_CLASS_BITMASK_2G; - telephonyManager.setAllowedNetworkTypesForReason( - TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G, newAllowedTypes); - } - } - } } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 417d6a5d12ee..89c7a3d89a54 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -57,6 +57,7 @@ import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_T import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; @@ -103,6 +104,7 @@ import android.util.TimeUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.LatencyTracker; import com.android.internal.util.function.pooled.PooledLambda; @@ -389,6 +391,14 @@ class ActivityMetricsLogger { return; } if (mLastLaunchedActivity != null) { + if (mLastLaunchedActivity.mLaunchCookie != null) { + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS, + "Transferring launch cookie=%s from=%s(%d) to=%s(%d)", + mLastLaunchedActivity.mLaunchCookie, + mLastLaunchedActivity.packageName, + System.identityHashCode(mLastLaunchedActivity), r.packageName, + System.identityHashCode(r)); + } // Transfer the launch cookie and launch root task because it is a consecutive // launch event. r.mLaunchCookie = mLastLaunchedActivity.mLaunchCookie; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 29a71328127b..348d326b83d2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -137,6 +137,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ORIENTATIO import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STARTING_WINDOW; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SWITCH; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; @@ -2137,6 +2138,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mHandoverLaunchDisplayId = options.getLaunchDisplayId(); mLaunchCookie = options.getLaunchCookie(); mLaunchRootTask = options.getLaunchRootTask(); + if (mLaunchCookie != null) { + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS, + "Activity created with launch cookie=%s act=%s(%d)", + mLaunchCookie, packageName, System.identityHashCode(this)); + } } else { mHasSceneTransition = false; } @@ -4141,6 +4147,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A r -> r.mLaunchCookie == null && !r.finishing && r.isUid(getUid()), this, false /* includeBoundary */, false /* traverseTopToBottom */); if (nextCookieTarget != null) { + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS, + "Transferring launch cookie=%s on finish from=%s(%d) to=%s(%d)", + mLaunchCookie, packageName, System.identityHashCode(this), + nextCookieTarget.packageName, System.identityHashCode(nextCookieTarget)); nextCookieTarget.mLaunchCookie = mLaunchCookie; mLaunchCookie = null; } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 2781592c6b4f..acb93844c945 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -65,6 +65,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; @@ -3097,6 +3098,10 @@ class ActivityStarter { // options if set. if (mStartActivity.mLaunchCookie != null) { intentActivity.mLaunchCookie = mStartActivity.mLaunchCookie; + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS, + "Updating launch cookie=%s act=%s(%d)", + intentActivity.mLaunchCookie, intentActivity.packageName, + System.identityHashCode(intentActivity)); } if (mStartActivity.mPendingRemoteAnimation != null) { intentActivity.mPendingRemoteAnimation = mStartActivity.mPendingRemoteAnimation; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index a077a0b9a2ca..0aff1de72cb1 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -54,6 +54,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS; @@ -1268,7 +1269,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // Checks if the caller can be shown in the given public display. int userId = UserHandle.getUserId(callingUid); int displayId = display.getDisplayId(); - boolean allowed = mWindowManager.mUmInternal.isUserVisible(userId, displayId); + boolean allowed = userId == UserHandle.USER_SYSTEM + || mWindowManager.mUmInternal.isUserVisible(userId, displayId); ProtoLog.d(WM_DEBUG_TASKS, "Launch on display check: %s launch for userId=%d on displayId=%d", (allowed ? "allow" : "disallow"), userId, displayId); @@ -2842,6 +2844,10 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { targetActivity.applyOptionsAnimation(); if (activityOptions != null && activityOptions.getLaunchCookie() != null) { targetActivity.mLaunchCookie = activityOptions.getLaunchCookie(); + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS, + "Updating launch cookie=%s for start from recents act=%s(%d)", + targetActivity.mLaunchCookie, targetActivity.packageName, + System.identityHashCode(targetActivity)); } } finally { mActivityMetricsLogger.notifyActivityLaunched(launchingState, diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e9e3c9ee389e..1a7c6b70f007 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -275,12 +275,8 @@ class BackNavigationController { final boolean isOccluded = isKeyguardOccluded(window); if (!canAnimate) { backType = BackNavigationInfo.TYPE_CALLBACK; - } else if ((window.getParent().getChildCount() > 1 - && window.getParent().getChildAt(0) != window)) { - // TODO Dialog window does not need to attach on activity, check - // window.mAttrs.type != TYPE_BASE_APPLICATION - // Are we the top window of our parent? If not, we are a window on top of the - // activity, we won't close the activity. + } else if (window.mAttrs.type != TYPE_BASE_APPLICATION) { + // The focus window belongs to an activity and it's not the base window. backType = BackNavigationInfo.TYPE_DIALOG_CLOSE; removedWindowContainer = window; } else if (hasTranslucentActivity(currentActivity, prevActivities)) { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 24a6f118ad04..4bcba13448e9 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -258,16 +258,13 @@ class InsetsPolicy { * We also need to exclude certain types of insets source for client within specific windowing * modes. * - * @param attrs the LayoutParams of the target - * @param windowingMode the windowing mode of the target - * @param isAlwaysOnTop is the target always on top + * @param target the target on which the policy is applied * @param state the input inset state containing all the sources * @return The state stripped of the necessary information. */ - InsetsState enforceInsetsPolicyForTarget(WindowManager.LayoutParams attrs, - @WindowConfiguration.WindowingMode int windowingMode, boolean isAlwaysOnTop, - InsetsState state) { + InsetsState enforceInsetsPolicyForTarget(WindowState target, InsetsState state) { final InsetsState originalState = state; + final WindowManager.LayoutParams attrs = target.getAttrs(); // The caller should not receive the visible insets provided by itself. if (attrs.type == TYPE_INPUT_METHOD) { @@ -316,12 +313,17 @@ class InsetsPolicy { } } + final @WindowConfiguration.WindowingMode int windowingMode = target.getWindowingMode(); if (WindowConfiguration.isFloating(windowingMode) - || (windowingMode == WINDOWING_MODE_MULTI_WINDOW && isAlwaysOnTop)) { + || (windowingMode == WINDOWING_MODE_MULTI_WINDOW && target.isAlwaysOnTop())) { // Keep frames, caption, and IME. int types = WindowInsets.Type.captionBar(); if (windowingMode != WINDOWING_MODE_PINNED) { - types |= WindowInsets.Type.ime(); + if (!Flags.refactorInsetsController() || (mDisplayContent != null + && target == mDisplayContent.getImeInputTarget() + && (WindowInsets.Type.ime() & target.getRequestedVisibleTypes()) != 0)) { + types |= WindowInsets.Type.ime(); + } } final InsetsState newState = new InsetsState(); newState.set(state, types); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index bcd12f253299..b4a22b0dd034 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -4150,7 +4150,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { .setSourceCrop(cropBounds) .setCaptureSecureLayers(true) .setAllowProtected(true) - .setHintForSeamlessTransition(isDisplayRotation) + // We always reroute this screenshot to the display, so this transition + // is ALWAYS seamless + .setHintForSeamlessTransition(true) .build(); ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = ScreenCapture.captureLayers(captureArgs); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 90bf053dfbef..68b4b6f0ae91 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1631,8 +1631,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } final InsetsState rawInsetsState = mFrozenInsetsState != null ? mFrozenInsetsState : getMergedInsetsState(); - final InsetsState insetsStateForWindow = insetsPolicy.enforceInsetsPolicyForTarget( - mAttrs, getWindowingMode(), isAlwaysOnTop(), rawInsetsState); + final InsetsState insetsStateForWindow = insetsPolicy.enforceInsetsPolicyForTarget(this, + rawInsetsState); return insetsPolicy.adjustInsetsForWindow(this, insetsStateForWindow, includeTransient); } @@ -3303,7 +3303,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // just kill it. And if it is a window of foreground activity, the activity can be // restarted automatically if needed. Slog.w(TAG, "Exception thrown during dispatchAppVisibility " + this, e); - if (android.os.Process.getUidForPid(mSession.mPid) == mSession.mUid) { + if (android.os.Process.getUidForPid(mSession.mPid) == mSession.mUid + && android.os.Process.getThreadGroupLeader(mSession.mPid) == mSession.mPid) { android.os.Process.killProcess(mSession.mPid); } } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 82699ea3badb..01639cc3b516 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -61,6 +61,7 @@ cc_library_static { "com_android_server_SystemServer.cpp", "com_android_server_tv_TvUinputBridge.cpp", "com_android_server_tv_TvInputHal.cpp", + "com_android_server_UsbAlsaDevice.cpp", "com_android_server_UsbAlsaJackDetector.cpp", "com_android_server_UsbAlsaMidiDevice.cpp", "com_android_server_UsbDeviceManager.cpp", diff --git a/services/core/jni/com_android_server_UsbAlsaDevice.cpp b/services/core/jni/com_android_server_UsbAlsaDevice.cpp new file mode 100644 index 000000000000..166932f167ed --- /dev/null +++ b/services/core/jni/com_android_server_UsbAlsaDevice.cpp @@ -0,0 +1,70 @@ +/* + * 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. + */ + +#define LOG_TAG "UsbAlsaDeviceJNI" + +#include <nativehelper/JNIPlatformHelp.h> +#include <tinyalsa/asoundlib.h> + +#include <string> +#include <vector> + +#include "jni.h" +#include "utils/Log.h" + +static const std::vector<std::string> POSSIBLE_HARDWARE_VOLUME_MIXER_NAMES = + {"Headphone Playback Volume", "Headset Playback Volume", "PCM Playback Volume"}; + +namespace android { + +static void android_server_UsbAlsaDevice_setVolume(JNIEnv* /*env*/, jobject /*thiz*/, jint card, + float volume) { + ALOGD("%s(%d, %f)", __func__, card, volume); + struct mixer* alsaMixer = mixer_open(card); + if (alsaMixer == nullptr) { + ALOGW("%s(%d, %f) returned as no mixer is opened", __func__, card, volume); + return; + } + struct mixer_ctl* ctl = nullptr; + for (const auto& mixerName : POSSIBLE_HARDWARE_VOLUME_MIXER_NAMES) { + ctl = mixer_get_ctl_by_name(alsaMixer, mixerName.c_str()); + if (ctl != nullptr) { + break; + } + } + if (ctl == nullptr) { + ALOGW("%s(%d, %f) returned as no volume mixer is found", __func__, card, volume); + return; + } + const unsigned int n = mixer_ctl_get_num_values(ctl); + for (unsigned int id = 0; id < n; id++) { + if (int error = mixer_ctl_set_percent(ctl, id, 100 * volume); error != 0) { + ALOGE("%s(%d, %f) failed, error=%d", __func__, card, volume, error); + return; + } + } + ALOGD("%s(%d, %f) succeed", __func__, card, volume); +} + +static JNINativeMethod method_table[] = { + {"nativeSetVolume", "(IF)V", (void*)android_server_UsbAlsaDevice_setVolume}, +}; + +int register_android_server_UsbAlsaDevice(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/usb/UsbAlsaDevice", method_table, + NELEM(method_table)); +} +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 09fd8d4ac02e..e3bd69c30de7 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -33,6 +33,7 @@ int register_android_server_power_stats_CpuPowerStatsCollector(JNIEnv* env); int register_android_server_HintManagerService(JNIEnv* env); int register_android_server_storage_AppFuse(JNIEnv* env); int register_android_server_SystemServer(JNIEnv* env); +int register_android_server_UsbAlsaDevice(JNIEnv* env); int register_android_server_UsbAlsaJackDetector(JNIEnv* env); int register_android_server_UsbAlsaMidiDevice(JNIEnv* env); int register_android_server_UsbDeviceManager(JavaVM* vm, JNIEnv* env); @@ -98,6 +99,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_InputManager(env); register_android_server_LightsService(env); register_android_server_UsbDeviceManager(vm, env); + register_android_server_UsbAlsaDevice(env); register_android_server_UsbAlsaJackDetector(env); register_android_server_UsbAlsaMidiDevice(env); register_android_server_UsbHostManager(env); diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index 0b7438cd1b17..018cf20e914f 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -464,6 +464,48 @@ class PermissionService(private val service: AccessCheckingService) : return size } + override fun getPermissionRequestState( + packageName: String, + permissionName: String, + deviceId: String + ): Int { + val uid = Binder.getCallingUid() + val result = context.checkPermission(permissionName, Binder.getCallingPid(), uid) + if (result == PackageManager.PERMISSION_GRANTED) { + return Context.PERMISSION_REQUEST_STATE_GRANTED + } + + val appId = UserHandle.getAppId(uid) + val userId = UserHandle.getUserId(uid) + val packageState = + packageManagerLocal.withFilteredSnapshot(uid, userId).use { + it.getPackageState(packageName) + } ?: return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + val androidPackage = packageState.androidPackage + ?: return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + if (appId != packageState.appId) { + return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + } + val permission = service.getState { + with(policy) { getPermissions()[permissionName] } + } + if (permission == null || !permission.isRuntime) { + return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + } + if (permissionName !in androidPackage.requestedPermissions) { + return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + } + + val permissionFlags = service.getState { + getPermissionFlagsWithPolicy(appId, userId, permissionName, deviceId) + } + return if (permissionFlags.hasAnyBit(UNREQUESTABLE_MASK)) { + Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE + } else { + Context.PERMISSION_REQUEST_STATE_REQUESTABLE + } + } + override fun checkUidPermission(uid: Int, permissionName: String, deviceId: String): Int { val userId = UserHandle.getUserId(uid) if (!userManagerInternal.exists(userId)) { @@ -472,7 +514,7 @@ class PermissionService(private val service: AccessCheckingService) : // PackageManagerInternal.getPackage(int) already checks package visibility and enforces // that instant apps can't see shared UIDs. Note that on the contrary, - // Note that PackageManagerInternal.getPackage(String) doesn't perform any checks. + // PackageManagerInternal.getPackage(String) doesn't perform any checks. val androidPackage = packageManagerInternal.getPackage(uid) if (androidPackage != null) { // Note that PackageManagerInternal.getPackageStateInternal() is not filtered. diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index 0ccaa6043f5f..073ee31ddd60 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -16,6 +16,11 @@ package com.android.server.supervision; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import static com.android.internal.util.Preconditions.checkCallAuthorization; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -31,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.os.Binder; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ResultReceiver; @@ -78,6 +84,9 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { + if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { + enforcePermission(INTERACT_ACROSS_USERS); + } synchronized (getLockObject()) { return getUserDataLocked(userId).supervisionEnabled; } @@ -151,7 +160,8 @@ public class SupervisionService extends ISupervisionManager.Stub { /** Returns whether the supervision app has profile owner status. */ private boolean isProfileOwner(@UserIdInt int userId) { - ComponentName profileOwner = mDpmInternal.getProfileOwnerAsUser(userId); + ComponentName profileOwner = + mDpmInternal != null ? mDpmInternal.getProfileOwnerAsUser(userId) : null; return profileOwner != null && isSupervisionAppPackage(profileOwner.getPackageName()); } @@ -161,6 +171,12 @@ public class SupervisionService extends ISupervisionManager.Stub { mContext.getResources().getString(R.string.config_systemSupervision)); } + /** Enforces that the caller has the given permission. */ + private void enforcePermission(String permission) { + checkCallAuthorization( + mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED); + } + public static class Lifecycle extends SystemService { private final SupervisionService mSupervisionService; diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt index f589a2c9385c..7db6ea0bf86d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt @@ -60,12 +60,6 @@ class BrightnessRangeControllerTest { } @Test - fun testMaxBrightness_HbmDisabledAndNotAllowed() { - val controller = createController(nbmEnabled = false, hbmAllowed = false) - assertThat(controller.currentBrightnessMax).isEqualTo(MAX_BRIGHTNESS) - } - - @Test fun testMaxBrightness_transitionPointLessThanCurrentNbmLimit() { val controller = createController( hbmAllowed = false, @@ -76,13 +70,11 @@ class BrightnessRangeControllerTest { } private fun createController( - nbmEnabled: Boolean = true, hbmSupported: Boolean = true, hbmAllowed: Boolean = true, hbmMaxBrightness: Float = MAX_BRIGHTNESS, nbmMaxBrightness: Float = NORMAL_BRIGHTNESS_LOW ): BrightnessRangeController { - whenever(mockFlags.isNbmControllerEnabled).thenReturn(nbmEnabled) whenever(mockHbmController.deviceSupportsHbm()).thenReturn(hbmSupported) whenever(mockHbmController.isHbmCurrentlyAllowed).thenReturn(hbmAllowed) whenever(mockHbmController.currentBrightnessMax).thenReturn(hbmMaxBrightness) diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 8ca39194de3b..a4dfecb8ed96 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -2626,8 +2626,8 @@ public final class DisplayPowerControllerTest { mock(ScreenOffBrightnessSensorController.class); final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class); final HdrClamper hdrClamper = mock(HdrClamper.class); - final NormalBrightnessModeController normalBrightnessModeController = mock( - NormalBrightnessModeController.class); + final NormalBrightnessModeController normalBrightnessModeController = + new NormalBrightnessModeController(); BrightnessClamperController clamperController = mock(BrightnessClamperController.class); when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java index df09b046ddd2..6d1e56d1f479 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java @@ -68,6 +68,8 @@ public final class BrightnessEventTest { mBrightnessEvent.setAutomaticBrightnessEnabled(true); mBrightnessEvent.setDisplayBrightnessStrategyName(DISPLAY_BRIGHTNESS_STRATEGY_NAME); mBrightnessEvent.setAutoBrightnessMode(AUTO_BRIGHTNESS_MODE_IDLE); + mBrightnessEvent.setSlowChange(true); + mBrightnessEvent.setRampSpeed(0.3f); } @Test @@ -88,7 +90,7 @@ public final class BrightnessEventTest { + "preLux=150.0, wasShortTermModelActive=true, autoBrightness=true (idle), " + "unclampedBrt=0.65, hbmMax=0.62, hbmMode=off, thrmMax=0.65, " + "rbcStrength=-1, powerFactor=0.2, physDisp=display_name(987654321), " - + "logicalId=1"; + + "logicalId=1, slowChange=true, rampSpeed=0.3"; assertEquals(expectedString, actualString); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java index f391e409a717..4e81b3530b62 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java @@ -20,10 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -89,6 +92,7 @@ public class ColorDisplayServiceTest { private ColorDisplayService.BinderService mBinderService; private Resources mResourcesSpy; + private ReduceBrightColorsTintController mRbcSpy; private static final int[] MINIMAL_COLOR_MODES = new int[] { ColorDisplayManager.COLOR_MODE_NATURAL, @@ -135,7 +139,8 @@ public class ColorDisplayServiceTest { mLocalServiceKeeperRule.overrideLocalService( DisplayManagerInternal.class, mDisplayManagerInternal); - mCds = new ColorDisplayService(mContext); + mRbcSpy = Mockito.spy(new ReduceBrightColorsTintController()); + mCds = new ColorDisplayService(mContext, mRbcSpy); mBinderService = mCds.new BinderService(); mLocalServiceKeeperRule.overrideLocalService( ColorDisplayService.ColorDisplayServiceInternal.class, @@ -1106,7 +1111,8 @@ public class ColorDisplayServiceTest { setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( - eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID)); + eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), any(), + eq(Display.COLOR_MODE_INVALID)); } @Test @@ -1124,7 +1130,8 @@ public class ColorDisplayServiceTest { setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( - eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID)); + eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), any(), + eq(Display.COLOR_MODE_INVALID)); } @Test @@ -1140,7 +1147,8 @@ public class ColorDisplayServiceTest { setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( - eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_SRGB)); + eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), any(), + eq(Display.COLOR_MODE_SRGB)); } @Test @@ -1156,7 +1164,8 @@ public class ColorDisplayServiceTest { setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED); startService(); verify(mDisplayTransformManager).setColorMode( - eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID)); + eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), any(), + eq(Display.COLOR_MODE_INVALID)); } @Test @@ -1164,10 +1173,27 @@ public class ColorDisplayServiceTest { when(mResourcesSpy.getIntArray(R.array.config_availableColorModes)) .thenReturn(new int[] {}); startService(); - verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt()); + verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), any(), anyInt()); assertThat(mBinderService.getColorMode()).isEqualTo(-1); } + @Test + public void ensureColorModeChangeTriggersRbcReload() { + // should set up RBC once at startup + startService(); + reset(mRbcSpy); + + // Make sure RBC is enabled and available for this test + doReturn(true).when(mRbcSpy).isAvailable(mContext); + + // When Color Mode changes, RBC needs to re-setup + // onDisplayColorModeChanged cancels animations, and should be called in handler thread + mCds.mHandler.runWithScissors( + () -> mCds.onDisplayColorModeChanged(ColorDisplayManager.COLOR_MODE_NATURAL), + 1000); + verify(mRbcSpy, times(1)).setUp(eq(mContext), anyBoolean()); + } + /** * Configures Night display to use a custom schedule. * diff --git a/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java index 27f87aae35bb..a7ef5e0afc0e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java @@ -19,6 +19,7 @@ package com.android.server.display.color; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.server.display.color.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY; +import static com.android.server.display.color.DisplayTransformManager.LEVEL_COLOR_MATRIX_REDUCE_BRIGHT_COLORS; import static com.android.server.display.color.DisplayTransformManager.PERSISTENT_PROPERTY_COMPOSITION_COLOR_MODE; import static com.android.server.display.color.DisplayTransformManager.PERSISTENT_PROPERTY_DISPLAY_COLOR; import static com.android.server.display.color.DisplayTransformManager.PERSISTENT_PROPERTY_SATURATION; @@ -51,12 +52,14 @@ public class DisplayTransformManagerTest { private MockitoSession mSession; private DisplayTransformManager mDtm; private float[] mNightDisplayMatrix; + private float[] mRbcMatrix; private HashMap<String, String> mSystemProperties; @Before public void setUp() { mDtm = new DisplayTransformManager(); mNightDisplayMatrix = mDtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY); + mRbcMatrix = mDtm.getColorMatrix(LEVEL_COLOR_MATRIX_REDUCE_BRIGHT_COLORS); mSession = ExtendedMockito.mockitoSession() .initMocks(this) @@ -81,7 +84,8 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_natural() { - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, -1); + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, mRbcMatrix, + Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo("0" /* managed */); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_SATURATION)) @@ -90,7 +94,8 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_boosted() { - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED, mNightDisplayMatrix, -1); + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED, mNightDisplayMatrix, mRbcMatrix, + Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo("0" /* managed */); @@ -100,7 +105,8 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_saturated() { - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED, mNightDisplayMatrix, -1); + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED, mNightDisplayMatrix, mRbcMatrix, + Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo("1" /* unmanaged */); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_SATURATION)) @@ -109,7 +115,8 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_automatic() { - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_AUTOMATIC, mNightDisplayMatrix, -1); + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_AUTOMATIC, mNightDisplayMatrix, mRbcMatrix, + Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo("2" /* enhanced */); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_SATURATION)) @@ -118,7 +125,7 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_vendor() { - mDtm.setColorMode(0x100, mNightDisplayMatrix, -1); + mDtm.setColorMode(0x100, mNightDisplayMatrix, mRbcMatrix, Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo(Integer.toString(0x100) /* pass-through */); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_SATURATION)) @@ -127,7 +134,7 @@ public class DisplayTransformManagerTest { @Test public void setColorMode_outOfBounds() { - mDtm.setColorMode(0x50, mNightDisplayMatrix, -1); + mDtm.setColorMode(0x50, mNightDisplayMatrix, mRbcMatrix, Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_DISPLAY_COLOR)) .isEqualTo(null); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_SATURATION)) @@ -141,7 +148,7 @@ public class DisplayTransformManagerTest { // Start with a known state, which we expect to remain unmodified SystemProperties.set(PERSISTENT_PROPERTY_COMPOSITION_COLOR_MODE, magicPropertyValue); - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, mRbcMatrix, Display.COLOR_MODE_INVALID); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_COMPOSITION_COLOR_MODE)) .isEqualTo(magicPropertyValue); @@ -155,7 +162,7 @@ public class DisplayTransformManagerTest { // Start with a known state, which we expect to get modified SystemProperties.set(PERSISTENT_PROPERTY_COMPOSITION_COLOR_MODE, magicPropertyValue); - mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, + mDtm.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL, mNightDisplayMatrix, mRbcMatrix, testPropertyValue); assertThat(mSystemProperties.get(PERSISTENT_PROPERTY_COMPOSITION_COLOR_MODE)) .isEqualTo(Integer.toString(testPropertyValue)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 1efe4707fc11..9e96800ca2e9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -18,6 +18,7 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_CAPABILITY_ALL; import static android.app.ActivityManager.PROCESS_CAPABILITY_BFSL; +import static android.app.ActivityManager.PROCESS_CAPABILITY_CPU_TIME; import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; @@ -40,6 +41,7 @@ import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING; import static android.app.ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -283,6 +285,15 @@ public class MockingOomAdjusterTests { } } + private static void assertNoCpuTime(ProcessRecord app) { + assertEquals(0, app.mState.getSetCapability() & PROCESS_CAPABILITY_CPU_TIME); + } + + private static void assertCpuTime(ProcessRecord app) { + assertEquals(PROCESS_CAPABILITY_CPU_TIME, + app.mState.getSetCapability() & PROCESS_CAPABILITY_CPU_TIME); + } + private static void assertBfsl(ProcessRecord app) { assertEquals(PROCESS_CAPABILITY_BFSL, app.mState.getSetCapability() & PROCESS_CAPABILITY_BFSL); @@ -661,6 +672,7 @@ public class MockingOomAdjusterTests { // SHORT_SERVICE, timed out already. s = ServiceRecord.newEmptyInstanceForTest(mService); s.appInfo = new ApplicationInfo(); + mProcessStateController.setStartRequested(s, true); s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -687,6 +699,51 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_bindingFromShortFgs() { + // Setting up a started short FGS within app1. + final ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(mService); + s.appInfo = new ApplicationInfo(); + mProcessStateController.setStartRequested(s, true); + s.isForeground = true; + s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; + mProcessStateController.setShortFgsInfo(s, SystemClock.uptimeMillis()); + + final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + mProcessStateController.setHostProcess(s, app); + mProcessStateController.setHasForegroundServices(app.mServices, true, + FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false); + mProcessStateController.startService(app.mServices, s); + app.mState.setLastTopTime(SystemClock.uptimeMillis() + - mService.mConstants.TOP_TO_FGS_GRACE_DURATION); + + final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID, + MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false)); + // App1 with short service binds to app2 + bindService(app2, app, null, null, 0, mock(IBinder.class)); + + setProcessesToLru(app, app2); + updateOomAdj(app); + + assertCpuTime(app); + assertCpuTime(app2); + + // Timeout the short FGS. + mProcessStateController.setShortFgsInfo(s, SystemClock.uptimeMillis() + - mService.mConstants.mShortFgsTimeoutDuration + - mService.mConstants.mShortFgsProcStateExtraWaitDuration); + mService.mServices.onShortFgsProcstateTimeout(s); + // mService is a mock, but this verifies that the timeout would trigger an update. + verify(mService).updateOomAdjLocked(app, OOM_ADJ_REASON_SHORT_FGS_TIMEOUT); + updateOomAdj(app); + + assertNoCpuTime(app); + assertNoCpuTime(app2); + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_OverlayUi() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); @@ -3142,11 +3199,19 @@ public class MockingOomAdjusterTests { assertEquals(true, app.getUidRecord().isSetAllowListed()); assertFreezeState(app, false); assertFreezeState(app2, false); + if (Flags.useCpuTimeCapability()) { + assertCpuTime(app); + assertCpuTime(app2); + } mProcessStateController.setUidTempAllowlistStateLSP(MOCKAPP_UID, false); assertEquals(false, app.getUidRecord().isSetAllowListed()); assertFreezeState(app, true); assertFreezeState(app2, true); + if (Flags.useCpuTimeCapability()) { + assertNoCpuTime(app); + assertNoCpuTime(app2); + } } @SuppressWarnings("GuardedBy") @@ -3171,6 +3236,11 @@ public class MockingOomAdjusterTests { assertFreezeState(app, false); assertFreezeState(app2, false); assertFreezeState(app3, false); + if (Flags.useCpuTimeCapability()) { + assertCpuTime(app); + assertCpuTime(app2); + assertCpuTime(app3); + } // Remove app1 from allowlist. mProcessStateController.setUidTempAllowlistStateLSP(MOCKAPP_UID, false); @@ -3179,6 +3249,11 @@ public class MockingOomAdjusterTests { assertFreezeState(app, true); assertFreezeState(app2, false); assertFreezeState(app3, false); + if (Flags.useCpuTimeCapability()) { + assertNoCpuTime(app); + assertCpuTime(app2); + assertCpuTime(app3); + } // Now remove app2 from allowlist. mProcessStateController.setUidTempAllowlistStateLSP(MOCKAPP2_UID, false); @@ -3187,6 +3262,11 @@ public class MockingOomAdjusterTests { assertFreezeState(app, true); assertFreezeState(app2, true); assertFreezeState(app3, true); + if (Flags.useCpuTimeCapability()) { + assertNoCpuTime(app); + assertNoCpuTime(app2); + assertNoCpuTime(app3); + } } @SuppressWarnings("GuardedBy") diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java index 2988c77703b7..7e052dcba3fd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -16,6 +16,7 @@ package com.android.server.am; +import static android.app.ActivityManager.PROCESS_CAPABILITY_CPU_TIME; import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE; import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; import static android.app.ActivityManager.PROCESS_STATE_CACHED_EMPTY; @@ -64,6 +65,8 @@ import android.content.pm.ServiceInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -326,6 +329,7 @@ public final class ServiceBindingOomAdjPolicyTest { @Test @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX) + @DisableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) public void testServiceDistinctBindingOomAdjShouldNotFreeze() throws Exception { // Enable the flags. mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); @@ -418,6 +422,7 @@ public final class ServiceBindingOomAdjPolicyTest { @Test @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX) + @DisableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) public void testServiceDistinctBindingOomAdjAllowOomManagement() throws Exception { // Enable the flags. mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); @@ -497,6 +502,7 @@ public final class ServiceBindingOomAdjPolicyTest { @Test @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX) + @DisableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) public void testServiceDistinctBindingOomAdjWaivePriority_propagateUnfreeze() throws Exception { // Enable the flags. mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); @@ -574,6 +580,50 @@ public final class ServiceBindingOomAdjPolicyTest { } @Test + @RequiresFlagsEnabled({ + Flags.FLAG_UNFREEZE_BIND_POLICY_FIX, + Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY + }) + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testServiceDistinctBindingOomAdj_propagateCpuTimeCapability() throws Exception { + // Note that PROCESS_CAPABILITY_CPU_TIME is special and should be propagated even when + // BIND_INCLUDE_CAPABILITIES is not present. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_CPU_TIME, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE, + PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHasForegroundServices, + BIND_AUTO_CREATE, + atLeastOnce(), atLeastOnce()); + + // BIND_WAIVE_PRIORITY should not affect propagation of capability CPU_TIME + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_CPU_TIME, + TEST_APP1_NAME, + this::setHasForegroundServices, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY, + atLeastOnce(), atLeastOnce()); + + // If both process have the capability, the bind should not need an update but the unbind + // is not safe to skip. + // Note that this check can fail on future changes that are not related to + // PROCESS_CAPABILITY_CPU_TIME and trigger updates but this is important to ensure + // efficiency of OomAdjuster. + performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, + PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_CPU_TIME, TEST_APP1_NAME, + this::setHomeProcess, + TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME, HOME_APP_ADJ, + PROCESS_CAPABILITY_CPU_TIME, TEST_APP2_NAME, TEST_SERVICE2_NAME, + this::setHomeProcess, + BIND_AUTO_CREATE, + never(), atLeastOnce()); + } + + @Test @RequiresFlagsDisabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX) public void testServiceDistinctBindingOomAdjWaivePriority() throws Exception { // Enable the flags. @@ -624,6 +674,9 @@ public final class ServiceBindingOomAdjPolicyTest { // Enable the flags. mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY); + // Note that some capabilities like PROCESS_CAPABILITY_CPU_TIME are special and propagated + // regardless of BIND_INCLUDE_CAPABILITIES. We don't test for them here. + // Verify that there should be 0 oom adj update // because we didn't specify the "BIND_INCLUDE_CAPABILITIES" performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID, diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java index 428e438443cd..4b2e850d08e7 100644 --- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -188,6 +188,7 @@ public class HintManagerServiceTest { mSupportInfo.headroom.cpuMinIntervalMillis = 2000; mSupportInfo.headroom.isGpuSupported = true; mSupportInfo.headroom.gpuMinIntervalMillis = 2000; + mSupportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo(); return mSupportInfo; } diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 8024915692aa..71a2651a0f14 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -16,7 +16,12 @@ package com.android.server; -import static com.android.server.GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; +import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP; +import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; + +import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -24,19 +29,27 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.app.StatusBarManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Resources; import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; import android.test.mock.MockContentResolver; import android.testing.TestableLooper; @@ -55,6 +68,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -62,6 +76,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Unit tests for {@link GestureLauncherService}. @@ -90,9 +106,32 @@ public class GestureLauncherServiceTest { private @Mock TelecomManager mTelecomManager; private @Mock MetricsLogger mMetricsLogger; @Mock private UiEventLogger mUiEventLogger; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; private MockContentResolver mContentResolver; private GestureLauncherService mGestureLauncherService; + private Context mInstrumentationContext = + InstrumentationRegistry.getInstrumentation().getContext(); + + private static final String LAUNCH_TEST_WALLET_ACTION = "LAUNCH_TEST_WALLET_ACTION"; + private static final String LAUNCH_FALLBACK_ACTION = "LAUNCH_FALLBACK_ACTION"; + private PendingIntent mGesturePendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_TEST_WALLET_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + private PendingIntent mFallbackPendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_FALLBACK_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void oneTimeInitialization() { if (Looper.myLooper() == null) { @@ -115,9 +154,49 @@ public class GestureLauncherServiceTest { when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager); when(mTelecomManager.createLaunchEmergencyDialerIntent(null)).thenReturn(new Intent()); + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(true); + + mGestureLauncherService = + new GestureLauncherService( + mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger); + + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } + + private WalletLaunchedReceiver registerWalletLaunchedReceiver(String action) { + IntentFilter filter = new IntentFilter(action); + WalletLaunchedReceiver receiver = new WalletLaunchedReceiver(); + mInstrumentationContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED); + return receiver; + } + + /** + * A simple {@link BroadcastReceiver} implementation that counts down a {@link CountDownLatch} + * when a matching message is received + */ + private static final class WalletLaunchedReceiver extends BroadcastReceiver { + private static final int TIMEOUT_SECONDS = 3; - mGestureLauncherService = new GestureLauncherService(mContext, mMetricsLogger, - mUiEventLogger); + private final CountDownLatch mLatch; + + WalletLaunchedReceiver() { + mLatch = new CountDownLatch(1); + } + + @Override + public void onReceive(Context context, Intent intent) { + mLatch.countDown(); + context.unregisterReceiver(this); + } + + Boolean waitUntilShown() { + try { + return mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + } } @Test @@ -134,37 +213,123 @@ public class GestureLauncherServiceTest { @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(0); - assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( - mContext, FAKE_USER_ID)); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(0); + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test public void testIsEmergencyGestureSettingEnabled_settingDisabled() { withEmergencyGestureEnabledConfigValue(true); withEmergencyGestureEnabledSettingValue(false); @@ -245,12 +410,9 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_firstPowerDownCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); - long eventTime = INITIAL_EVENT_TIME_MILLIS + - CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + long eventTime = INITIAL_EVENT_TIME_MILLIS + POWER_DOUBLE_TAP_MAX_TIME_MS - 1; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); boolean interactive = true; @@ -284,8 +446,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -298,7 +464,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -309,7 +475,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -329,8 +495,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -343,7 +513,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -354,7 +524,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -401,7 +571,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -422,10 +592,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -437,7 +604,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -450,7 +617,7 @@ public class GestureLauncherServiceTest { verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected( StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP); verify(mMetricsLogger) - .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); + .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); verify(mUiEventLogger, times(1)) .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); @@ -470,15 +637,145 @@ public class GestureLauncherServiceTest { } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void + testInterceptPowerKeyDown_fiveInboundPresses_walletAndEmergencyEnabled_bothLaunch() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableEmergencyGesture(); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertTrue(receiver.waitUntilShown()); + + // Presses 3 and 4 should not trigger any gesture + for (int i = 0; i < 2; i++) { + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + } + + // Fifth button press should trigger the emergency flow + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + verify(mUiEventLogger, times(1)) + .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_intervalInBoundsWalletPowerGesture() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + assertTrue(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletGestureOn_quickAccessWalletServiceUnavailable() { + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false); + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + public void testInterceptPowerKeyDown_walletGestureOn_userSetupIncomplete() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + withUserSetupCompleteValue(false); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_nullPendingIntent() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertFalse(gestureReceiver.waitUntilShown()); + assertTrue(fallbackReceiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_intervalOutOfBounds() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(gestureReceiver.waitUntilShown()); + assertFalse(fallbackReceiver.waitUntilShown()); + } + + @Test public void testInterceptPowerKeyDown_fiveInboundPresses_cameraAndEmergencyEnabled_bothLaunch() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - withEmergencyGestureEnabledConfigValue(true); - withEmergencyGestureEnabledSettingValue(true); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - mGestureLauncherService.updateEmergencyGestureEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); + enableEmergencyGesture(); // First button press does nothing long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -491,7 +788,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 2nd button triggers camera eventTime += interval; @@ -507,7 +804,7 @@ public class GestureLauncherServiceTest { verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected( StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP); verify(mMetricsLogger) - .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); + .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); verify(mUiEventLogger, times(1)) .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); @@ -580,7 +877,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture (camera gesture disabled) for (int i = 0; i < 3; i++) { eventTime += interval; @@ -634,7 +931,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture, but intercepts action. for (int i = 0; i < 3; i++) { eventTime += interval; @@ -737,7 +1034,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -765,7 +1062,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -916,7 +1213,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT, IGNORED_META_STATE, IGNORED_DEVICE_ID, IGNORED_SCANCODE, @@ -947,9 +1244,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -962,7 +1257,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -973,7 +1268,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -995,9 +1290,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1009,7 +1302,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1020,7 +1313,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1042,9 +1335,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1067,7 +1358,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1087,8 +1378,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1101,7 +1396,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1112,7 +1407,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1146,7 +1441,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1156,7 +1451,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1202,7 +1497,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1223,10 +1518,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1238,7 +1530,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1250,7 +1542,7 @@ public class GestureLauncherServiceTest { verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected( StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP); verify(mMetricsLogger) - .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); + .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); verify(mUiEventLogger, times(1)) .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); @@ -1272,9 +1564,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1287,7 +1577,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1298,7 +1588,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1332,7 +1622,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1343,7 +1633,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1365,9 +1655,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1390,7 +1678,7 @@ public class GestureLauncherServiceTest { assertFalse(outLaunched.value); verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); verify(mUiEventLogger, never()).log(any()); final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class); @@ -1414,7 +1702,7 @@ public class GestureLauncherServiceTest { * @return last event time. */ private long triggerEmergencyGesture() { - return triggerEmergencyGesture(CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1); + return triggerEmergencyGesture(POWER_DOUBLE_TAP_MAX_TIME_MS - 1); } /** @@ -1473,6 +1761,27 @@ public class GestureLauncherServiceTest { .thenReturn(enableConfigValue); } + private void withDoubleTapPowerEnabledConfigValue(boolean enable) { + when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled)) + .thenReturn(enable); + } + + private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + enable ? 1 : 0, + UserHandle.USER_CURRENT); + } + + private void withDefaultDoubleTapPowerGestureAction(int action) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + action, + UserHandle.USER_CURRENT); + } + private void withEmergencyGestureEnabledConfigValue(boolean enableConfigValue) { when(mResources.getBoolean( com.android.internal.R.bool.config_emergencyGestureEnabled)) @@ -1510,4 +1819,72 @@ public class GestureLauncherServiceTest { userSetupCompleteValue, UserHandle.USER_CURRENT); } + + private void setUpGetGestureTargetActivityPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.GesturePendingIntentCallback callback = + (QuickAccessWalletClient.GesturePendingIntentCallback) + invocation.getArguments()[1]; + callback.onGesturePendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getGestureTargetActivityPendingIntent(any(), any()); + } + + private void setUpWalletFallbackPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.WalletPendingIntentCallback callback = + (QuickAccessWalletClient.WalletPendingIntentCallback) + invocation.getArguments()[1]; + callback.onWalletPendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getWalletPendingIntent(any(), any()); + } + + private void enableWalletGesture() { + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerEnabledConfigValue(true); + + mGestureLauncherService.updateWalletDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableEmergencyGesture() { + withEmergencyGestureEnabledConfigValue(true); + withEmergencyGestureEnabledSettingValue(true); + mGestureLauncherService.updateEmergencyGestureEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableCameraGesture() { + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } + mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues( + long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) { + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = + mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, outLaunched); + assertEquals(intercepted, expectedIntercept); + assertEquals(outLaunched.value, expectedOutLaunchedValue); + } } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java index d69e47684f8b..9b878b349618 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; +import static org.junit.Assert.fail; import android.app.ActivityManager; import android.app.LocaleManager; @@ -26,6 +27,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.multiuser.Flags; +import android.os.Bundle; import android.os.LocaleList; import android.os.SystemClock; import android.os.UserHandle; @@ -261,6 +263,162 @@ public final class UserManagerCacheTest { assertThat(um.getUserName()).isEqualTo(newName); } + + @MediumTest + @Test + public void testDefaultRestrictionsApplied() throws Exception { + final UserInfo userInfo = mUserManager.createUser("Useroid", + UserManager.USER_TYPE_FULL_SECONDARY, 0); + mUsersToRemove.add(userInfo.id); + final UserTypeDetails userTypeDetails = + UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_FULL_SECONDARY); + final Bundle expectedRestrictions = userTypeDetails.getDefaultRestrictions(); + // Note this can fail if DO unset those restrictions. + for (String restriction : expectedRestrictions.keySet()) { + if (expectedRestrictions.getBoolean(restriction)) { + assertThat(mUserManager.hasUserRestriction(restriction, UserHandle.of(userInfo.id))) + .isTrue(); + // Test cached value + assertThat(mUserManager.hasUserRestriction(restriction, UserHandle.of(userInfo.id))) + .isTrue(); + } + } + } + + @MediumTest + @Test + public void testSetDefaultGuestRestrictions() { + final Bundle origRestrictions = mUserManager.getDefaultGuestRestrictions(); + try { + final boolean isFunDisallowed = origRestrictions.getBoolean(UserManager.DISALLOW_FUN, + false); + final UserInfo guest1 = mUserManager.createUser("Guest 1", UserInfo.FLAG_GUEST); + assertThat(guest1).isNotNull(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest1.getUserHandle())).isEqualTo(isFunDisallowed); + removeUser(guest1.id, true); + // Cache return false after user was removed + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest1.getUserHandle())).isFalse(); + + Bundle restrictions = new Bundle(); + restrictions.putBoolean(UserManager.DISALLOW_FUN, !isFunDisallowed); + mUserManager.setDefaultGuestRestrictions(restrictions); + UserInfo guest2 = mUserManager.createUser("Guest 2", UserInfo.FLAG_GUEST); + assertThat(guest2).isNotNull(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest2.getUserHandle())).isNotEqualTo(isFunDisallowed); + removeUser(guest2.id, true); + assertThat(mUserManager.getUserInfo(guest2.id)).isNull(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest2.getUserHandle())).isFalse(); + } finally { + mUserManager.setDefaultGuestRestrictions(origRestrictions); + } + } + + @MediumTest + @Test + public void testCacheInvalidatedAfterUserAddedOrRemoved() { + final Bundle origRestrictions = mUserManager.getDefaultGuestRestrictions(); + try { + final boolean isFunDisallowed = origRestrictions.getBoolean(UserManager.DISALLOW_FUN, + false); + final UserInfo guest1 = mUserManager.createUser("Guest 1", UserInfo.FLAG_GUEST); + assertThat(guest1).isNotNull(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest1.getUserHandle())).isEqualTo(isFunDisallowed); + removeUser(guest1.id, true); + + Bundle restrictions = new Bundle(); + restrictions.putBoolean(UserManager.DISALLOW_FUN, !isFunDisallowed); + mUserManager.setDefaultGuestRestrictions(restrictions); + int latest_id = guest1.id; + // Cache removed id and few next ids. + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + UserHandle.of(latest_id))).isFalse(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + UserHandle.of(latest_id + 1))).isFalse(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + UserHandle.of(latest_id + 2))).isFalse(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + UserHandle.of(latest_id + 3))).isFalse(); + + UserInfo guest2 = mUserManager.createUser("Guest 2", UserInfo.FLAG_GUEST); + assertThat(guest2).isNotNull(); + // Cache was invalidated after user was added + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest2.getUserHandle())).isTrue(); + removeUser(guest2.id, true); + assertThat(mUserManager.getUserInfo(guest2.id)).isNull(); + // Cache was invalidated after user was removed + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + guest2.getUserHandle())).isFalse(); + } finally { + mUserManager.setDefaultGuestRestrictions(origRestrictions); + } + } + + + @MediumTest + @Test + public void testAddRemoveUsersAndRestrictions() { + try { + final UserInfo userInfo = mUserManager.createUser("Useroid", + UserManager.USER_TYPE_FULL_SECONDARY, 0); + mUsersToRemove.add(userInfo.id); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + userInfo.getUserHandle())).isFalse(); + mUserManager.setUserRestriction(UserManager.DISALLOW_FUN, true, + userInfo.getUserHandle()); + + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + userInfo.getUserHandle())).isTrue(); + removeUser(userInfo.id, true); + assertThat(mUserManager.getUserSerialNumber(userInfo.id)).isEqualTo(-1); + assertThat(mUserManager.getUserInfo(userInfo.id)).isNull(); + assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN, + userInfo.getUserHandle())).isFalse(); + } catch (java.lang.Exception e) { + } + } + + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + + @MediumTest + @Test + public void testDefaultUserRestrictionsForPrivateProfile() { + assumeTrue(mUserManager.canAddPrivateProfile()); + final int currentUserId = ActivityManager.getCurrentUser(); + UserInfo privateProfileInfo = null; + try { + privateProfileInfo = mUserManager.createProfileForUser( + "Private", UserManager.USER_TYPE_PROFILE_PRIVATE, 0, currentUserId, null); + assertThat(privateProfileInfo).isNotNull(); + } catch (Exception e) { + fail("Creation of private profile failed due to " + e.getMessage()); + } + assertDefaultPrivateProfileRestrictions(privateProfileInfo.getUserHandle()); + // Assert cached values + assertDefaultPrivateProfileRestrictions(privateProfileInfo.getUserHandle()); + } + + private void assertDefaultPrivateProfileRestrictions(UserHandle userHandle) { + Bundle defaultPrivateProfileRestrictions = + UserTypeFactory.getDefaultPrivateProfileRestrictions(); + for (String restriction : defaultPrivateProfileRestrictions.keySet()) { + assertThat(mUserManager.hasUserRestrictionForUser(restriction, userHandle)).isTrue(); + } + } + private void assumeManagedUsersSupported() { // In Automotive, if headless system user is enabled, a managed user cannot be created // under a primary user. @@ -270,9 +428,23 @@ public final class UserManagerCacheTest { } private void removeUser(int userId) { + removeUser(userId, false); + } + + private void removeUser(int userId, boolean waitForCompleteRemoval) { mUserManager.removeUser(userId); mUserRemovalWaiter.waitFor(userId); mUsersToRemove.remove(userId); + if (waitForCompleteRemoval) { + int serialNumber = mUserManager.getUserSerialNumber(userId); + int timeout = REMOVE_USER_TIMEOUT_SECONDS * 5; // called every 200ms + // Wait for the user to be removed from memory + while (serialNumber > 0 && timeout > 0) { + sleep(200); + timeout--; + serialNumber = mUserManager.getUserSerialNumber(userId); + } + } } private boolean isAutomotive() { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 661d07e09f99..5cb741fe81d4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2472,11 +2472,14 @@ public class ActivityRecordTests extends WindowTestsBase { final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); assertEquals(0, activity.getChildCount()); - final WindowState win1 = createWindow(null, TYPE_APPLICATION, activity, "win1"); - final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, activity, - "startingWin"); - final WindowState baseWin = createWindow(null, TYPE_BASE_APPLICATION, activity, "baseWin"); - final WindowState win4 = createWindow(null, TYPE_APPLICATION, activity, "win4"); + final WindowState win1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken( + activity).build(); + final WindowState startingWin = newWindowBuilder("startingWin", + TYPE_APPLICATION_STARTING).setWindowToken(activity).build(); + final WindowState baseWin = newWindowBuilder("baseWin", + TYPE_BASE_APPLICATION).setWindowToken(activity).build(); + final WindowState win4 = newWindowBuilder("win4", TYPE_APPLICATION).setWindowToken( + activity).build(); // Should not contain the windows that were added above. assertEquals(4, activity.getChildCount()); @@ -2499,14 +2502,17 @@ public class ActivityRecordTests extends WindowTestsBase { final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); assertNull(activity.findMainWindow()); - final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "window1"); - final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, activity, "window11"); - final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, activity, "window12"); + final WindowState window1 = newWindowBuilder("window1", + TYPE_BASE_APPLICATION).setWindowToken(activity).build(); + final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent( + window1).setWindowToken(activity).build(); + final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent( + window1).setWindowToken(activity).build(); assertEquals(window1, activity.findMainWindow()); window1.mAnimatingExit = true; assertEquals(window1, activity.findMainWindow()); - final WindowState window2 = createWindow(null, TYPE_APPLICATION_STARTING, activity, - "window2"); + final WindowState window2 = newWindowBuilder("window2", + TYPE_APPLICATION_STARTING).setWindowToken(activity).build(); assertEquals(window2, activity.findMainWindow()); activity.removeImmediately(); } @@ -2651,8 +2657,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testStuckExitingWindow() { - final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW, - "closingWindow"); + final WindowState closingWindow = newWindowBuilder("closingWindow", + FIRST_APPLICATION_WINDOW).build(); closingWindow.mAnimatingExit = true; closingWindow.mRemoveOnExit = true; closingWindow.mActivityRecord.commitVisibility( @@ -3313,7 +3319,7 @@ public class ActivityRecordTests extends WindowTestsBase { @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); makeWindowVisibleAndDrawn(app, mImeWindow); mDisplayContent.setImeLayeringTarget(app); mDisplayContent.setImeInputTarget(app); @@ -3341,7 +3347,7 @@ public class ActivityRecordTests extends WindowTestsBase { @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( mImeWindow, null, null); @@ -3385,8 +3391,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest() throws RemoteException { - final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1"); - final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2"); + final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build(); + final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( mImeWindow, null, null); @@ -3430,7 +3436,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testImeInsetsFrozenFlag_multiWindowActivities() { final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken( + imeToken).build(); makeWindowVisibleAndDrawn(ime); // Create a split-screen root task with activity1 and activity 2. @@ -3451,8 +3458,10 @@ public class ActivityRecordTests extends WindowTestsBase { activity1.mImeInsetsFrozenUntilStartInput = true; activity2.mImeInsetsFrozenUntilStartInput = true; - final WindowState app1 = createWindow(null, TYPE_APPLICATION, activity1, "app1"); - final WindowState app2 = createWindow(null, TYPE_APPLICATION, activity2, "app2"); + final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken( + activity1).build(); + final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken( + activity2).build(); makeWindowVisibleAndDrawn(app1, app2); final InsetsStateController controller = mDisplayContent.getInsetsStateController(); @@ -3481,7 +3490,7 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); makeWindowVisibleAndDrawn(app); // Put the activity in close transition. @@ -3508,7 +3517,7 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testInClosingAnimation_visibilityCommitted_hideSurface() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); makeWindowVisibleAndDrawn(app); app.mActivityRecord.prepareSurfaces(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index f339d292ed82..429a396ad997 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -92,7 +92,7 @@ import java.util.concurrent.TimeUnit; * Tests for the {@link DragDropController} class. * * Build/Install/Run: - * atest WmTests:DragDropControllerTests + * atest WmTests:DragDropControllerTests */ @SmallTest @Presubmit @@ -146,12 +146,12 @@ public class DragDropControllerTests extends WindowTestsBase { */ private WindowState createDropTargetWindow(String name, int ownerId) { final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build(); - final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task) - .setUseProcess(mProcess).build(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess( + mProcess).build(); // Use a new TestIWindow so we don't collect events for other windows - final WindowState window = createWindow( - null, TYPE_BASE_APPLICATION, activity, name, ownerId, false, new TestIWindow()); + final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, activity, name, + ownerId, false, new TestIWindow()); InputChannel channel = new InputChannel(); window.openInputChannel(channel); window.mHasSurface = true; @@ -173,12 +173,11 @@ public class DragDropControllerTests extends WindowTestsBase { @Before public void setUp() throws Exception { mTarget = new TestDragDropController(mWm, mWm.mH.getLooper()); - mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", - TEST_PID, TEST_UID); + mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID); mWindow = createDropTargetWindow("Drag test window", 0); doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0); - when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), - any(IBinder.class))).thenReturn(true); + when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn( + true); mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow); } @@ -286,16 +285,15 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify the start-drag event is sent for the local and global intercept window // but not the other window assertTrue(nonLocalWindowDragEvents.isEmpty()); - assertTrue(localWindowDragEvents.get(0).getAction() - == ACTION_DRAG_STARTED); + assertTrue(localWindowDragEvents.get(0).getAction() == ACTION_DRAG_STARTED); assertTrue(globalInterceptWindowDragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify that only the global intercept window receives the clip data with the // resolved activity info for the drag assertNull(localWindowDragEvents.get(0).getClipData()); - assertTrue(globalInterceptWindowDragEvents.get(0).getClipData() - .willParcelWithActivityInfo()); + assertTrue(globalInterceptWindowDragEvents.get( + 0).getClipData().willParcelWithActivityInfo()); mTarget.reportDropWindow(globalInterceptWindow.mInputChannelToken, 0, 0); mTarget.handleMotionEvent(false, 0, 0); @@ -330,9 +328,8 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify the start-drag event has the drag flags final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); - assertTrue(dragEvent.getDragFlags() == - (View.DRAG_FLAG_GLOBAL - | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); + assertTrue(dragEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL + | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); try { mTarget.mDeferDragStateClosed = true; @@ -340,9 +337,8 @@ public class DragDropControllerTests extends WindowTestsBase { // // Verify the drop event does not have the drag flags mTarget.handleMotionEvent(false, 0, 0); final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); - assertTrue(dropEvent.getDragFlags() == - (View.DRAG_FLAG_GLOBAL - | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); + assertTrue(dropEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL + | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); mTarget.reportDropResult(iwindow, true); } finally { @@ -385,16 +381,15 @@ public class DragDropControllerTests extends WindowTestsBase { data.putExtra(Intent.EXTRA_USER, user); } final ClipData clipData = new ClipData( - new ClipDescription("drag", new String[] { - MIMETYPE_APPLICATION_ACTIVITY}), + new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_ACTIVITY}), new ClipData.Item(data)); return clipData; } @Test public void testValidateAppShortcutArguments() { - doReturn(PERMISSION_GRANTED).when(mWm.mContext) - .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS)); + doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission( + eq(START_TASKS_FROM_RECENTS)); final Session session = createTestSession(mAtm); try { session.validateAndResolveDragMimeTypeExtras( @@ -414,8 +409,8 @@ public class DragDropControllerTests extends WindowTestsBase { } try { session.validateAndResolveDragMimeTypeExtras( - createClipDataForShortcut("test_package", "test_shortcut_id", null), - TEST_UID, TEST_PID, TEST_PACKAGE); + createClipDataForShortcut("test_package", "test_shortcut_id", null), TEST_UID, + TEST_PID, TEST_PACKAGE); fail("Expected failure without package name"); } catch (IllegalArgumentException e) { // Expected failure @@ -424,8 +419,8 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testValidateProfileAppShortcutArguments_notCallingUid() { - doReturn(PERMISSION_GRANTED).when(mWm.mContext) - .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS)); + doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission( + eq(START_TASKS_FROM_RECENTS)); final Session session = createTestSession(mAtm); final ShortcutServiceInternal shortcutService = mock(ShortcutServiceInternal.class); final Intent[] shortcutIntents = new Intent[1]; @@ -438,10 +433,9 @@ public class DragDropControllerTests extends WindowTestsBase { ArgumentCaptor<Integer> callingUser = ArgumentCaptor.forClass(Integer.class); session.validateAndResolveDragMimeTypeExtras( createClipDataForShortcut("test_package", "test_shortcut_id", - mock(UserHandle.class)), - TEST_PROFILE_UID, TEST_PID, TEST_PACKAGE); - verify(shortcutService).createShortcutIntents(callingUser.capture(), any(), - any(), any(), anyInt(), anyInt(), anyInt()); + mock(UserHandle.class)), TEST_PROFILE_UID, TEST_PID, TEST_PACKAGE); + verify(shortcutService).createShortcutIntents(callingUser.capture(), any(), any(), any(), + anyInt(), anyInt(), anyInt()); assertTrue(callingUser.getValue() == UserHandle.getUserId(TEST_PROFILE_UID)); } @@ -458,20 +452,19 @@ public class DragDropControllerTests extends WindowTestsBase { data.putExtra(Intent.EXTRA_USER, user); } final ClipData clipData = new ClipData( - new ClipDescription("drag", new String[] { - MIMETYPE_APPLICATION_SHORTCUT}), + new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_SHORTCUT}), new ClipData.Item(data)); return clipData; } @Test public void testValidateAppTaskArguments() { - doReturn(PERMISSION_GRANTED).when(mWm.mContext) - .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS)); + doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission( + eq(START_TASKS_FROM_RECENTS)); final Session session = createTestSession(mAtm); try { final ClipData clipData = new ClipData( - new ClipDescription("drag", new String[] { MIMETYPE_APPLICATION_TASK }), + new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_TASK}), new ClipData.Item(new Intent())); session.validateAndResolveDragMimeTypeExtras(clipData, TEST_UID, TEST_PID, @@ -496,8 +489,8 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testValidateFlagsWithPermission() { - doReturn(PERMISSION_GRANTED).when(mWm.mContext) - .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS)); + doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission( + eq(START_TASKS_FROM_RECENTS)); final Session session = createTestSession(mAtm); try { session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, @@ -533,8 +526,8 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify the DRAG_ENDED event does NOT include the drag surface final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); - assertTrue(dragEvents.get(dragEvents.size() - 1).getAction() - == ACTION_DRAG_ENDED); + assertTrue( + dragEvents.get(dragEvents.size() - 1).getAction() == ACTION_DRAG_ENDED); assertTrue(dropEvent.getDragSurface() == null); }); } @@ -564,8 +557,8 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify the DRAG_ENDED event includes the drag surface final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); - assertTrue(dragEvents.get(dragEvents.size() - 1).getAction() - == ACTION_DRAG_ENDED); + assertTrue( + dragEvents.get(dragEvents.size() - 1).getAction() == ACTION_DRAG_ENDED); assertTrue(dropEvent.getDragSurface() != null); }); } @@ -591,18 +584,18 @@ public class DragDropControllerTests extends WindowTestsBase { final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, ClipData.newPlainText("label", "Test"), () -> { - // Trigger an unhandled drop and verify the global drag listener was called - mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); - mTarget.reportDropResult(mWindow.mClient, false); - mTarget.onUnhandledDropCallback(true); - mToken = null; - try { - verify(listener, times(1)).onUnhandledDrop(any(), any()); - } catch (RemoteException e) { - fail("Failed to verify unhandled drop: " + e); - } - }); + // Trigger an unhandled drop and verify the global drag listener was called + mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.reportDropResult(mWindow.mClient, false); + mTarget.onUnhandledDropCallback(true); + mToken = null; + try { + verify(listener, times(1)).onUnhandledDrop(any(), any()); + } catch (RemoteException e) { + fail("Failed to verify unhandled drop: " + e); + } + }); } @Test @@ -615,17 +608,17 @@ public class DragDropControllerTests extends WindowTestsBase { final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, ClipData.newPlainText("label", "Test"), () -> { - // Trigger an unhandled drop and verify the global drag listener was called - mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); - mTarget.onUnhandledDropCallback(true); - mToken = null; - try { - verify(listener, times(1)).onUnhandledDrop(any(), any()); - } catch (RemoteException e) { - fail("Failed to verify unhandled drop: " + e); - } - }); + // Trigger an unhandled drop and verify the global drag listener was called + mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.onUnhandledDropCallback(true); + mToken = null; + try { + verify(listener, times(1)).onUnhandledDrop(any(), any()); + } catch (RemoteException e) { + fail("Failed to verify unhandled drop: " + e); + } + }); } @Test @@ -636,18 +629,17 @@ public class DragDropControllerTests extends WindowTestsBase { doReturn(mock(Binder.class)).when(listener).asBinder(); mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; - startDrag(View.DRAG_FLAG_GLOBAL, - ClipData.newPlainText("label", "Test"), () -> { - // Trigger an unhandled drop and verify the global drag listener was not called - mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); - mToken = null; - try { - verify(listener, never()).onUnhandledDrop(any(), any()); - } catch (RemoteException e) { - fail("Failed to verify unhandled drop: " + e); - } - }); + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + // Trigger an unhandled drop and verify the global drag listener was not called + mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mToken = null; + try { + verify(listener, never()).onUnhandledDrop(any(), any()); + } catch (RemoteException e) { + fail("Failed to verify unhandled drop: " + e); + } + }); } @Test @@ -660,20 +652,22 @@ public class DragDropControllerTests extends WindowTestsBase { final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, ClipData.newPlainText("label", "Test"), () -> { - // Trigger an unhandled drop and verify the global drag listener was called - mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + // Trigger an unhandled drop and verify the global drag listener was called + mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); - // Verify that the unhandled drop listener callback timeout has been scheduled - final Handler handler = mTarget.getHandler(); - assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + // Verify that the unhandled drop listener callback timeout has been scheduled + final Handler handler = mTarget.getHandler(); + assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); - // Force trigger the timeout and verify that it actually cleans up the drag & timeout - handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); - assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); - assertFalse(mTarget.dragDropActiveLocked()); - mToken = null; - }); + // Force trigger the timeout and verify that it actually cleans up the drag & + // timeout + handler.handleMessage( + Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT)); + assertFalse(mTarget.dragDropActiveLocked()); + mToken = null; + }); } private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { @@ -690,15 +684,13 @@ public class DragDropControllerTests extends WindowTestsBase { private void startDrag(int flag, ClipData data, Runnable r) { final SurfaceSession appSession = new SurfaceSession(); try { - final SurfaceControl surface = new SurfaceControl.Builder(appSession) - .setName("drag surface") - .setBufferSize(100, 100) - .setFormat(PixelFormat.TRANSLUCENT) - .build(); + final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName( + "drag surface").setBufferSize(100, 100).setFormat( + PixelFormat.TRANSLUCENT).build(); assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder())); - mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, - flag, surface, 0, 0, 0, 0, 0, 0, 0, data); + mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, + 0, 0, data); assertNotNull(mToken); r.run(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index b27025c34d6b..b61dada809d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -89,6 +89,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.service.voice.IVoiceInteractionSession; +import android.tools.function.Supplier; import android.util.MergedConfiguration; import android.util.SparseArray; import android.view.Display; @@ -1804,6 +1805,124 @@ public class WindowTestsBase extends SystemServiceTestsBase { } } + protected WindowStateBuilder newWindowBuilder(String name, int type) { + return new WindowStateBuilder(name, type, mWm, mDisplayContent, mIWindow, + this::getTestSession, this::createWindowToken); + } + + /** + * Builder for creating new window. + */ + protected static class WindowStateBuilder { + private final String mName; + private final int mType; + private final WindowManagerService mWm; + private final DisplayContent mDefaultTargetDisplay; + private final Supplier<WindowToken, Session> mSessionSupplier; + private final WindowTokenCreator mWindowTokenCreator; + + private int mActivityType = ACTIVITY_TYPE_STANDARD; + private IWindow mClientWindow; + private boolean mOwnerCanAddInternalSystemWindow = false; + private int mOwnerId = 0; + private WindowState mParent; + private DisplayContent mTargetDisplay; + private int mWindowingMode = WINDOWING_MODE_FULLSCREEN; + private WindowToken mWindowToken; + + WindowStateBuilder(String name, int type, WindowManagerService windowManagerService, + DisplayContent dc, IWindow iWindow, Supplier<WindowToken, Session> sessionSupplier, + WindowTokenCreator windowTokenCreator) { + mName = name; + mType = type; + mClientWindow = iWindow; + mDefaultTargetDisplay = dc; + mSessionSupplier = sessionSupplier; + mWindowTokenCreator = windowTokenCreator; + mWm = windowManagerService; + } + + WindowStateBuilder setActivityType(int activityType) { + mActivityType = activityType; + return this; + } + + WindowStateBuilder setClientWindow(IWindow clientWindow) { + mClientWindow = clientWindow; + return this; + } + + WindowStateBuilder setDisplay(DisplayContent displayContent) { + mTargetDisplay = displayContent; + return this; + } + + WindowStateBuilder setOwnerCanAddInternalSystemWindow( + boolean ownerCanAddInternalSystemWindow) { + mOwnerCanAddInternalSystemWindow = ownerCanAddInternalSystemWindow; + return this; + } + + WindowStateBuilder setOwnerId(int ownerId) { + mOwnerId = ownerId; + return this; + } + + WindowStateBuilder setParent(WindowState parent) { + mParent = parent; + return this; + } + + WindowStateBuilder setWindowToken(WindowToken token) { + mWindowToken = token; + return this; + } + + WindowStateBuilder setWindowingMode(int windowingMode) { + mWindowingMode = windowingMode; + return this; + } + + WindowState build() { + SystemServicesTestRule.checkHoldsLock(mWm.mGlobalLock); + + final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(mType); + attrs.setTitle(mName); + attrs.packageName = "test"; + + assertFalse( + "targetDisplay shouldn't be specified together with windowToken, since" + + " windowToken will be derived from targetDisplay.", + mWindowToken != null && mTargetDisplay != null); + + if (mWindowToken == null) { + if (mTargetDisplay != null) { + mWindowToken = mWindowTokenCreator.createWindowToken(mTargetDisplay, + mWindowingMode, mActivityType, mType); + } else if (mParent != null) { + mWindowToken = mParent.mToken; + } else { + // Use default mDisplayContent as window token. + mWindowToken = mWindowTokenCreator.createWindowToken(mDefaultTargetDisplay, + mWindowingMode, mActivityType, mType); + } + } + + final WindowState w = new WindowState(mWm, mSessionSupplier.get(mWindowToken), + mClientWindow, mWindowToken, mParent, OP_NONE, attrs, VISIBLE, mOwnerId, + UserHandle.getUserId(mOwnerId), mOwnerCanAddInternalSystemWindow); + // TODO: Probably better to make this call in the WindowState ctor to avoid errors with + // adding it to the token... + mWindowToken.addWindow(w); + return w; + } + + interface WindowTokenCreator { + WindowToken createWindowToken(DisplayContent dc, int windowingMode, int activityType, + int type); + } + } + static class TestStartingWindowOrganizer extends WindowOrganizerTests.StubOrganizer { private final ActivityTaskManagerService mAtm; private final WindowManagerService mWMService; diff --git a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java index c508fa968b14..ce3cd29a3ce2 100644 --- a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java +++ b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java @@ -26,6 +26,7 @@ import android.util.Slog; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.server.audio.AudioService; +import com.android.server.usb.flags.Flags; import java.util.Arrays; @@ -211,6 +212,9 @@ public final class UsbAlsaDevice { mIsSelected[direction] = true; mState[direction] = 0; startJackDetect(); + if (direction == OUTPUT && Flags.maximizeUsbAudioVolumeWhenConnecting()) { + nativeSetVolume(mCardNum, 1.0f /*volume*/); + } updateWiredDeviceConnectionState(direction, true /*enable*/); } @@ -412,5 +416,7 @@ public final class UsbAlsaDevice { return result; } + + private native void nativeSetVolume(int card, float volume); } diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig index a2d0efd1d063..dfbd74c1f3e1 100644 --- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig +++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig @@ -21,3 +21,10 @@ flag { description: "This flag checks if phone is unlocked after boot" bug: "73654179" } + +flag { + name: "maximize_usb_audio_volume_when_connecting" + namespace: "usb" + description: "This flag maximizes the usb audio volume when it is connected" + bug: "245041322" +} |