diff options
115 files changed, 2543 insertions, 1134 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 071cc6555be7..e5c059ecbfb7 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -84,7 +84,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", - "android.xr.flags-aconfig-java", + "android.xr.flags-aconfig-java-export", "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", @@ -989,15 +989,22 @@ java_aconfig_library { // XR aconfig_declarations { name: "android.xr.flags-aconfig", - package: "android.xr", container: "system", + exportable: true, + package: "android.xr", srcs: ["core/java/android/content/pm/xr.aconfig"], } java_aconfig_library { - name: "android.xr.flags-aconfig-java", + name: "android.xr.flags-aconfig-java-export", aconfig_declarations: "android.xr.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], + min_sdk_version: "30", + mode: "exported", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], } // android.app diff --git a/api/OWNERS b/api/OWNERS index 965093c9ab38..f2bcf13d2d2e 100644 --- a/api/OWNERS +++ b/api/OWNERS @@ -9,4 +9,4 @@ per-file *.go,go.mod,go.work,go.work.sum = file:platform/build/soong:/OWNERS per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} # For metalava team to disable lint checks in platform -per-file Android.bp = aurimas@google.com,emberrose@google.com +per-file Android.bp = aurimas@google.com diff --git a/core/java/android/accessibilityservice/OWNERS b/core/java/android/accessibilityservice/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/android/accessibilityservice/OWNERS +++ b/core/java/android/accessibilityservice/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 82c746a8ad4c..b8c20bd97264 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2230,6 +2230,16 @@ public class ActivityOptions extends ComponentOptions { return mLaunchCookie; } + /** + * Set the ability for the current transition/animation to work cross-task. + * @param allowTaskOverride true to allow cross-task use, otherwise false. + * + * @hide + */ + public ActivityOptions setOverrideTaskTransition(boolean allowTaskOverride) { + this.mOverrideTaskTransition = allowTaskOverride; + return this; + } /** @hide */ public boolean getOverrideTaskTransition() { diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 91ad22f51345..24f8672c1e7c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -22,6 +22,7 @@ import static android.app.AppOpsManager.OP_LEGACY_STORAGE; import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_EXTERNAL_STORAGE; import static android.app.AppOpsManager.OP_READ_MEDIA_IMAGES; +import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.UserHandle.PER_USER_RANGE; @@ -44,6 +45,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.PendingIntent; +import android.app.PropertyInvalidatedCache; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; import android.content.Context; @@ -269,14 +271,15 @@ public class StorageManager { public static final int FLAG_STORAGE_SDK = IInstalld.FLAG_STORAGE_SDK; /** {@hide} */ - @IntDef(prefix = "FLAG_STORAGE_", value = { + @IntDef(prefix = "FLAG_STORAGE_", value = { FLAG_STORAGE_DE, FLAG_STORAGE_CE, FLAG_STORAGE_EXTERNAL, FLAG_STORAGE_SDK, }) @Retention(RetentionPolicy.SOURCE) - public @interface StorageFlags {} + public @interface StorageFlags { + } /** {@hide} */ public static final int FLAG_FOR_WRITE = 1 << 8; @@ -309,6 +312,44 @@ public class StorageManager { @GuardedBy("mDelegates") private final ArrayList<StorageEventListenerDelegate> mDelegates = new ArrayList<>(); + static record VolumeListQuery(int mUserId, String mPackageName, int mFlags) { + } + + private static final PropertyInvalidatedCache.QueryHandler<VolumeListQuery, StorageVolume[]> + sVolumeListQuery = new PropertyInvalidatedCache.QueryHandler<>() { + @androidx.annotation.Nullable + @Override + public StorageVolume[] apply(@androidx.annotation.NonNull VolumeListQuery query) { + final IStorageManager storageManager = IStorageManager.Stub.asInterface( + ServiceManager.getService("mount")); + if (storageManager == null) { + // negative results won't be cached, so we will just try again next time + return null; + } + try { + return storageManager.getVolumeList( + query.mUserId, query.mPackageName, query.mFlags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + }; + + // Generally, the userId and packageName parameters stay pretty constant, but flags may change + // regularly; we have observed some processes hitting 10+ variations. + private static final int VOLUME_LIST_CACHE_MAX = 16; + + private static final PropertyInvalidatedCache<VolumeListQuery, StorageVolume[]> + sVolumeListCache = new PropertyInvalidatedCache<>( + new PropertyInvalidatedCache.Args(MODULE_SYSTEM).cacheNulls(false) + .api("getVolumeList").maxEntries(VOLUME_LIST_CACHE_MAX), "getVolumeList", + sVolumeListQuery); + + /** {@hide} */ + public static void invalidateVolumeListCache() { + sVolumeListCache.invalidateCache(); + } + private class StorageEventListenerDelegate extends IStorageEventListener.Stub { final Executor mExecutor; final StorageEventListener mListener; @@ -395,7 +436,8 @@ public class StorageManager { private class ObbActionListener extends IObbActionListener.Stub { @SuppressWarnings("hiding") - private SparseArray<ObbListenerDelegate> mListeners = new SparseArray<ObbListenerDelegate>(); + private SparseArray<ObbListenerDelegate> mListeners = + new SparseArray<ObbListenerDelegate>(); @Override public void onObbResult(String filename, int nonce, int status) { @@ -477,10 +519,10 @@ public class StorageManager { * * @param looper The {@link android.os.Looper} which events will be received on. * - * <p>Applications can get instance of this class by calling - * {@link android.content.Context#getSystemService(java.lang.String)} with an argument - * of {@link android.content.Context#STORAGE_SERVICE}. - * + * <p>Applications can get instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an + * argument + * of {@link android.content.Context#STORAGE_SERVICE}. * @hide */ @UnsupportedAppUsage @@ -488,15 +530,16 @@ public class StorageManager { mContext = context; mResolver = context.getContentResolver(); mLooper = looper; - mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount")); + mStorageManager = IStorageManager.Stub.asInterface( + ServiceManager.getServiceOrThrow("mount")); mAppOps = mContext.getSystemService(AppOpsManager.class); } /** * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage @@ -516,14 +559,14 @@ public class StorageManager { /** * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}. * - * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. - * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} + * object. * @hide */ @UnsupportedAppUsage public void unregisterListener(StorageEventListener listener) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mListener == listener) { try { @@ -558,7 +601,8 @@ public class StorageManager { * {@link StorageManager#getStorageVolumes()} to observe the latest * value. */ - public void onStateChanged(@NonNull StorageVolume volume) { } + public void onStateChanged(@NonNull StorageVolume volume) { + } } /** @@ -592,7 +636,7 @@ public class StorageManager { */ public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) { synchronized (mDelegates) { - for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) { + for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) { final StorageEventListenerDelegate delegate = i.next(); if (delegate.mCallback == callback) { try { @@ -628,8 +672,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) host is connected. - * @return true if UMS host is connected. * + * @return true if UMS host is connected. * @hide */ @Deprecated @@ -640,8 +684,8 @@ public class StorageManager { /** * Query if a USB Mass Storage (UMS) is enabled on the device. - * @return true if UMS host is enabled. * + * @return true if UMS host is enabled. * @hide */ @Deprecated @@ -663,11 +707,11 @@ public class StorageManager { * That is, shared UID applications can attempt to mount any other * application's OBB that shares its UID. * - * @param rawPath the path to the OBB file - * @param key must be <code>null</code>. Previously, some Android device - * implementations accepted a non-<code>null</code> key to mount - * an encrypted OBB file. However, this never worked reliably and - * is no longer supported. + * @param rawPath the path to the OBB file + * @param key must be <code>null</code>. Previously, some Android device + * implementations accepted a non-<code>null</code> key to mount + * an encrypted OBB file. However, this never worked reliably and + * is no longer supported. * @param listener will receive the success or failure of the operation * @return whether the mount call was successfully queued or not */ @@ -739,9 +783,9 @@ public class StorageManager { * application's OBB that shares its UID. * <p> * - * @param rawPath path to the OBB file - * @param force whether to kill any programs using this in order to unmount - * it + * @param rawPath path to the OBB file + * @param force whether to kill any programs using this in order to unmount + * it * @param listener will receive the success or failure of the operation * @return whether the unmount call was successfully queued or not */ @@ -781,7 +825,7 @@ public class StorageManager { * * @param rawPath path to OBB image * @return absolute path to mounted OBB image data or <code>null</code> if - * not mounted or exception encountered trying to read status + * not mounted or exception encountered trying to read status */ public String getMountedObbPath(String rawPath) { Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); @@ -899,7 +943,7 @@ public class StorageManager { * {@link #UUID_DEFAULT}. * * @throws IOException when the storage device hosting the given path isn't - * present, or when it doesn't have a valid UUID. + * present, or when it doesn't have a valid UUID. */ public @NonNull UUID getUuidForPath(@NonNull File path) throws IOException { Preconditions.checkNotNull(path); @@ -1172,8 +1216,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#getPrimaryStorageCurrentVolume() * @hide + * @see PackageManager#getPrimaryStorageCurrentVolume() */ public String getPrimaryStorageUuid() { try { @@ -1186,8 +1230,8 @@ public class StorageManager { /** * This is not the API you're looking for. * - * @see PackageManager#movePrimaryStorage(VolumeInfo) * @hide + * @see PackageManager#movePrimaryStorage(VolumeInfo) */ public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) { try { @@ -1216,7 +1260,7 @@ public class StorageManager { // resolve the actual volume name if (Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL)) { try (Cursor c = mContext.getContentResolver().query(uri, - new String[] { MediaStore.MediaColumns.VOLUME_NAME }, null, null)) { + new String[]{MediaStore.MediaColumns.VOLUME_NAME}, null, null)) { if (c.moveToFirst()) { volumeName = c.getString(0); } @@ -1275,6 +1319,7 @@ public class StorageManager { /** * Gets the state of a volume via its mountpoint. + * * @hide */ @Deprecated @@ -1308,7 +1353,7 @@ public class StorageManager { * Return the list of shared/external storage volumes currently available to * the calling user and the user it shares media with. Please refer to * <a href="https://source.android.com/compatibility/12/android-12-cdd#95_multi-user_support"> - * multi-user support</a> for more details. + * multi-user support</a> for more details. * * <p> * This is similar to {@link StorageManager#getStorageVolumes()} except that the result also @@ -1353,7 +1398,7 @@ public class StorageManager { public static Pair<String, Long> getPrimaryStoragePathAndSize() { return Pair.create(null, FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace() - + Environment.getRootDirectory().getTotalSpace())); + + Environment.getRootDirectory().getTotalSpace())); } /** {@hide} */ @@ -1389,8 +1434,6 @@ public class StorageManager { /** {@hide} */ @UnsupportedAppUsage public static @NonNull StorageVolume[] getVolumeList(int userId, int flags) { - final IStorageManager storageManager = IStorageManager.Stub.asInterface( - ServiceManager.getService("mount")); try { String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) { @@ -1406,7 +1449,7 @@ public class StorageManager { } packageName = packageNames[0]; } - return storageManager.getVolumeList(userId, packageName, flags); + return sVolumeListCache.query(new VolumeListQuery(userId, packageName, flags)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1414,6 +1457,7 @@ public class StorageManager { /** * Returns list of paths for all mountable volumes. + * * @hide */ @Deprecated @@ -1605,7 +1649,7 @@ public class StorageManager { * <p> * This is only intended to be called by UserManagerService, as part of creating a user. * - * @param userId ID of the user + * @param userId ID of the user * @param ephemeral whether the user is ephemeral * @throws RuntimeException on error. The user's keys already existing is considered an error. * @hide @@ -1711,7 +1755,8 @@ public class StorageManager { return false; } - /** {@hide} + /** + * {@hide} * Is this device encrypted? * <p> * Note: all devices launching with Android 10 (API level 29) or later are @@ -1724,8 +1769,10 @@ public class StorageManager { return RoSystemProperties.CRYPTO_ENCRYPTED; } - /** {@hide} + /** + * {@hide} * Does this device have file-based encryption (FBE) enabled? + * * @return true if the device has file-based encryption enabled. */ public static boolean isFileEncrypted() { @@ -1759,8 +1806,8 @@ public class StorageManager { } /** - * @deprecated disabled now that FUSE has been replaced by sdcardfs * @hide + * @deprecated disabled now that FUSE has been replaced by sdcardfs */ @Deprecated public static File maybeTranslateEmulatedPathToInternal(File path) { @@ -1790,6 +1837,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ public static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1800,6 +1848,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop but do not noteOp. + * * @hide */ public static boolean checkPermissionAndCheckOp(Context context, boolean enforce, @@ -1810,6 +1859,7 @@ public class StorageManager { /** * Check that given app holds both permission and appop. + * * @hide */ private static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid, @@ -1877,7 +1927,9 @@ public class StorageManager { // Legacy apps technically have the access granted by this op, // even when the op is denied if ((mAppOps.checkOpNoThrow(OP_LEGACY_STORAGE, uid, - packageName) == AppOpsManager.MODE_ALLOWED)) return true; + packageName) == AppOpsManager.MODE_ALLOWED)) { + return true; + } if (enforce) { throw new SecurityException("Op " + AppOpsManager.opToName(op) + " " @@ -1924,7 +1976,7 @@ public class StorageManager { return true; } if (mode == AppOpsManager.MODE_DEFAULT && mContext.checkPermission( - MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { + MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) { return true; } // If app doesn't have MANAGE_EXTERNAL_STORAGE, then check if it has requested granular @@ -1936,7 +1988,7 @@ public class StorageManager { @VisibleForTesting public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler, ThreadFactory factory) - throws IOException { + throws IOException { Preconditions.checkNotNull(callback); MetricsLogger.count(mContext, "storage_open_proxy_file_descriptor", 1); // Retry is needed because the mount point mFuseAppLoop is using may be unmounted before @@ -1987,7 +2039,7 @@ public class StorageManager { /** {@hide} */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback) - throws IOException { + throws IOException { return openProxyFileDescriptor(mode, callback, null, null); } @@ -2006,19 +2058,18 @@ public class StorageManager { * you're willing to decrypt on-demand, but where you want to avoid * persisting the cleartext version. * - * @param mode The desired access mode, must be one of - * {@link ParcelFileDescriptor#MODE_READ_ONLY}, - * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or - * {@link ParcelFileDescriptor#MODE_READ_WRITE} + * @param mode The desired access mode, must be one of + * {@link ParcelFileDescriptor#MODE_READ_ONLY}, + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or + * {@link ParcelFileDescriptor#MODE_READ_WRITE} * @param callback Callback to process file operation requests issued on - * returned file descriptor. - * @param handler Handler that invokes callback methods. + * returned file descriptor. + * @param handler Handler that invokes callback methods. * @return Seekable ParcelFileDescriptor. - * @throws IOException */ public @NonNull ParcelFileDescriptor openProxyFileDescriptor( int mode, ProxyFileDescriptorCallback callback, Handler handler) - throws IOException { + throws IOException { Preconditions.checkNotNull(handler); return openProxyFileDescriptor(mode, callback, handler, null); } @@ -2050,10 +2101,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheSizeBytes(UUID) */ @WorkerThread @@ -2085,10 +2136,10 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume that you're interested - * in. The UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * in. The UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support cache quotas. + * doesn't support cache quotas. * @see #getCacheQuotaBytes(UUID) */ @WorkerThread @@ -2106,7 +2157,7 @@ public class StorageManager { /** @hide */ - @IntDef(prefix = { "MOUNT_MODE_" }, value = { + @IntDef(prefix = {"MOUNT_MODE_"}, value = { MOUNT_MODE_EXTERNAL_NONE, MOUNT_MODE_EXTERNAL_DEFAULT, MOUNT_MODE_EXTERNAL_INSTALLER, @@ -2115,16 +2166,19 @@ public class StorageManager { }) @Retention(RetentionPolicy.SOURCE) /** @hide */ - public @interface MountMode {} + public @interface MountMode { + } /** * No external storage should be mounted. + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** * Default external storage should be mounted. + * * @hide */ @SystemApi @@ -2132,12 +2186,14 @@ public class StorageManager { /** * Mount mode for package installers which should give them access to * all obb dirs in addition to their package sandboxes + * * @hide */ @SystemApi public static final int MOUNT_MODE_EXTERNAL_INSTALLER = IVold.REMOUNT_MODE_INSTALLER; /** * The lower file system should be bind mounted directly on external storage + * * @hide */ @SystemApi @@ -2146,6 +2202,7 @@ public class StorageManager { /** * Use the regular scoped storage filesystem, but Android/ should be writable. * Used to support the applications hosting DownloadManager and the MTP server. + * * @hide */ @SystemApi @@ -2164,10 +2221,10 @@ public class StorageManager { * this flag to take effect. * </p> * + * @hide * @see #getAllocatableBytes(UUID, int) * @see #allocateBytes(UUID, long, int) * @see #allocateBytes(FileDescriptor, long, int) - * @hide */ @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE) @SystemApi @@ -2194,6 +2251,7 @@ public class StorageManager { * freeable cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_NON_CACHE_ONLY = 1 << 3; @@ -2203,12 +2261,13 @@ public class StorageManager { * cached space when determining allocatable space. * * Intended for use with {@link #getAllocatableBytes()}. + * * @hide */ public static final int FLAG_ALLOCATE_CACHE_ONLY = 1 << 4; /** @hide */ - @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = { + @IntDef(flag = true, prefix = {"FLAG_ALLOCATE_"}, value = { FLAG_ALLOCATE_AGGRESSIVE, FLAG_ALLOCATE_DEFY_ALL_RESERVED, FLAG_ALLOCATE_DEFY_HALF_RESERVED, @@ -2216,7 +2275,8 @@ public class StorageManager { FLAG_ALLOCATE_CACHE_ONLY, }) @Retention(RetentionPolicy.SOURCE) - public @interface AllocateFlags {} + public @interface AllocateFlags { + } /** * Return the maximum number of new bytes that your app can allocate for @@ -2246,15 +2306,15 @@ public class StorageManager { * </p> * * @param storageUuid the UUID of the storage volume where you're - * considering allocating disk space, since allocatable space can - * vary widely depending on the underlying storage device. The - * UUID for a specific path can be obtained using - * {@link #getUuidForPath(File)}. + * considering allocating disk space, since allocatable space can + * vary widely depending on the underlying storage device. The + * UUID for a specific path can be obtained using + * {@link #getUuidForPath(File)}. * @return the maximum number of new bytes that the calling app can allocate - * using {@link #allocateBytes(UUID, long)} or - * {@link #allocateBytes(FileDescriptor, long)}. + * using {@link #allocateBytes(UUID, long)} or + * {@link #allocateBytes(FileDescriptor, long)}. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space. + * doesn't support allocating space. */ @WorkerThread public @BytesLong long getAllocatableBytes(@NonNull UUID storageUuid) @@ -2297,12 +2357,12 @@ public class StorageManager { * more than once every 60 seconds. * * @param storageUuid the UUID of the storage volume where you'd like to - * allocate disk space. The UUID for a specific path can be - * obtained using {@link #getUuidForPath(File)}. - * @param bytes the number of bytes to allocate. + * allocate disk space. The UUID for a specific path can be + * obtained using {@link #getUuidForPath(File)}. + * @param bytes the number of bytes to allocate. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #getAllocatableBytes(UUID) */ @WorkerThread @@ -2332,10 +2392,9 @@ public class StorageManager { * These mount modes specify different views and access levels for * different apps on external storage. * + * @return {@code MountMode} for the given uid and packageName. * @params uid UID of the application * @params packageName name of the package - * @return {@code MountMode} for the given uid and packageName. - * * @hide */ @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) @@ -2366,15 +2425,15 @@ public class StorageManager { * (such as when recording a video) you should avoid calling this method * more than once every 60 seconds. * - * @param fd the open file that you'd like to allocate disk space for. + * @param fd the open file that you'd like to allocate disk space for. * @param bytes the number of bytes to allocate. This is the desired final - * size of the open file. If the open file is smaller than this - * requested size, it will be extended without modifying any - * existing contents. If the open file is larger than this - * requested size, it will be truncated. + * size of the open file. If the open file is smaller than this + * requested size, it will be extended without modifying any + * existing contents. If the open file is larger than this + * requested size, it will be truncated. * @throws IOException when the storage device isn't present, or when it - * doesn't support allocating space, or if the device had - * trouble allocating the requested space. + * doesn't support allocating space, or if the device had + * trouble allocating the requested space. * @see #isAllocationSupported(FileDescriptor) * @see Environment#isExternalStorageEmulated(File) */ @@ -2499,13 +2558,14 @@ public class StorageManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "QUOTA_TYPE_" }, value = { + @IntDef(prefix = {"QUOTA_TYPE_"}, value = { QUOTA_TYPE_MEDIA_NONE, QUOTA_TYPE_MEDIA_AUDIO, QUOTA_TYPE_MEDIA_VIDEO, QUOTA_TYPE_MEDIA_IMAGE, }) - public @interface QuotaType {} + public @interface QuotaType { + } private static native boolean setQuotaProjectId(String path, long projectId); @@ -2532,15 +2592,13 @@ public class StorageManager { * The default platform user of this API is the MediaProvider process, which is * responsible for managing all of external storage. * - * @param path the path to the file for which we should update the quota type + * @param path the path to the file for which we should update the quota type * @param quotaType the quota type of the file; this is based on the * {@code QuotaType} constants, eg * {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO} - * * @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid * quota type. * @throws IOException if the quota type could not be updated. - * * @hide */ @SystemApi @@ -2616,7 +2674,6 @@ public class StorageManager { * permissions of a directory to what they should anyway be. * * @param path the path for which we should fix up the permissions - * * @hide */ public void fixupAppDir(@NonNull File path) { @@ -2822,11 +2879,12 @@ public class StorageManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "APP_IO_BLOCKED_REASON_" }, value = { - APP_IO_BLOCKED_REASON_TRANSCODING, - APP_IO_BLOCKED_REASON_UNKNOWN, + @IntDef(prefix = {"APP_IO_BLOCKED_REASON_"}, value = { + APP_IO_BLOCKED_REASON_TRANSCODING, + APP_IO_BLOCKED_REASON_UNKNOWN, }) - public @interface AppIoBlockedReason {} + public @interface AppIoBlockedReason { + } /** * Notify the system that an app with {@code uid} and {@code tid} is blocked on an IO request on @@ -2839,10 +2897,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is blocked on - * @param uid the UID of the app blocked on IO - * @param tid the tid of the app blocked on IO - * @param reason the reason the app is blocked on IO - * + * @param uid the UID of the app blocked on IO + * @param tid the tid of the app blocked on IO + * @param reason the reason the app is blocked on IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2866,10 +2923,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume that the app IO is resumed on - * @param uid the UID of the app resuming IO - * @param tid the tid of the app resuming IO - * @param reason the reason the app is resuming IO - * + * @param uid the UID of the app resuming IO + * @param tid the tid of the app resuming IO + * @param reason the reason the app is resuming IO * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2890,10 +2946,9 @@ public class StorageManager { * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission. * * @param volumeUuid the UUID of the storage volume to check IO blocked status - * @param uid the UID of the app to check IO blocked status - * @param tid the tid of the app to check IO blocked status - * @param reason the reason to check IO blocked status for - * + * @param uid the UID of the app to check IO blocked status + * @param tid the tid of the app to check IO blocked status + * @param reason the reason to check IO blocked status for * @hide */ @TestApi @@ -2962,7 +3017,6 @@ public class StorageManager { * information is available, -1 is returned. * * @return Percentage of the remaining useful lifetime of the internal storage device. - * * @hide */ @FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API) diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 1c36eaf99afa..9c1f134bff3e 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -290,9 +290,15 @@ public abstract class InputEventReceiver { @SuppressWarnings("unused") @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchInputEvent(int seq, InputEvent event) { - Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event)); + if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) { + // This 'if' block is an optimization - without it, 'getShortDescription' will be + // called unconditionally, which is expensive. + Trace.traceBegin(Trace.TRACE_TAG_INPUT, + "dispatchInputEvent " + getShortDescription(event)); + } mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); + // If tracing is not enabled, `traceEnd` is a no-op (so we don't need to guard it with 'if') Trace.traceEnd(Trace.TRACE_TAG_INPUT); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 900f22d2b37b..0d6f82773622 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -133,6 +133,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -271,7 +272,9 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; +import android.window.WindowContext; import android.window.WindowOnBackInvokedDispatcher; +import android.window.WindowTokenClient; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -6609,12 +6612,26 @@ public final class ViewRootImpl implements ViewParent, mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId, activityWindowInfo); } else { - // There is no activity callback - update the configuration right away. + if (enableWindowContextResourcesUpdateOnConfigChange()) { + // There is no activity callback - update resources for window token, if needed. + final WindowTokenClient windowTokenClient = getWindowTokenClient(); + if (windowTokenClient != null) { + windowTokenClient.onConfigurationChanged( + mLastReportedMergedConfiguration.getMergedConfiguration(), + newDisplayId == INVALID_DISPLAY ? mDisplay.getDisplayId() + : newDisplayId); + } + } updateConfiguration(newDisplayId); } mForceNextConfigUpdate = false; } + private WindowTokenClient getWindowTokenClient() { + if (!(mContext instanceof WindowContext)) return null; + return (WindowTokenClient) mContext.getWindowContextToken(); + } + /** * Update display and views if last applied merged configuration changed. * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise. diff --git a/core/java/android/view/accessibility/OWNERS b/core/java/android/view/accessibility/OWNERS index f62b33f1f753..799ef0091f71 100644 --- a/core/java/android/view/accessibility/OWNERS +++ b/core/java/android/view/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0fb80422833c..56f0415b40cc 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -3778,8 +3778,32 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), - false /* fromIme */, statsToken); + synchronized (mH) { + Handler vh = rootView.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out from + // under us. + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) { + Log.v(TAG, "Close current input: reschedule hide to view thread"); + } + final var viewRootImpl = mCurRootView; + vh.post(() -> viewRootImpl.getInsetsController().hide( + WindowInsets.Type.ime(), false /* fromIme */, statsToken)); + } else { + mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); + } + } } else { IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 512113692c76..cf21e50e0a19 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -1291,12 +1291,13 @@ public final class TransitionInfo implements Parcelable { return options; } - /** Make options for a scale-up animation. */ + /** Make options for a scale-up animation with task override option */ @NonNull public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width, - int height) { + int height, boolean overrideTaskTransition) { AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP); options.mTransitionBounds.set(startX, startY, startX + width, startY + height); + options.mOverrideTaskTransition = overrideTaskTransition; return options; } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index a551fe701c5b..f7bee619bc4b 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -106,7 +106,6 @@ public class WindowTokenClient extends Binder { * @param newConfig the updated {@link Configuration} * @param newDisplayId the updated {@link android.view.Display} ID */ - @VisibleForTesting(visibility = PACKAGE) @MainThread public void onConfigurationChanged(Configuration newConfig, int newDisplayId) { onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 09c6dc0e2b20..509e084e01e6 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -677,6 +677,17 @@ flag { } flag { + name: "enable_window_context_resources_update_on_config_change" + namespace: "lse_desktop_experience" + description: "Updates window context resources before the view receives the config change callback." + bug: "394527409" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_tab_tearing_minimize_animation_bugfix" namespace: "lse_desktop_experience" description: "Enabling a minimize animation when a new window is opened via tab tearing and the Desktop Windowing open windows limit is reached." diff --git a/core/java/com/android/internal/accessibility/OWNERS b/core/java/com/android/internal/accessibility/OWNERS index 1265dfa2c441..dac64f47ba7e 100644 --- a/core/java/com/android/internal/accessibility/OWNERS +++ b/core/java/com/android/internal/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners include /services/accessibility/OWNERS
\ No newline at end of file diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS index 731dca9b128f..df867252c01c 100644 --- a/core/java/com/android/internal/graphics/palette/OWNERS +++ b/core/java/com/android/internal/graphics/palette/OWNERS @@ -1,3 +1,2 @@ -# Bug component: 484670
-dupin@google.com
-jamesoleary@google.com
\ No newline at end of file +# Bug component: 484670 +dupin@google.com diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 4ff3f8825cc4..ef5875eff06f 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -110,4 +110,8 @@ tap power gesture from triggering the selected target action. --> <integer name="config_doubleTapPowerGestureMode">0</integer> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index b3581d98face..d7ffcc54562c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7256,6 +7256,9 @@ <!-- Wear devices: An intent action that is used for remote intent. --> <string name="config_wearRemoteIntentAction" translatable="false" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <bool name="config_canInternalDisplayHostDesktops">false</bool> + <!-- Whether desktop mode is supported on the current device --> <bool name="config_isDesktopModeSupported">false</bool> @@ -7348,4 +7351,8 @@ <!-- Array containing the notification assistant service adjustments that are not supported by default on this device--> <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" /> + + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This + config enables OEMs to support its usage across tasks.--> + <bool name="config_enableCrossTaskScaleUpAnimation">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9393aa4b6086..6701e63c4f90 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5754,6 +5754,9 @@ <!-- Whether the developer option for desktop mode is supported on the current device --> <java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" /> + <!-- Whether the current device's internal display can host desktop sessions. --> + <java-symbol type="bool" name="config_canInternalDisplayHostDesktops" /> + <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. --> <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/> @@ -5902,4 +5905,8 @@ <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" /> + + <!-- Enable OEMs to support scale up anim across tasks.--> + <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" /> + </resources> diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index dfded7321b2c..0c4ea79dd5be 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -102,6 +102,10 @@ public final class Bitmap implements Parcelable { private static volatile int sDefaultDensity = -1; + /** + * This id is not authoritative and can be duplicated if an ashmem bitmap is decoded from a + * parcel. + */ private long mId; /** diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 2586bd6d86cb..643c1506e4c2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -220,6 +220,13 @@ public class DesktopModeStatus { } /** + * Return {@code true} if the current device can host desktop sessions on its internal display. + */ + public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopModeDevOption(@NonNull Context context) { @@ -231,21 +238,24 @@ public class DesktopModeStatus { * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { - return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context); + return Flags.showDesktopExperienceDevOption() + && isInternalDisplayEligibleToHostDesktops(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ public static boolean shouldDevOptionBeEnabledByDefault(Context context) { - return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode(); + return isInternalDisplayEligibleToHostDesktops(context) + && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDeviceEligibleForDesktopMode(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) - || isDesktopModeEnabledByDevOption(context); + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !enforceDeviceRestrictions()) + || isDesktopModeEnabledByDevOption(context)); } /** @@ -313,10 +323,11 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop mode is unrestricted and is supported in the device. + * Return {@code true} if desktop sessions is unrestricted and can be host for the device's + * internal display. */ - public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported( context)); } @@ -325,7 +336,7 @@ public class DesktopModeStatus { * Return {@code true} if the developer option for desktop mode is unrestricted and is supported * in the device. * - * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then + * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. */ private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { 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 8cf2370df48d..c7a0401c2b88 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 @@ -793,15 +793,21 @@ public class BubbleController implements ConfigurationChangeListener, public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, @BubbleBarLocation.UpdateSource int source) { if (isShowingAsBubbleBar()) { + updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source); + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; + mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + } + } + + private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { + if (isShowingAsBubbleBar()) { BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); } - BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); - bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; - mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); - logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); } } @@ -874,7 +880,8 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent itemIntent) { + public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location, + Intent itemIntent) { hideBubbleBarExpandedViewDropTarget(); ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO); @@ -1521,18 +1528,19 @@ public class BubbleController implements ConfigurationChangeListener, public void expandStackAndSelectBubble(ShortcutInfo info, @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; - if (bubbleBarLocation != null) { - //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & - // fix bubble bar flicking - setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); } Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, + updateLocation); } } @@ -1562,19 +1570,19 @@ public class BubbleController implements ConfigurationChangeListener, public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user, @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; - if (bubbleBarLocation != null) { - //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & - // fix bubble bar flicking - setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); } Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user); ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s", pendingIntent); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation); } } @@ -1940,11 +1948,22 @@ public class BubbleController implements ConfigurationChangeListener, @VisibleForTesting public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null); + } + + /** + * Inflates and adds a bubble. Updates Bubble Bar location if bubbles + * are shown in the Bubble Bar and the location is not null. + */ + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate( - b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade, + bubbleBarLocation), mContext, mExpandedViewManager, mBubbleTaskViewFactory, @@ -2278,7 +2297,8 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" + " expanded=%b selectionChanged=%b selected=%s" - + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", + + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b" + + " bubbleBarLocation=%s", update.addedBubble != null ? update.addedBubble.getKey() : "null", !update.removedBubbles.isEmpty(), update.updatedBubble != null ? update.updatedBubble.getKey() : "null", @@ -2287,7 +2307,9 @@ public class BubbleController implements ConfigurationChangeListener, update.selectedBubble != null ? update.selectedBubble.getKey() : "null", update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", - update.shouldShowEducation, update.showOverflowChanged); + update.shouldShowEducation, update.showOverflowChanged, + update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString() + : "null"); ensureBubbleViewsAndWindowCreated(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index f97133a4c3d1..abcdb7e70cec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -43,6 +43,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.RemovedBubble; @@ -91,6 +92,8 @@ public class BubbleData { @Nullable Bubble suppressedBubble; @Nullable Bubble unsuppressedBubble; @Nullable String suppressedSummaryGroup; + @Nullable + BubbleBarLocation mBubbleBarLocation; // Pair with Bubble and @DismissReason Integer final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); @@ -116,6 +119,7 @@ public class BubbleData { || unsuppressedBubble != null || suppressedSummaryChanged || suppressedSummaryGroup != null + || mBubbleBarLocation != null || showOverflowChanged; } @@ -169,6 +173,7 @@ public class BubbleData { } bubbleBarUpdate.showOverflowChanged = showOverflowChanged; bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); + bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation; return bubbleBarUpdate; } @@ -396,8 +401,23 @@ public class BubbleData { * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. */ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null); + } + + /** + * Sets the selected bubble and expands it. Also updates bubble bar location if the + * bubbleBarLocation is not {@code null} + * + * <p>This dispatches a single state update for 3 changes and should be used instead of + * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by + * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will + * generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble, + @Nullable BubbleBarLocation bubbleBarLocation) { setSelectedBubbleInternal(bubble); setExpandedInternal(true); + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } @@ -513,13 +533,25 @@ public class BubbleData { } /** + * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing + * {@code null} for bubbleBarLocation. + * + * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation) + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */ + null); + } + + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, * BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView, * com.android.wm.shell.bubbles.bar.BubbleBarLayerView, * com.android.launcher3.icons.BubbleIconFactory, boolean) */ - void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); suppressFlyout |= !bubble.isTextChanged(); @@ -567,6 +599,7 @@ public class BubbleData { doSuppress(bubble); } } + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index aa42de67152a..e3b0872df593 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -524,8 +524,8 @@ public class BubbleBarLayerView extends FrameLayout * Skips logging if it is {@link BubbleOverflow}. */ private void logBubbleEvent(BubbleLogger.Event event) { - if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) { - mBubbleLogger.log(bubble, event); + if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) { + mBubbleLogger.log((Bubble) mExpandedBubble, event); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index ae8f8c4eff79..7751741ae082 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -46,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; @@ -73,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -1353,6 +1356,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */); t.show(mPausingTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mPausingTasks); if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } @@ -1390,6 +1395,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mOpeningTasks); for (int i = 0; i < mPausingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint); } @@ -1450,6 +1457,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.clear(); if (Flags.enableRecentsBookendTransition()) { + // Notify the mixers of the pending finish + for (int i = 0; i < mMixers.size(); ++i) { + mMixers.get(i).handleFinishRecents(returningToApp, wct, t); + } + // In this case, we've already started the PIP transition, so we can // clean up immediately mPendingRunnerFinishCb = runnerFinishCb; @@ -1509,6 +1521,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + private static void setCornerRadiusForFreeformTasks( + Context context, + SurfaceControl.Transaction t, + ArrayList<TaskState> tasks) { + if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) { + return; + } + int cornerRadius = getCornerRadius(context); + for (int i = 0; i < tasks.size(); ++i) { + TaskState task = tasks.get(i); + if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) { + t.setCornerRadius(task.mTaskSurface, cornerRadius); + } + } + } + + private static int getCornerRadius(Context context) { + return context.getResources().getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) { if (tasks == null) { return false; 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 aff21cbe0ae6..15ac03ccaf30 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 @@ -1675,8 +1675,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct, @ExitReason int exitReason) { if (!isSplitActive()) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s", - stageTypeToString(stageToTop)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s", + stageTypeToString(stageToTop), exitReasonToString(exitReason)); if (enableFlexibleSplit()) { mStageOrderOperator.getActiveStages().stream() .filter(stage -> stage.getId() != stageToTop) @@ -3395,12 +3395,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, TransitionInfo.Change sideChild = null; StageTaskListener firstAppStage = null; StageTaskListener secondAppStage = null; + boolean foundPausingTask = false; final WindowContainerTransaction evictWct = new WindowContainerTransaction(); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; if (mPausingTasks.contains(taskInfo.taskId)) { + foundPausingTask = true; continue; } StageTaskListener stage = getStageOfTask(taskInfo); @@ -3443,9 +3445,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN); logExit(EXIT_REASON_UNKNOWN); }); - Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", - "launched 2 tasks in split, but didn't receive " - + "2 tasks in transition. Possibly one of them failed to launch")); + Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in " + + "split, but didn't receive 2 tasks in transition. Possibly one of them " + + "failed to launch (foundPausingTask=" + foundPausingTask + ")")); if (mRecentTasks.isPresent() && mainChild != null) { mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId); } @@ -3800,6 +3802,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Call this when the recents animation canceled during split-screen. */ public void onRecentsInSplitAnimationCanceled() { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled"); mPausingTasks.clear(); setSplitsVisible(false); @@ -3809,31 +3812,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - public void onRecentsInSplitAnimationFinishing(boolean returnToApp, - @NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (!Flags.enableRecentsBookendTransition()) { - // The non-bookend recents transition case will be handled by - // RecentsMixedTransition wrapping the finish callback and calling - // onRecentsInSplitAnimationFinish() - return; - } - - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); - } - - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (Flags.enableRecentsBookendTransition()) { - // The bookend recents transition case will be handled by - // onRecentsInSplitAnimationFinishing above - return; - } - - // Check if the recent transition is finished by returning to the current - // split, so we can restore the divider bar. - boolean returnToApp = false; + /** + * Returns whether the given WCT is reordering any of the split tasks to top. + */ + public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) { for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); @@ -3848,14 +3830,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() && anyStageContainsContainer) { - returnToApp = true; + return true; } } - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + return false; } - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinishInner(boolean returnToApp, + /** Called when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinishing(boolean returnToApp, @NonNull WindowContainerTransaction finishWct, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index f40dc8ad93b5..1e926c57ca61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // If pair-to-pair switching, the post-recents clean-up isn't needed. wct = wct != null ? wct : new WindowContainerTransaction(); if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { - // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove - // once that rolls out - mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + // We've dispatched to the mLeftoversHandler to handle the rest of the transition + // and called onRecentsInSplitAnimationStart(), but if the recents handler is not + // actually handling the transition, then onRecentsInSplitAnimationFinishing() + // won't actually get called by the recents handler. In such cases, we still need + // to clean up after the changes from the start call. + boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler; + if (!splitNotifiedByRecents) { + mSplitHandler.onRecentsInSplitAnimationFinishing( + mSplitHandler.wctIsReorderingSplitToTop(wct), + wct, finishTransaction); + } } else { // notify pair-to-pair recents animation finish mSplitHandler.onRecentsPairToPairAnimationFinish(wct); diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 19829e7e5677..bac8e5062128 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -12,7 +12,6 @@ atsjenk@google.com jorgegil@google.com vaniadesmonda@google.com pbdr@google.com -tkachenkoi@google.com mpodolian@google.com jeremysim@google.com peanutbutter@google.com diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index ffcc3446d436..7a7d88b80ce3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -572,6 +572,22 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.shouldShowEducation).isTrue(); } + /** Verifies that the update should contain the bubble bar location. */ + @Test + public void test_shouldUpdateBubbleBarLocation() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true, BubbleBarLocation.LEFT); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + } + /** * Verifies that the update shouldn't show the user education, if the education is required but * the bubble should auto-expand @@ -1367,6 +1383,20 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void setSelectedBubbleAndExpandStackWithLocation() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + assertLocationChangedTo(BubbleBarLocation.LEFT); + } + + @Test public void testShowOverflowChanged_hasOverflowBubbles() { assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); sendUpdatedEntryAtTime(mEntryA1, 1000); @@ -1450,6 +1480,12 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); } + private void assertLocationChangedTo(BubbleBarLocation location) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("locationChanged").that(update.mBubbleBarLocation) + .isEqualTo(location); + } + private void assertExpandedChangedTo(boolean expected) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index d6b13610c9c1..70a30a3ca7a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -113,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index 403d468a7034..d510570e8839 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -30,7 +30,6 @@ import android.view.KeyEvent import android.window.DisplayAreaInfo import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER @@ -48,7 +47,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction -import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel @@ -107,12 +105,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) - mockitoSession = - mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking() testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index cd1c16a93475..5ac615eaff35 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -171,7 +171,6 @@ import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock @@ -292,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .spyStatic(DesktopModeStatus::class.java) .spyStatic(Toast::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) @@ -363,9 +362,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shellInit.init() - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + val captor = argumentCaptor<RecentsTransitionStateListener>() verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value + recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener @@ -441,7 +440,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { val task1 = setUpFreeformTask() - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -461,7 +460,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() STABLE_BOUNDS.height(), displayController, ) - assertThat(argumentCaptor.value).isTrue() + assertThat(argumentCaptor.firstValue).isTrue() } @Test @@ -476,7 +475,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -497,7 +496,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(displayController), anyOrNull(), ) - assertThat(argumentCaptor.value).isFalse() + assertThat(argumentCaptor.firstValue).isFalse() } @Test @@ -1736,7 +1735,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1751,12 +1750,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1768,7 +1767,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test @@ -2224,26 +2223,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertThat(transitionHandlerArgCaptor.value) + assertThat(transitionHandlerArgCaptor.firstValue) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @@ -2718,9 +2717,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2759,9 +2758,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2775,9 +2774,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @Test @@ -2791,10 +2790,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2808,9 +2807,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is already minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2825,9 +2824,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2845,10 +2844,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // task1 is the only visible task as task2 is minimized. controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4635,7 +4634,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4643,9 +4642,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(task2.configuration.windowConfiguration.bounds), ) - assertThat(wctArgument.value.hierarchyOps).hasSize(1) + assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop - wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4660,7 +4659,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4669,7 +4668,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(task2.configuration.windowConfiguration.bounds), ) // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() + assertThat(wctArgument.firstValue.hierarchyOps).isEmpty() } @Test @@ -4677,7 +4676,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromFullscreenOpensInSplit() { setUpLandscapeDisplay() val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4690,7 +4689,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4699,7 +4698,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromSplitOpensInSplit() { setUpLandscapeDisplay() val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4712,7 +4711,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4807,11 +4806,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpFullscreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4821,11 +4820,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpSplitScreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -5912,35 +5911,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockDragEvent, mockCallback as Consumer<Boolean>, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() var expectedWindowingMode: Int if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { expectedWindowingMode = WINDOWING_MODE_FULLSCREEN // Fullscreen launches currently use default transitions - verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + verify(transitions).startTransition(any(), arg.capture(), anyOrNull()) } else { expectedWindowingMode = WINDOWING_MODE_FREEFORM if (tabTearingAnimationFlagEnabled) { verify(desktopMixedTransitionHandler) .startLaunchTransition( eq(TRANSIT_OPEN), - capture(arg), + arg.capture(), anyOrNull(), anyOrNull(), anyOrNull(), ) } else { // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture()) } } assertThat( - ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode ) .isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + assertThat( + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) + .launchBounds + ) .isEqualTo(expectedBounds) } @@ -6122,52 +6123,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out TransitionHandler>? = null, ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } - return arg.value + return arg.lastValue } private fun getLatestToggleResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value + .startTransition(arg.capture(), eq(currentBounds)) + return arg.lastValue } private fun getLatestDesktopMixedTaskWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull()) - return arg.value + .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + return arg.lastValue } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value + return arg.lastValue } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value + val arg = argumentCaptor<WindowContainerTransaction>() + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture()) + return arg.lastValue } private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value + return arg.lastValue } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index b50af741b2a6..439be9155b26 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -17,9 +17,13 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; @@ -44,9 +48,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -57,6 +63,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.IResultReceiver; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -92,9 +99,13 @@ import java.util.Optional; @SmallTest public class RecentsTransitionHandlerTest extends ShellTestCase { + private static final int FREEFORM_TASK_CORNER_RADIUS = 32; + @Mock private Context mContext; @Mock + private Resources mResources; + @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private ShellCommandHandler mShellCommandHandler; @@ -134,6 +145,10 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getSystemService(KeyguardManager.class)) .thenReturn(mock(KeyguardManager.class)); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius) + ).thenReturn(FREEFORM_TASK_CORNER_RADIUS); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mDisplayInsetsController, mMainExecutor)); @@ -276,6 +291,57 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING); } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, freeformTask) + .build(); + SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testFinish_returningToFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE, freeformTask) + .build(); + SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, transitionInfo, new StubTransaction(), finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + private IBinder startRecentsTransition(boolean synthetic) { return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index 33f14acd0f02..391d46287498 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -157,33 +157,33 @@ class DesktopModeStatusTest : ShellTestCase() { } @Test - fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() { + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test - fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() { + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() { doReturn(true).whenever(mockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ) - assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() } @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index b9d6a454694d..e5a6a6d258dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT, mock(SurfaceControl.Transaction.class)); } assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT, mock(SurfaceControl.Transaction.class)); } assertTrue(mStageCoordinator.isSplitScreenVisible()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index 53ae967e7bbf..067dcec5d65d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -73,7 +73,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())} setUpCommon() whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay) 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 f15418adf1e3..49812d381178 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 @@ -116,7 +116,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } setUpCommon() @@ -384,7 +385,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b1550b0b6888..63a024b8e780 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -260,7 +260,7 @@ sk_sp<Bitmap> Bitmap::createFrom(AHardwareBuffer* hardwareBuffer, const SkImageI #endif sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly) { + size_t size, bool readOnly, int64_t id) { #ifdef _WIN32 // ashmem not implemented on Windows return nullptr; #else @@ -279,7 +279,7 @@ sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int f } } - sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes)); + sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id)); if (readOnly) { bitmap->setImmutable(); } @@ -334,7 +334,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Ashmem) - , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { + , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 8abe6a8c445a..4e9bcf27c0ef 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -97,7 +97,7 @@ public: BitmapPalette palette); #endif static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly); + size_t size, bool readOnly, int64_t id); static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&); int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); } @@ -183,15 +183,15 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); -private: - static constexpr uint64_t INVALID_BITMAP_ID = 0u; + static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u; +private: static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, - uint64_t id = INVALID_BITMAP_ID); + uint64_t id = UNDEFINED_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 29efd98b41d0..cfde0b28c0d5 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -191,9 +191,8 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, info.width(), info.height(), isPremultiplied); } -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, - int density) { +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, + jobject ninePatchInsets, int density, int64_t id) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); @@ -208,10 +207,12 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, if (!isMutable) { bitmapWrapper->bitmap().setImmutable(); } + int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId(); jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), - bitmap->width(), bitmap->height(), density, - isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); + static_cast<jlong>(bitmapId), + reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), + bitmap->height(), density, isPremultiplied, ninePatchChunk, + ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { ALOGE("*** Uncaught exception returned from Java call!\n"); @@ -759,6 +760,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { const int32_t height = p.readInt32(); const int32_t rowBytes = p.readInt32(); const int32_t density = p.readInt32(); + const int64_t sourceId = p.readInt64(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && @@ -815,7 +817,8 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { return STATUS_NO_MEMORY; } nativeBitmap = - Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable); + Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, + !isMutable, sourceId); return STATUS_OK; }); @@ -831,15 +834,15 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { } return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr, - nullptr, density); + nullptr, density, sourceId); #else jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); return NULL; #endif } -static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, - jlong bitmapHandle, jint density, jobject parcel) { +static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, + jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { ALOGD("------- writeToParcel null parcel\n"); @@ -870,6 +873,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, binder_status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) { + p.writeInt64(bitmapWrapper->bitmap().getId()); #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as " "immutable blob (fds %s)", @@ -889,7 +893,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)", p.allowFds() ? "allowed" : "forbidden"); #endif - + p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID); status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h index 21a93f066d9b..c93246a972b6 100644 --- a/libs/hwui/jni/Bitmap.h +++ b/libs/hwui/jni/Bitmap.h @@ -18,6 +18,7 @@ #include <jni.h> #include <android/bitmap.h> +#include <hwui/Bitmap.h> struct SkImageInfo; @@ -33,9 +34,9 @@ enum BitmapCreateFlags { kBitmapCreateFlag_Premultiplied = 0x2, }; -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr, - jobject ninePatchInsets = nullptr, int density = -1); +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, + jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr, + int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID); Bitmap& toBitmap(jlong bitmapHandle); diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp index b0f5423813b7..95e4e01d8df8 100644 --- a/libs/hwui/jni/ScopedParcel.cpp +++ b/libs/hwui/jni/ScopedParcel.cpp @@ -39,6 +39,16 @@ uint32_t ScopedParcel::readUint32() { return temp; } +int64_t ScopedParcel::readInt64() { + int64_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + float ScopedParcel::readFloat() { float temp = 0.; if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) { diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h index fd8d6a210f0f..f2f138fda43c 100644 --- a/libs/hwui/jni/ScopedParcel.h +++ b/libs/hwui/jni/ScopedParcel.h @@ -35,12 +35,16 @@ public: uint32_t readUint32(); + int64_t readInt64(); + float readFloat(); void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); } + void writeFloat(float value) { AParcel_writeFloat(mParcel, value); } bool allowFds() const { return AParcel_getAllowFds(mParcel); } diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index ec287c1b65b7..52a2160cdd74 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -95,6 +95,8 @@ message PreferenceProto { optional PermissionsProto write_permissions = 18; // Tag constants associated with the preference. repeated string tags = 19; + // Permit to read and write preference value (the lower 15 bits is reserved for read permit). + optional int32 read_write_permit = 20; // Target of an Intent message ActionTarget { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index e511bf1c175d..13541b1ebc9a 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -56,6 +56,8 @@ import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY import com.android.settingslib.preference.PreferenceScreenFactory import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale @@ -415,52 +417,46 @@ fun PreferenceMetadata.toProto( for (tag in metadata.tags(context)) addTags(tag) } persistent = metadata.isPersistent(context) - if (persistent) { - if (metadata is PersistentPreference<*>) { - sensitivityLevel = metadata.sensitivityLevel - metadata.getReadPermissions(context)?.let { - if (it.size > 0) readPermissions = it.toProto() - } - metadata.getWritePermissions(context)?.let { - if (it.size > 0) writePermissions = it.toProto() + if (metadata !is PersistentPreference<*>) return@preferenceProto + sensitivityLevel = metadata.sensitivityLevel + metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() } + metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() } + val readPermit = metadata.evalReadPermit(context, callingPid, callingUid) + val writePermit = + metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW + readWritePermit = ReadWritePermit.make(readPermit, writePermit) + if ( + flags.includeValue() && + enabled && + (!hasAvailable() || available) && + (!hasRestricted() || !restricted) && + readPermit == ReadWritePermit.ALLOW + ) { + val storage = metadata.storage(context) + value = preferenceValueProto { + when (metadata.valueType) { + Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } + Boolean::class.javaObjectType -> + storage.getBoolean(metadata.key)?.let { booleanValue = it } + Float::class.javaObjectType -> + storage.getFloat(metadata.key)?.let { floatValue = it } + else -> {} } } - if ( - flags.includeValue() && - enabled && - (!hasAvailable() || available) && - (!hasRestricted() || !restricted) && - metadata is PersistentPreference<*> && - metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW - ) { - val storage = metadata.storage(context) - value = preferenceValueProto { - when (metadata.valueType) { - Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it } - Boolean::class.javaObjectType -> - storage.getBoolean(metadata.key)?.let { booleanValue = it } - Float::class.javaObjectType -> - storage.getFloat(metadata.key)?.let { floatValue = it } - else -> {} - } - } - } - if (flags.includeValueDescriptor()) { - valueDescriptor = preferenceValueDescriptorProto { - when (metadata) { - is IntRangeValuePreference -> rangeValue = rangeValueProto { - min = metadata.getMinValue(context) - max = metadata.getMaxValue(context) - step = metadata.getIncrementStep(context) - } - else -> {} - } - if (metadata is PersistentPreference<*>) { - when (metadata.valueType) { - Boolean::class.javaObjectType -> booleanType = true - Float::class.javaObjectType -> floatType = true + } + if (flags.includeValueDescriptor()) { + valueDescriptor = preferenceValueDescriptorProto { + when (metadata) { + is IntRangeValuePreference -> rangeValue = rangeValueProto { + min = metadata.getMinValue(context) + max = metadata.getMaxValue(context) + step = metadata.getIncrementStep(context) } - } + else -> {} + } + when (metadata.valueType) { + Boolean::class.javaObjectType -> booleanType = true + Float::class.javaObjectType -> floatType = true } } } @@ -478,6 +474,20 @@ fun <T> PersistentPreference<T>.evalReadPermit( else -> getReadPermit(context, callingPid, callingUid) } +/** Evaluates the write permit of a persistent preference. */ +fun <T> PersistentPreference<T>.evalWritePermit( + context: Context, + callingPid: Int, + callingUid: Int, +): Int? = + when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW + getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> + ReadWritePermit.REQUIRE_APP_PERMISSION + else -> getWritePermit(context, callingPid, callingUid) + } + private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? { if (isRoot && this is PreferenceScreenMetadata) { val titleRes = screenTitle diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 60f9c6bb92a3..72f6934b5f35 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -36,8 +36,6 @@ import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.ReadWritePermit -import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY -import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ class PreferenceSetterRequest( @@ -223,13 +221,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingPid: Int, callingUid: Int, ): Int = - when { - sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> - ReadWritePermit.DISALLOW - getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> - ReadWritePermit.REQUIRE_APP_PERMISSION - else -> getWritePermit(context, value, callingPid, callingUid) - } + evalWritePermit(context, callingPid, callingUid) + ?: getWritePermit(context, value, callingPid, callingUid) /** Message codec for [PreferenceSetterRequest]. */ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e456a7f1aa1c..c723dce82b5a 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -41,6 +41,19 @@ annotation class ReadWritePermit { const val REQUIRE_APP_PERMISSION = 2 /** Require explicit user agreement (e.g. terms of service). */ const val REQUIRE_USER_AGREEMENT = 3 + + private const val READ_PERMIT_BITS = 15 + private const val READ_PERMIT_MASK = (1 shl 16) - 1 + + /** Wraps given read and write permit into an integer. */ + fun make(readPermit: @ReadWritePermit Int, writePermit: @ReadWritePermit Int): Int = + (writePermit shl READ_PERMIT_BITS) or readPermit + + /** Extracts the read permit from given integer generated by [make]. */ + fun getReadPermit(readWritePermit: Int): Int = readWritePermit and READ_PERMIT_MASK + + /** Extracts the write permit from given integer generated by [make]. */ + fun getWritePermit(readWritePermit: Int): Int = readWritePermit shr READ_PERMIT_BITS } } @@ -81,6 +94,12 @@ interface PersistentPreference<T> : PreferenceMetadata { /** The value type the preference is associated with. */ val valueType: Class<T> + /** The sensitivity level of the preference. */ + val sensitivityLevel: @SensitivityLevel Int + get() = SensitivityLevel.UNKNOWN_SENSITIVITY + + override fun isPersistent(context: Context) = true + /** * Returns the key-value storage of the preference. * @@ -102,19 +121,27 @@ interface PersistentPreference<T> : PreferenceMetadata { * behind the scene. */ fun getReadPermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int = - PreferenceScreenRegistry.getReadPermit( - context, - callingPid, - callingUid, - this, - ) + PreferenceScreenRegistry.defaultReadPermit /** Returns the required permissions to write preference value. */ fun getWritePermissions(context: Context): Permissions? = null /** * Returns if the external application (identified by [callingPid] and [callingUid]) is - * permitted to write preference with given [value]. + * permitted to write preference value. If the write permit depends on certain value, implement + * the overloading [getWritePermit] instead. + * + * The underlying implementation does NOT need to check common states like isEnabled, + * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it + * behind the scene. + */ + fun getWritePermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int? = + null + + /** + * Returns if the external application (identified by [callingPid] and [callingUid]) is + * permitted to write preference with given [value]. Note that if the overloading + * [getWritePermit] returns non null value, this method will be ignored! * * The underlying implementation does NOT need to check common states like isEnabled, * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it @@ -125,18 +152,7 @@ interface PersistentPreference<T> : PreferenceMetadata { value: T?, callingPid: Int, callingUid: Int, - ): @ReadWritePermit Int = - PreferenceScreenRegistry.getWritePermit( - context, - value, - callingPid, - callingUid, - this, - ) - - /** The sensitivity level of the preference. */ - val sensitivityLevel: @SensitivityLevel Int - get() = SensitivityLevel.UNKNOWN_SENSITIVITY + ): @ReadWritePermit Int = PreferenceScreenRegistry.defaultWritePermit } /** Descriptor of values. */ diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index a8939ab0d902..7f2a61081fbb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -127,7 +127,7 @@ interface PreferenceMetadata { fun dependencies(context: Context): Array<String> = arrayOf() /** Returns if the preference is persistent in datastore. */ - fun isPersistent(context: Context): Boolean = this is PersistentPreference<*> + fun isPersistent(context: Context): Boolean = false /** * Returns if preference value backup is allowed (by default returns `true` if preference is diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt index 246310984db9..8d4bfffb1fdb 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -22,12 +22,18 @@ import android.util.Log import com.android.settingslib.datastore.KeyValueStore /** Registry of all available preference screens in the app. */ -object PreferenceScreenRegistry : ReadWritePermitProvider { +object PreferenceScreenRegistry { private const val TAG = "ScreenRegistry" /** Provider of key-value store. */ private lateinit var keyValueStoreProvider: KeyValueStoreProvider + /** The default permit for external application to read preference values. */ + var defaultReadPermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + + /** The default permit for external application to write preference values. */ + var defaultWritePermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW + /** * Factories of all available [PreferenceScreenMetadata]s. * @@ -38,9 +44,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { /** Metrics logger for preference actions triggered by user interaction. */ var preferenceUiActionMetricsLogger: PreferenceUiActionMetricsLogger? = null - private var readWritePermitProvider: ReadWritePermitProvider = - object : ReadWritePermitProvider {} - /** Sets the [KeyValueStoreProvider]. */ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) { this.keyValueStoreProvider = keyValueStoreProvider @@ -77,28 +80,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { return null } } - - /** - * Sets the provider to check read write permit. Read and write requests are denied by default. - */ - fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider) { - this.readWritePermitProvider = readWritePermitProvider - } - - override fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getReadPermit(context, callingPid, callingUid, preference) - - override fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ) = readWritePermitProvider.getWritePermit(context, value, callingPid, callingUid, preference) } /** Provider of [KeyValueStore]. */ @@ -113,25 +94,3 @@ fun interface KeyValueStoreProvider { */ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? } - -/** Provider of read and write permit. */ -interface ReadWritePermitProvider { - - val defaultReadWritePermit: @ReadWritePermit Int - get() = ReadWritePermit.DISALLOW - - fun getReadPermit( - context: Context, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit - - fun getWritePermit( - context: Context, - value: Any?, - callingPid: Int, - callingUid: Int, - preference: PreferenceMetadata, - ): @ReadWritePermit Int = defaultReadWritePermit -} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index bf86911ee683..572444edea29 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -30,11 +30,13 @@ import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -385,7 +387,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingSourceIfNeeded(preferredMainDevice); + syncAudioSharingStatusIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -399,13 +401,16 @@ public class CsipDeviceManager { return userManager != null && userManager.isManagedProfile(); } - private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { + private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); - if (isAudioSharingEnabled) { + if (isAudioSharingEnabled && mainDevice != null) { if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile"); return; } + Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); + deviceSet.add(mainDevice); + deviceSet.addAll(mainDevice.getMemberDevice()); boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -419,9 +424,6 @@ public class CsipDeviceManager { if (metadata != null && assistant != null) { log("addMemberDevicesIntoMainDevice: sync audio sharing source after " + "combining the top level devices."); - Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); - deviceSet.add(mainDevice); - deviceSet.addAll(mainDevice.getMemberDevice()); Set<BluetoothDevice> sinksToSync = deviceSet.stream() .map(CachedBluetoothDevice::getDevice) .filter(device -> @@ -435,8 +437,24 @@ public class CsipDeviceManager { } } } + if (Flags.enableTemporaryBondDevicesUi()) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing " + + "sinks after combining the top level devices."); + Set<BluetoothDevice> sinksToSync = deviceSet.stream() + .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect( + Collectors.toSet()); + if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) { + for (BluetoothDevice device : sinksToSync) { + if (!BluetoothUtils.isTemporaryBondDevice(device)) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for " + + device.getAnonymizedAddress()); + BluetoothUtils.setTemporaryBondMetadata(device); + } + } + } + } } else { - log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled"); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index fd14d1ff6786..2eccaa626f3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -40,6 +40,8 @@ import android.content.Context; import android.os.Looper; import android.os.Parcel; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -74,6 +76,9 @@ public class CsipDeviceManagerTest { private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String TEMP_BOND_METADATA = + "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private final static int GROUP1 = 1; private final BluetoothClass DEVICE_CLASS_1 = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); @@ -337,6 +342,7 @@ public class CsipDeviceManagerTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list @@ -346,7 +352,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -359,6 +364,7 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { // Condition: The preferredDevice is main and there is another main device in top list @@ -369,7 +375,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -377,6 +382,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -387,10 +394,13 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice1; @@ -399,7 +409,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -407,6 +416,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -415,6 +426,8 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test @@ -436,13 +449,13 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) @@ -457,16 +470,20 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant, never()).addSource(any(BluetoothDevice.class), any(BluetoothLeBroadcastMetadata.class), anyBoolean()); + verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -474,6 +491,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); + when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -488,6 +507,10 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false); verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false); + verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 028a0c6e978b..910f71276376 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1946,6 +1946,16 @@ flag { } flag { + name: "unfold_latency_tracking_fix" + namespace: "systemui" + description: "New implementation to track unfold latency that excludes broken cases" + bug: "390649568" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "ui_rich_ongoing_force_expanded" namespace: "systemui" description: "Force promoted notifications to always be expanded" 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 d7d4e1714aa6..09b8d178cc8e 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 @@ -175,7 +175,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( viewModel: NotificationsPlaceholderViewModel, ) { - val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) + val isSnoozable by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) var scrollOffset by remember { mutableFloatStateOf(0f) } val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() } @@ -192,7 +192,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ) } - val nestedScrollConnection = + val snoozeScrollConnection = object : NestedScrollConnection { override suspend fun onPreFling(available: Velocity): Velocity { if ( @@ -206,7 +206,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( } } - LaunchedEffect(isHeadsUp) { scrollOffset = 0f } + LaunchedEffect(isSnoozable) { scrollOffset = 0f } LaunchedEffect(scrollableState.isScrollInProgress) { if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) { @@ -230,10 +230,8 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace( ), ) } - .thenIf(isHeadsUp) { - Modifier.nestedScroll(nestedScrollConnection) - .scrollable(orientation = Orientation.Vertical, state = scrollableState) - }, + .thenIf(isSnoozable) { Modifier.nestedScroll(snoozeScrollConnection) } + .scrollable(orientation = Orientation.Vertical, state = scrollableState), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt new file mode 100644 index 000000000000..0c97750ba281 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 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.communal + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.keyguard.wakefulnessLifecycle +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.kotlin.JavaAdapter +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceInactiveConditionTest : SysuiTestCase() { + private val kosmos = + testKosmos().useUnconfinedTestDispatcher().also { + whenever(it.wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_AWAKE + } + + private val Kosmos.underTest by + Kosmos.Fixture { + DeviceInactiveCondition( + applicationCoroutineScope, + keyguardStateController, + wakefulnessLifecycle, + keyguardUpdateMonitor, + keyguardInteractor, + JavaAdapter(applicationCoroutineScope), + ) + } + + @Test + fun asleep_conditionTrue() = + kosmos.runTest { + // Condition is false to start. + underTest.start() + assertThat(underTest.isConditionMet).isFalse() + + // Condition is true when device goes to sleep. + sleep() + assertThat(underTest.isConditionMet).isTrue() + } + + @Test + fun dozingAndAsleep_conditionFalse() = + kosmos.runTest { + // Condition is true when device is asleep. + underTest.start() + sleep() + assertThat(underTest.isConditionMet).isTrue() + + // Condition turns false after doze starts. + fakeKeyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE) + ) + assertThat(underTest.isConditionMet).isFalse() + } + + fun Kosmos.sleep() { + whenever(wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_ASLEEP + argumentCaptor<WakefulnessLifecycle.Observer>().apply { + verify(wakefulnessLifecycle).addObserver(capture()) + firstValue.onStartedGoingToSleep() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index fecf1fd2f222..354edac75452 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -20,9 +20,12 @@ import android.content.Context import android.content.res.Resources import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD +import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl @@ -44,8 +47,10 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -56,11 +61,13 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -73,6 +80,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import org.mockito.kotlin.times @RunWith(AndroidJUnit4::class) @SmallTest @@ -88,6 +96,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + private val latencyTracker = mock<LatencyTracker>() private val deviceStateManager = kosmos.deviceStateManager private val closedDeviceState = kosmos.foldedDeviceStateList.first() @@ -142,6 +151,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) } @@ -195,6 +205,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) displaySwitchLatencyTracker.start() @@ -370,6 +381,256 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun unfoldingDevice_startsLatencyTracking() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + + verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldingDevice_doesntTrackLatency() { + testScope.runTest { + setDeviceState(UNFOLDED) + displaySwitchLatencyTracker.start() + runCurrent() + + startFolding() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldedState_doesntStartTrackingOnScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByStartEvents() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds) + powerInteractor.setScreenPowerState(SCREEN_ON) + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchTimedOut_trackingCancelled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds) + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(FOLDED) + tracker.start() + runCurrent() + } + + private suspend fun TestScope.startUnfolding() { + setDeviceState(HALF_FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private suspend fun TestScope.startFolding() { + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private fun TestScope.finishFolding() { + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + } + + private fun TestScope.finishUnfolding() { + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + } + private suspend fun setDeviceState(state: DeviceState) { foldStateRepository.emit(state) } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS index 1ed8c068f974..5a59b7aaef56 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS jonesriley@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS index b65d29c6a0bb..429b4b0fccab 100644 --- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS @@ -5,5 +5,4 @@ linyuh@google.com pauldpong@google.com praveenj@google.com vicliang@google.com -mfolkerts@google.com yuklimko@google.com diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java index 2e1b5ad177b5..e456310febfd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java +++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java @@ -17,16 +17,19 @@ package com.android.systemui.communal; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; -import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.shared.model.DozeStateModel; import com.android.systemui.shared.condition.Condition; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.kotlin.JavaAdapter; import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; import javax.inject.Inject; @@ -38,6 +41,10 @@ public class DeviceInactiveCondition extends Condition { private final KeyguardStateController mKeyguardStateController; private final WakefulnessLifecycle mWakefulnessLifecycle; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardInteractor mKeyguardInteractor; + private final JavaAdapter mJavaAdapter; + private Job mAnyDozeListenerJob; + private boolean mAnyDoze; private final KeyguardStateController.Callback mKeyguardStateCallback = new KeyguardStateController.Callback() { @Override @@ -63,12 +70,14 @@ public class DeviceInactiveCondition extends Condition { @Inject public DeviceInactiveCondition(@Application CoroutineScope scope, KeyguardStateController keyguardStateController, - WakefulnessLifecycle wakefulnessLifecycle, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) { super(scope); mKeyguardStateController = keyguardStateController; mWakefulnessLifecycle = wakefulnessLifecycle; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardInteractor = keyguardInteractor; + mJavaAdapter = javaAdapter; } @Override @@ -77,6 +86,11 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.addCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow( + mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> { + mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo()); + updateState(); + }); } @Override @@ -84,6 +98,7 @@ public class DeviceInactiveCondition extends Condition { mKeyguardStateController.removeCallback(mKeyguardStateCallback); mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback); mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); + mAnyDozeListenerJob.cancel(null); } @Override @@ -92,10 +107,10 @@ public class DeviceInactiveCondition extends Condition { } private void updateState() { - final boolean asleep = - mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP - || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP; - updateCondition(asleep || mKeyguardStateController.isShowing() - || mKeyguardUpdateMonitor.isDreaming()); + final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP; + // Doze/AoD is also a dream, but we should never override it with low light as to the user + // it's totally unrelated. + updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing() + || mKeyguardUpdateMonitor.isDreaming())); } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index fcc3ea9f7d58..fed77090c477 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification @@ -60,6 +61,7 @@ import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController import com.android.systemui.unfold.DisplaySwitchLatencyTracker +import com.android.systemui.unfold.NoCooldownDisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -67,8 +69,10 @@ import com.android.systemui.wallpapers.dagger.WallpaperModule import com.android.systemui.wmshell.WMShell import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Provider /** * DEPRECATED: DO NOT ADD THINGS TO THIS FILE. @@ -148,12 +152,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable - /** Inject into DisplaySwitchLatencyTracker. */ - @Binds - @IntoMap - @ClassKey(DisplaySwitchLatencyTracker::class) - abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable - /** Inject into NotificationChannels. */ @Binds @IntoMap @@ -353,4 +351,15 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(ComplicationTypesUpdater::class) abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable + + companion object { + @Provides + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + fun provideDisplaySwitchLatencyTracker( + noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>, + coolDownVariant: Provider<DisplaySwitchLatencyTracker>, + ): CoreStartable = + if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get() + } } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java index 8469cb4ab565..f8072f2f79b4 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java @@ -78,7 +78,7 @@ public abstract class LowLightModule { @Provides @IntoSet - @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS) + @Named(LOW_LIGHT_PRECONDITIONS) static Condition provideLowLightCondition(LowLightCondition lowLightCondition, DirectBootCondition directBootCondition) { // Start lowlight if we are either in lowlight or in direct boot. The ordering of the diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS index 0ec996be72de..9b4902a9e7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS @@ -6,5 +6,4 @@ madym@google.com mgalhardo@google.com petrcermak@google.com stevenckng@google.com -tkachenkoi@google.com -vanjan@google.com
\ No newline at end of file +vanjan@google.com diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index f5aac720fd47..cd401d5deb6e 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -19,8 +19,11 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.app.tracing.TraceUtils.traceAsync import com.android.app.tracing.instantForTrack +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,10 +33,12 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.util.Compile import com.android.systemui.util.Utils.isDeviceFoldable @@ -42,17 +47,23 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.race import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.measureTimeMillis -import java.time.Duration import java.util.concurrent.Executor import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout /** @@ -73,63 +84,96 @@ constructor( @Application private val applicationScope: CoroutineScope, private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, private val systemClock: SystemClock, - private val deviceStateManager: DeviceStateManager + private val deviceStateManager: DeviceStateManager, + private val latencyTracker: LatencyTracker, ) : CoreStartable { private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() private val isAodEnabled: Boolean get() = keyguardInteractor.isAodAvailable.value + private val displaySwitchStarted = + deviceStateRepository.state.pairwise().filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + + private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow()) + + private var isCoolingDown = false + override fun start() { if (!isDeviceFoldable(context.resources, deviceStateManager)) { return } applicationScope.launch(context = backgroundDispatcher) { - deviceStateRepository.state - .pairwise() - .filter { - // Start tracking only when the foldable device is - // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or - // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) - foldableDeviceState -> - foldableDeviceState.previousValue == DeviceState.FOLDED || - foldableDeviceState.newValue == DeviceState.FOLDED + displaySwitchStarted.collectLatest { (previousState, newState) -> + if (isCoolingDown) return@collectLatest + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + instantForTrack(TAG) { "unfold latency tracking started" } } - .flatMapLatest { foldableDeviceState -> - flow { - var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() - val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withBeforeFields( - foldableDeviceState.previousValue.toStatsInt() - ) - + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + val event = + DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt()) val displaySwitchTimeMs = measureTimeMillis(systemClock) { - try { - withTimeout(SCREEN_EVENT_TIMEOUT) { - traceAsync(TAG, "displaySwitch") { - waitForDisplaySwitch(toFoldableDeviceState) - } - } - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "Wait for display switch timed out") + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(newState.toStatsInt()) } } - - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withAfterFields( - toFoldableDeviceState, - displaySwitchTimeMs.toInt(), - getCurrentState() - ) - emit(displaySwitchLatencyEvent) + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + logDisplaySwitchEvent(event, newState, displaySwitchTimeMs) } + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { "tracking timed out" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } catch (e: CancellationException) { + instantForTrack(TAG) { "new state interrupted, entering cool down" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + startCoolDown() } - .collect { displaySwitchLatencyLogger.log(it) } + } } } + @OptIn(FlowPreview::class) + private fun startCoolDown() { + if (isCoolingDown) return + isCoolingDown = true + applicationScope.launch(context = backgroundDispatcher) { + val startTime = systemClock.elapsedRealtime() + try { + startOrEndEvent.timeout(COOL_DOWN_DURATION).collect() + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { + "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms" + } + isCoolingDown = false + } + } + } + + private fun logDisplaySwitchEvent( + event: DisplaySwitchLatencyEvent, + toFoldableDeviceState: DeviceState, + displaySwitchTimeMs: Long, + ) { + displaySwitchLatencyLogger.log( + event.withAfterFields( + toFoldableDeviceState.toStatsInt(), + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + ) + } + private fun DeviceState.toStatsInt(): Int = when (this) { DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED @@ -152,9 +196,20 @@ constructor( } } + private fun anyEndEventFlow(): Flow<Any> { + val unfoldStatus = + unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted } + // dropping first emission as we're only interested in new emissions, not current state + val screenOn = + powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON } + val goToSleep = + powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) } + return merge(screenOn, goToSleep, unfoldStatus) + } + private fun shouldWaitForTransitionStart( toFoldableDeviceState: Int, - isTransitionEnabled: Boolean + isTransitionEnabled: Boolean, ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) private suspend fun waitForScreenTurnedOn() { @@ -165,12 +220,13 @@ constructor( private suspend fun waitForGoToSleepWithScreenOff() { traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { - powerInteractor.detailedWakefulness - .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } - .first() + powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first() } } + private fun sleepWithScreenOff(model: WakefulnessModel) = + model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled + private fun getCurrentState(): Int = when { isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD @@ -205,7 +261,7 @@ constructor( private fun DisplaySwitchLatencyEvent.withAfterFields( toFoldableDeviceState: Int, displaySwitchTimeMs: Int, - toState: Int + toState: Int, ): DisplaySwitchLatencyEvent { log { "toFoldableDeviceState=$toFoldableDeviceState, " + @@ -217,7 +273,7 @@ constructor( return copy( toFoldableDeviceState = toFoldableDeviceState, latencyMs = displaySwitchTimeMs, - toState = toState + toState = toState, ) } @@ -250,14 +306,15 @@ constructor( val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, - val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN + val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN, ) companion object { private const val VALUE_UNKNOWN = -1 private const val TAG = "DisplaySwitchLatency" private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) - private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds + @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds private const val FOLDABLE_DEVICE_STATE_UNKNOWN = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..6ac0bb168f18 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold + +import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import android.util.Log +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.instantForTrack +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.Compile +import com.android.systemui.util.Utils.isDeviceFoldable +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.race +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.time.Duration +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withTimeout + +/** + * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which + * version is used for tracking depends on [unfoldLatencyTrackingFix] flag. + */ +@SysUISingleton +class NoCooldownDisplaySwitchLatencyTracker +@Inject +constructor( + private val context: Context, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val animationStatusRepository: AnimationStatusRepository, + private val keyguardInteractor: KeyguardInteractor, + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor, + @Application private val applicationScope: CoroutineScope, + private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, + private val systemClock: SystemClock, + private val deviceStateManager: DeviceStateManager, +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + private val isAodEnabled: Boolean + get() = keyguardInteractor.isAodAvailable.value + + override fun start() { + if (!isDeviceFoldable(context.resources, deviceStateManager)) { + return + } + applicationScope.launch(context = backgroundDispatcher) { + deviceStateRepository.state + .pairwise() + .filter { + // Start tracking only when the foldable device is + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + .flatMapLatest { foldableDeviceState -> + flow { + var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() + val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withBeforeFields( + foldableDeviceState.previousValue.toStatsInt() + ) + + val displaySwitchTimeMs = + measureTimeMillis(systemClock) { + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "Wait for display switch timed out") + } + } + + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState, + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + emit(displaySwitchLatencyEvent) + } + } + .collect { displaySwitchLatencyLogger.log(it) } + } + } + + private fun DeviceState.toStatsInt(): Int = + when (this) { + DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED + DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN + DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN + DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED + else -> FOLDABLE_DEVICE_STATE_UNKNOWN + } + + private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) { + val isTransitionEnabled = + unfoldTransitionInteractor.isAvailable && + animationStatusRepository.areAnimationsEnabled().first() + if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } else { + race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() }) + } + } + + private fun shouldWaitForTransitionStart( + toFoldableDeviceState: Int, + isTransitionEnabled: Boolean, + ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) + + private suspend fun waitForScreenTurnedOn() { + traceAsync(TAG, "waitForScreenTurnedOn()") { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + } + + private suspend fun waitForGoToSleepWithScreenOff() { + traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { + powerInteractor.detailedWakefulness + .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } + .first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled) + + private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled) + + private fun isAsleepDueToFold(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD)) + } + + private inline fun log(msg: () -> String) { + if (DEBUG) Log.d(TAG, msg()) + } + + private fun DisplaySwitchLatencyEvent.withBeforeFields( + fromFoldableDeviceState: Int + ): DisplaySwitchLatencyEvent { + log { "fromFoldableDeviceState=$fromFoldableDeviceState" } + instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } + + return copy(fromFoldableDeviceState = fromFoldableDeviceState) + } + + private fun DisplaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState: Int, + displaySwitchTimeMs: Int, + toState: Int, + ): DisplaySwitchLatencyEvent { + log { + "toFoldableDeviceState=$toFoldableDeviceState, " + + "toState=$toState, " + + "latencyMs=$displaySwitchTimeMs" + } + instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" } + + return copy( + toFoldableDeviceState = toFoldableDeviceState, + latencyMs = displaySwitchTimeMs, + toState = toState, + ) + } + + companion object { + private const val VALUE_UNKNOWN = -1 + private const val TAG = "DisplaySwitchLatency" + private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + + private const val FOLDABLE_DEVICE_STATE_UNKNOWN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN + const val FOLDABLE_DEVICE_STATE_CLOSED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED + const val FOLDABLE_DEVICE_STATE_HALF_OPEN = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED + private const val FOLDABLE_DEVICE_STATE_OPEN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED + private const val FOLDABLE_DEVICE_STATE_FLIPPED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt index f806a5c52d5a..9248cc801227 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt @@ -22,6 +22,7 @@ import android.hardware.devicestate.DeviceStateManager import android.os.Trace import android.util.Log import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -63,7 +64,7 @@ constructor( /** Registers for relevant events only if the device is foldable. */ fun init() { - if (!isFoldable) { + if (unfoldLatencyTrackingFix() || !isFoldable) { return } deviceStateManager.registerCallback(uiBgExecutor, foldStateListener) @@ -85,7 +86,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -109,7 +110,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -161,7 +162,7 @@ constructor( Log.d( TAG, "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " + - "isTransitionEnabled = $isTransitionEnabled" + "isTransitionEnabled = $isTransitionEnabled", ) } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index 885a2b0d7305..c2f86a37c6d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted @@ -48,6 +49,9 @@ constructor( val isAvailable: Boolean get() = repository.isAvailable + /** Flow of latest [UnfoldTransitionStatus] changes */ + val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus + /** * This mapping emits 1 when the device is completely unfolded and 0.0 when the device is * completely folded. diff --git a/services/accessibility/OWNERS b/services/accessibility/OWNERS index 4e1175034b5b..ab1e9ffe3bfe 100644 --- a/services/accessibility/OWNERS +++ b/services/accessibility/OWNERS @@ -1,4 +1,7 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. # Android Accessibility Framework owners danielnorman@google.com diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index e8dddcb537cd..529a564ea607 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -100,6 +100,13 @@ flag { } flag { + name: "enable_low_vision_generic_feedback" + namespace: "accessibility" + description: "Use generic feedback for low vision." + bug: "393981463" +} + +flag { name: "enable_low_vision_hats" namespace: "accessibility" description: "Use HaTS for low vision feedback." diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..ff812ad7e7e6 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +juchengchou@google.com +chenjean@google.com +chihtinglo@google.com diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 350ecab1dd5f..d2a5734f323f 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -163,6 +163,10 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; import com.android.server.storage.StorageSessionController; import com.android.server.storage.StorageSessionController.ExternalStorageServiceException; +import com.android.server.storage.WatchedVolumeInfo; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchedArrayMap; +import com.android.server.utils.Watcher; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver; @@ -452,7 +456,7 @@ class StorageManagerService extends IStorageManager.Stub private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>(); /** Map from volume ID to disk */ @GuardedBy("mLock") - private final ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>(); + private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>(); /** Map from UUID to record */ @GuardedBy("mLock") @@ -503,9 +507,9 @@ class StorageManagerService extends IStorageManager.Stub "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?"); - private VolumeInfo findVolumeByIdOrThrow(String id) { + private WatchedVolumeInfo findVolumeByIdOrThrow(String id) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(id); + final WatchedVolumeInfo vol = mVolumes.get(id); if (vol != null) { return vol; } @@ -516,9 +520,9 @@ class StorageManagerService extends IStorageManager.Stub private VolumeRecord findRecordForPath(String path) { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.path != null && path.startsWith(vol.path)) { - return mRecords.get(vol.fsUuid); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) { + return mRecords.get(vol.getFsUuid()); } } } @@ -764,7 +768,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_MOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; if (isMountDisallowed(vol)) { Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy"); break; @@ -774,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_UNMOUNT: { - final VolumeInfo vol = (VolumeInfo) msg.obj; + final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; unmount(vol); break; } @@ -828,7 +832,8 @@ class StorageManagerService extends IStorageManager.Stub } case H_VOLUME_STATE_CHANGED: { final SomeArgs args = (SomeArgs) msg.obj; - onVolumeStateChangedAsync((VolumeInfo) args.arg1, args.argi1, args.argi2); + onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1, + args.argi2); args.recycle(); break; } @@ -892,9 +897,9 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final int size = mVolumes.size(); for (int i = 0; i < size; i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (vol.mountUserId == userId) { - vol.mountUserId = UserHandle.USER_NULL; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (vol.getMountUserId() == userId) { + vol.setMountUserId(UserHandle.USER_NULL); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); } } @@ -1084,7 +1089,7 @@ class StorageManagerService extends IStorageManager.Stub VolumeInfo.TYPE_PRIVATE, null, null); internal.state = VolumeInfo.STATE_MOUNTED; internal.path = Environment.getDataDirectory().getAbsolutePath(); - mVolumes.put(internal.id, internal); + mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal)); } private void resetIfBootedAndConnected() { @@ -1242,7 +1247,7 @@ class StorageManagerService extends IStorageManager.Stub } } for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) { final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false); mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget(); @@ -1291,21 +1296,21 @@ class StorageManagerService extends IStorageManager.Stub } private void maybeRemountVolumes(int userId) { - List<VolumeInfo> volumesToRemount = new ArrayList<>(); + List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { // If there's a visible secondary volume mounted, // we need to update the currentUserId and remount - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); volumesToRemount.add(vol); } } } - for (VolumeInfo vol : volumesToRemount) { + for (WatchedVolumeInfo vol : volumesToRemount) { Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol); mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); @@ -1317,12 +1322,12 @@ class StorageManagerService extends IStorageManager.Stub * trying to mount doesn't have the same mount user id as the current user being maintained by * StorageManagerService and change the mount Id. The checks are same as * {@link StorageManagerService#maybeRemountVolumes(int)} - * @param VolumeInfo object to consider for changing the mountId + * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId */ - private void updateVolumeMountIdIfRequired(VolumeInfo vol) { + private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) { synchronized (mLock) { if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); } } } @@ -1485,20 +1490,21 @@ class StorageManagerService extends IStorageManager.Stub final DiskInfo disk = mDisks.get(diskId); final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid); vol.mountUserId = userId; - mVolumes.put(volId, vol); - onVolumeCreatedLocked(vol); + WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol); + mVolumes.put(volId, watchedVol); + onVolumeCreatedLocked(watchedVol); } } @Override public void onVolumeStateChanged(String volId, final int newState, final int userId) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - final int oldState = vol.state; - vol.state = newState; - final VolumeInfo vInfo = new VolumeInfo(vol); - vInfo.mountUserId = userId; + final int oldState = vol.getState(); + vol.setState(newState); + final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol); + vInfo.setMountUserId(userId); final SomeArgs args = SomeArgs.obtain(); args.arg1 = vInfo; args.argi1 = oldState; @@ -1513,11 +1519,11 @@ class StorageManagerService extends IStorageManager.Stub public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid, String fsLabel) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.fsType = fsType; - vol.fsUuid = fsUuid; - vol.fsLabel = fsLabel; + vol.setFsType(fsType); + vol.setFsUuid(fsUuid); + vol.setFsLabel(fsLabel); } } } @@ -1525,9 +1531,9 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumePathChanged(String volId, String path) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.path = path; + vol.setFsPath(path); } } } @@ -1535,24 +1541,24 @@ class StorageManagerService extends IStorageManager.Stub @Override public void onVolumeInternalPathChanged(String volId, String internalPath) { synchronized (mLock) { - final VolumeInfo vol = mVolumes.get(volId); + final WatchedVolumeInfo vol = mVolumes.get(volId); if (vol != null) { - vol.internalPath = internalPath; + vol.setInternalPath(internalPath); } } } @Override public void onVolumeDestroyed(String volId) { - VolumeInfo vol = null; + WatchedVolumeInfo vol = null; synchronized (mLock) { vol = mVolumes.remove(volId); } if (vol != null) { - mStorageSessionController.onVolumeRemove(vol); + mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo()); try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { @@ -1566,7 +1572,7 @@ class StorageManagerService extends IStorageManager.Stub private void onDiskScannedLocked(DiskInfo disk) { int volumeCount = 0; for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (Objects.equals(disk.id, vol.getDiskId())) { volumeCount++; } @@ -1589,19 +1595,19 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeCreatedLocked(VolumeInfo vol) { + private void onVolumeCreatedLocked(WatchedVolumeInfo vol) { final ActivityManagerInternal amInternal = LocalServices.getService(ActivityManagerInternal.class); - if (vol.mountUserId >= 0 && !amInternal.isUserRunning(vol.mountUserId, 0)) { + if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + " is no longer running."); + + Integer.toString(vol.getMountUserId()) + " is no longer running."); return; } - if (vol.type == VolumeInfo.TYPE_EMULATED) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = (volumeUserContext != null) ? volumeUserContext.getSystemService( @@ -1611,60 +1617,60 @@ class StorageManagerService extends IStorageManager.Stub // should not be skipped even if media provider instance is not running in that user // space if (!isMediaSharedWithParent - && !mStorageSessionController.supportsExternalStorage(vol.mountUserId)) { + && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) { Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user " - + Integer.toString(vol.mountUserId) + + Integer.toString(vol.getMountUserId()) + " does not support external storage."); return; } final StorageManager storage = mContext.getSystemService(StorageManager.class); - final VolumeInfo privateVol = storage.findPrivateForEmulated(vol); + final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo()); if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid) && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) - || Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) { + || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } - } else if (vol.type == VolumeInfo.TYPE_PUBLIC) { + } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) { // TODO: only look at first public partition if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid) - && vol.disk.isDefaultPrimary()) { + && vol.getDisk().isDefaultPrimary()) { Slog.v(TAG, "Found primary storage at " + vol); - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY; - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY); + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } // Adoptable public disks are visible to apps, since they meet // public API requirement of being in a stable location. - if (vol.disk.isAdoptable()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + if (vol.getDisk().isAdoptable()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_PRIVATE) { + } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); - } else if (vol.type == VolumeInfo.TYPE_STUB) { - if (vol.disk.isStubVisible()) { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE; + } else if (vol.getType() == VolumeInfo.TYPE_STUB) { + if (vol.getDisk().isStubVisible()) { + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE); } else { - vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ; + vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ); } - vol.mountUserId = mCurrentUserId; + vol.setMountUserId(mCurrentUserId); mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); } else { Slog.d(TAG, "Skipping automatic mounting of " + vol); } } - private boolean isBroadcastWorthy(VolumeInfo vol) { + private boolean isBroadcastWorthy(WatchedVolumeInfo vol) { switch (vol.getType()) { case VolumeInfo.TYPE_PRIVATE: case VolumeInfo.TYPE_PUBLIC: @@ -1691,8 +1697,8 @@ class StorageManagerService extends IStorageManager.Stub } @GuardedBy("mLock") - private void onVolumeStateChangedLocked(VolumeInfo vol, int newState) { - if (vol.type == VolumeInfo.TYPE_EMULATED) { + private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) { + if (vol.getType() == VolumeInfo.TYPE_EMULATED) { if (newState != VolumeInfo.STATE_MOUNTED) { mFuseMountedUser.remove(vol.getMountUserId()); } else if (mVoldAppDataIsolationEnabled){ @@ -1741,7 +1747,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void onVolumeStateChangedAsync(VolumeInfo vol, int oldState, int newState) { + private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) { if (newState == VolumeInfo.STATE_MOUNTED) { // Private volumes can be unmounted and re-mounted even after a user has // been unlocked; on devices that support encryption keys tied to the filesystem, @@ -1751,7 +1757,7 @@ class StorageManagerService extends IStorageManager.Stub } catch (Exception e) { // Unusable partition, unmount. try { - mVold.unmount(vol.id); + mVold.unmount(vol.getId()); } catch (Exception ee) { Slog.wtf(TAG, ee); } @@ -1762,20 +1768,20 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { // Remember that we saw this volume so we're ready to accept user // metadata, or so we can annoy them when a private volume is ejected - if (!TextUtils.isEmpty(vol.fsUuid)) { - VolumeRecord rec = mRecords.get(vol.fsUuid); + if (!TextUtils.isEmpty(vol.getFsUuid())) { + VolumeRecord rec = mRecords.get(vol.getFsUuid()); if (rec == null) { - rec = new VolumeRecord(vol.type, vol.fsUuid); - rec.partGuid = vol.partGuid; + rec = new VolumeRecord(vol.getType(), vol.getFsUuid()); + rec.partGuid = vol.getPartGuid(); rec.createdMillis = System.currentTimeMillis(); - if (vol.type == VolumeInfo.TYPE_PRIVATE) { - rec.nickname = vol.disk.getDescription(); + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { + rec.nickname = vol.getDisk().getDescription(); } mRecords.put(rec.fsUuid, rec); } else { // Handle upgrade case where we didn't store partition GUID if (TextUtils.isEmpty(rec.partGuid)) { - rec.partGuid = vol.partGuid; + rec.partGuid = vol.getPartGuid(); } } @@ -1788,7 +1794,7 @@ class StorageManagerService extends IStorageManager.Stub // before notifying other listeners. // Intentionally called without the mLock to avoid deadlocking from the Storage Service. try { - mStorageSessionController.notifyVolumeStateChanged(vol); + mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo()); } catch (ExternalStorageServiceException e) { Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e); } @@ -1799,9 +1805,9 @@ class StorageManagerService extends IStorageManager.Stub // processes that receive the intent unnecessarily. if (mBootCompleted && isBroadcastWorthy(vol)) { final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED); - intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState); - intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid); + intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid()); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget(); @@ -1826,8 +1832,8 @@ class StorageManagerService extends IStorageManager.Stub } } - if ((vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_STUB) - && vol.state == VolumeInfo.STATE_EJECTING) { + if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB) + && vol.getState() == VolumeInfo.STATE_EJECTING) { // TODO: this should eventually be handled by new ObbVolume state changes /* * Some OBBs might have been unmounted when this volume was @@ -1835,7 +1841,7 @@ class StorageManagerService extends IStorageManager.Stub * remove those from the list of mounted OBBS. */ mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage( - OBB_FLUSH_MOUNT_STATE, vol.path)); + OBB_FLUSH_MOUNT_STATE, vol.getFsPath())); } maybeLogMediaMount(vol, newState); } @@ -1860,7 +1866,7 @@ class StorageManagerService extends IStorageManager.Stub } } - private void maybeLogMediaMount(VolumeInfo vol, int newState) { + private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) { if (!SecurityLog.isLoggingEnabled()) { return; } @@ -1875,10 +1881,10 @@ class StorageManagerService extends IStorageManager.Stub if (newState == VolumeInfo.STATE_MOUNTED || newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label); } else if (newState == VolumeInfo.STATE_UNMOUNTED || newState == VolumeInfo.STATE_BAD_REMOVAL) { - SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.path, label); + SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label); } } @@ -1920,18 +1926,18 @@ class StorageManagerService extends IStorageManager.Stub /** * Decide if volume is mountable per device policies. */ - private boolean isMountDisallowed(VolumeInfo vol) { + private boolean isMountDisallowed(WatchedVolumeInfo vol) { UserManager userManager = mContext.getSystemService(UserManager.class); boolean isUsbRestricted = false; - if (vol.disk != null && vol.disk.isUsb()) { + if (vol.getDisk() != null && vol.getDisk().isUsb()) { isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, Binder.getCallingUserHandle()); } boolean isTypeRestricted = false; - if (vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_PRIVATE - || vol.type == VolumeInfo.TYPE_STUB) { + if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE + || vol.getType() == VolumeInfo.TYPE_STUB) { isTypeRestricted = userManager .hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA, Binder.getCallingUserHandle()); @@ -1967,6 +1973,13 @@ class StorageManagerService extends IStorageManager.Stub mContext = context; mCallbacks = new Callbacks(FgThread.get().getLooper()); + mVolumes.registerObserver(new Watcher() { + @Override + public void onChange(Watchable what) { + // When we change the list or any volume contained in it, invalidate the cache + StorageManager.invalidateVolumeListCache(); + } + }); HandlerThread hthread = new HandlerThread(TAG); hthread.start(); mHandler = new StorageManagerServiceHandler(hthread.getLooper()); @@ -2339,7 +2352,7 @@ class StorageManagerService extends IStorageManager.Stub super.mount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); if (isMountDisallowed(vol)) { throw new SecurityException("Mounting " + volId + " restricted by policy"); } @@ -2365,23 +2378,24 @@ class StorageManagerService extends IStorageManager.Stub } } - private void mount(VolumeInfo vol) { + private void mount(WatchedVolumeInfo vol) { try { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId()); // TODO(b/135341433): Remove cautious logging when FUSE is stable Slog.i(TAG, "Mounting volume " + vol); extendWatchdogTimeout("#mount might be slow"); - mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() { + mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(), + new IVoldMountCallback.Stub() { @Override public boolean onVolumeChecking(FileDescriptor fd, String path, String internalPath) { - vol.path = path; - vol.internalPath = internalPath; + vol.setFsPath(path); + vol.setInternalPath(internalPath); ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd); try { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - "SMS.startFuseFileSystem: " + vol.id); - mStorageSessionController.onVolumeMount(pfd, vol); + "SMS.startFuseFileSystem: " + vol.getId()); + mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo()); return true; } catch (ExternalStorageServiceException e) { Slog.e(TAG, "Failed to mount volume " + vol, e); @@ -2416,21 +2430,21 @@ class StorageManagerService extends IStorageManager.Stub super.unmount_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); unmount(vol); } - private void unmount(VolumeInfo vol) { + private void unmount(WatchedVolumeInfo vol) { try { try { - if (vol.type == VolumeInfo.TYPE_PRIVATE) { + if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); } } catch (Installer.InstallerException e) { Slog.e(TAG, "Failed unmount mirror data", e); } - mVold.unmount(vol.id); - mStorageSessionController.onVolumeUnmount(vol); + mVold.unmount(vol.getId()); + mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo()); } catch (Exception e) { Slog.wtf(TAG, e); } @@ -2442,10 +2456,10 @@ class StorageManagerService extends IStorageManager.Stub super.format_enforcePermission(); - final VolumeInfo vol = findVolumeByIdOrThrow(volId); - final String fsUuid = vol.fsUuid; + final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); + final String fsUuid = vol.getFsUuid(); try { - mVold.format(vol.id, "auto"); + mVold.format(vol.getId(), "auto"); // After a successful format above, we should forget about any // records for the old partition, since it'll never appear again @@ -3105,7 +3119,7 @@ class StorageManagerService extends IStorageManager.Stub private void warnOnNotMounted() { synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary() && vol.isMountedWritable()) { // Cool beans, we have a mounted primary volume return; @@ -3392,8 +3406,8 @@ class StorageManagerService extends IStorageManager.Stub } } - private void prepareUserStorageIfNeeded(VolumeInfo vol) throws Exception { - if (vol.type != VolumeInfo.TYPE_PRIVATE) { + private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception { + if (vol.getType() != VolumeInfo.TYPE_PRIVATE) { return; } @@ -3411,7 +3425,7 @@ class StorageManagerService extends IStorageManager.Stub continue; } - prepareUserStorageInternal(vol.fsUuid, user.id, flags); + prepareUserStorageInternal(vol.getFsUuid(), user.id, flags); } } @@ -3960,7 +3974,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final String volId = mVolumes.keyAt(i); - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); switch (vol.getType()) { case VolumeInfo.TYPE_PUBLIC: case VolumeInfo.TYPE_STUB: @@ -4112,7 +4126,7 @@ class StorageManagerService extends IStorageManager.Stub synchronized (mLock) { final VolumeInfo[] res = new VolumeInfo[mVolumes.size()]; for (int i = 0; i < mVolumes.size(); i++) { - res[i] = mVolumes.valueAt(i); + res[i] = mVolumes.valueAt(i).getVolumeInfo(); } return res; } @@ -4708,7 +4722,8 @@ class StorageManagerService extends IStorageManager.Stub break; } case MSG_VOLUME_STATE_CHANGED: { - callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3); + VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo(); + callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3); break; } case MSG_VOLUME_RECORD_CHANGED: { @@ -4738,7 +4753,7 @@ class StorageManagerService extends IStorageManager.Stub obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget(); } - private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { + private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) { final SomeArgs args = SomeArgs.obtain(); args.arg1 = vol.clone(); args.argi2 = oldState; @@ -4790,8 +4805,8 @@ class StorageManagerService extends IStorageManager.Stub pw.println("Volumes:"); pw.increaseIndent(); for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); - if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) continue; + final WatchedVolumeInfo vol = mVolumes.valueAt(i); + if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue; vol.dump(pw); } pw.decreaseIndent(); @@ -5088,7 +5103,7 @@ class StorageManagerService extends IStorageManager.Stub final List<String> primaryVolumeIds = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { - final VolumeInfo vol = mVolumes.valueAt(i); + final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.isPrimary()) { primaryVolumeIds.add(vol.getId()); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 6cca7d16842a..cce29592d912 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8302,8 +8302,6 @@ public final class ActiveServices { if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) { @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges); - // We store them to compare the old and new while-in-use logics to each other. - // (They're not used for any other purposes.) if (allowWiu == REASON_DENIED) { allowWiu = allowWhileInUse; } @@ -8706,6 +8704,7 @@ public final class ActiveServices { + ",duration:" + tempAllowListReason.mDuration + ",callingUid:" + tempAllowListReason.mCallingUid)) + ">" + + "; allowWiu:" + allowWhileInUse + "; targetSdkVersion:" + r.appInfo.targetSdkVersion + "; callerTargetSdkVersion:" + callerTargetSdkVersion + "; startForegroundCount:" + r.mStartForegroundCount diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java new file mode 100644 index 000000000000..9d60a576d9bc --- /dev/null +++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 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.storage; + +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.File; + +/** + * An immutable version of {@link VolumeInfo} with only getters. + * + * @hide + */ +public final class ImmutableVolumeInfo { + private final VolumeInfo mVolumeInfo; + + private ImmutableVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = new VolumeInfo(volumeInfo); + } + + public static ImmutableVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new ImmutableVolumeInfo(info); + } + + public ImmutableVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); // Return a copy, not the original + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +} diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index b9c9b64cd2c6..342b864c6473 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -45,6 +45,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.server.storage.ImmutableVolumeInfo; import java.util.Objects; @@ -79,18 +80,18 @@ public final class StorageSessionController { * @param vol for which the storage session has to be started * @return userId for connection for this volume */ - public int getConnectionUserIdForVolume(VolumeInfo vol) { + public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) { final Context volumeUserContext = mContext.createContextAsUser( - UserHandle.of(vol.mountUserId), 0); + UserHandle.of(vol.getMountUserId()), 0); boolean isMediaSharedWithParent = volumeUserContext.getSystemService( UserManager.class).isMediaSharedWithParent(); - UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId); + UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId()); if (userInfo != null && isMediaSharedWithParent) { // Clones use the same connection as their parent return userInfo.profileGroupId; } else { - return vol.mountUserId; + return vol.getMountUserId(); } } @@ -108,7 +109,7 @@ public final class StorageSessionController { * @throws ExternalStorageServiceException if the session fails to start * @throws IllegalStateException if a session has already been created for {@code vol} */ - public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol) + public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol) throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; @@ -144,7 +145,8 @@ public final class StorageSessionController { * * @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService */ - public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException { + public void notifyVolumeStateChanged(ImmutableVolumeInfo vol) + throws ExternalStorageServiceException { if (!shouldHandle(vol)) { return; } @@ -214,7 +216,7 @@ public final class StorageSessionController { * @return the connection that was removed or {@code null} if nothing was removed */ @Nullable - public StorageUserConnection onVolumeRemove(VolumeInfo vol) { + public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) { if (!shouldHandle(vol)) { return null; } @@ -246,7 +248,7 @@ public final class StorageSessionController { * * Call {@link #onVolumeRemove} to remove the connection without waiting for exit */ - public void onVolumeUnmount(VolumeInfo vol) { + public void onVolumeUnmount(ImmutableVolumeInfo vol) { String sessionId = vol.getId(); final long token = Binder.clearCallingIdentity(); try { @@ -457,9 +459,9 @@ public final class StorageSessionController { * Returns {@code true} if {@code vol} is an emulated or visible public volume, * {@code false} otherwise **/ - public static boolean isEmulatedOrPublic(VolumeInfo vol) { - return vol.type == VolumeInfo.TYPE_EMULATED - || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); + public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) { + return vol.getType() == VolumeInfo.TYPE_EMULATED + || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible()); } /** Exception thrown when communication with the {@link ExternalStorageService} fails. */ @@ -477,11 +479,11 @@ public final class StorageSessionController { } } - private static boolean isSupportedVolume(VolumeInfo vol) { - return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB; + private static boolean isSupportedVolume(ImmutableVolumeInfo vol) { + return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB; } - private boolean shouldHandle(@Nullable VolumeInfo vol) { + private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) { return !mIsResetting && (vol == null || isSupportedVolume(vol)); } diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java new file mode 100644 index 000000000000..4124cfb4f092 --- /dev/null +++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java @@ -0,0 +1,206 @@ +/* + * 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.storage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.storage.DiskInfo; +import android.os.storage.StorageVolume; +import android.os.storage.VolumeInfo; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.utils.Watchable; +import com.android.server.utils.WatchableImpl; + +import java.io.File; + +/** + * A wrapper for {@link VolumeInfo} implementing the {@link Watchable} interface. + * + * The {@link VolumeInfo} class itself cannot safely implement Watchable, because it has several + * UnsupportedAppUsage annotations and public fields, which allow it to be modified without + * notifying watchers. + * + * @hide + */ +public class WatchedVolumeInfo extends WatchableImpl { + private final VolumeInfo mVolumeInfo; + + private WatchedVolumeInfo(VolumeInfo volumeInfo) { + mVolumeInfo = volumeInfo; + } + + public WatchedVolumeInfo(WatchedVolumeInfo watchedVolumeInfo) { + mVolumeInfo = new VolumeInfo(watchedVolumeInfo.mVolumeInfo); + } + + public static WatchedVolumeInfo fromVolumeInfo(VolumeInfo info) { + return new WatchedVolumeInfo(info); + } + + /** + * Returns a copy of the embedded VolumeInfo object, to be used by components + * that just need it for retrieving some state from it. + * + * @return A copy of the embedded VolumeInfo object + */ + + public WatchedVolumeInfo clone() { + return fromVolumeInfo(mVolumeInfo.clone()); + } + + public ImmutableVolumeInfo getImmutableVolumeInfo() { + return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo); + } + + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { + return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); + } + + public void dump(IndentingPrintWriter pw) { + mVolumeInfo.dump(pw); + } + + public DiskInfo getDisk() { + return mVolumeInfo.getDisk(); + } + + public String getDiskId() { + return mVolumeInfo.getDiskId(); + } + + public String getFsLabel() { + return mVolumeInfo.fsLabel; + } + + public void setFsLabel(String fsLabel) { + mVolumeInfo.fsLabel = fsLabel; + dispatchChange(this); + } + + public String getFsPath() { + return mVolumeInfo.path; + } + + public void setFsPath(String path) { + mVolumeInfo.path = path; + dispatchChange(this); + } + + public String getFsType() { + return mVolumeInfo.fsType; + } + + public void setFsType(String fsType) { + mVolumeInfo.fsType = fsType; + dispatchChange(this); + } + + public @Nullable String getFsUuid() { + return mVolumeInfo.fsUuid; + } + + public void setFsUuid(String fsUuid) { + mVolumeInfo.fsUuid = fsUuid; + dispatchChange(this); + } + + public @NonNull String getId() { + return mVolumeInfo.id; + } + + public File getInternalPath() { + return mVolumeInfo.getInternalPath(); + } + + public void setInternalPath(String internalPath) { + mVolumeInfo.internalPath = internalPath; + dispatchChange(this); + } + + public int getMountFlags() { + return mVolumeInfo.mountFlags; + } + + public void setMountFlags(int mountFlags) { + mVolumeInfo.mountFlags = mountFlags; + dispatchChange(this); + } + + public int getMountUserId() { + return mVolumeInfo.mountUserId; + } + + public void setMountUserId(int mountUserId) { + mVolumeInfo.mountUserId = mountUserId; + dispatchChange(this); + } + + public String getPartGuid() { + return mVolumeInfo.partGuid; + } + + public File getPath() { + return mVolumeInfo.getPath(); + } + + public int getState() { + return mVolumeInfo.state; + } + + public int getState(int state) { + return mVolumeInfo.state; + } + + public void setState(int state) { + mVolumeInfo.state = state; + dispatchChange(this); + } + + public int getType() { + return mVolumeInfo.type; + } + + public VolumeInfo getVolumeInfo() { + return new VolumeInfo(mVolumeInfo); + } + + public boolean isMountedReadable() { + return mVolumeInfo.isMountedReadable(); + } + + public boolean isMountedWritable() { + return mVolumeInfo.isMountedWritable(); + } + + public boolean isPrimary() { + return mVolumeInfo.isPrimary(); + } + + public boolean isVisible() { + return mVolumeInfo.isVisible(); + } + + public boolean isVisibleForUser(int userId) { + return mVolumeInfo.isVisibleForUser(userId); + } + + public boolean isVisibleForWrite(int userId) { + return mVolumeInfo.isVisibleForWrite(userId); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b17eef85f93d..d84016b3816e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -246,9 +246,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WIND import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES; -import static com.android.server.wm.WindowManagerService.sEnableShellTransitions; import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; @@ -764,13 +762,6 @@ final class ActivityRecord extends WindowToken { boolean mLastImeShown; /** - * When set to true, the IME insets will be frozen until the next app becomes IME input target. - * @see InsetsPolicy#adjustVisibilityForIme - * @see ImeInsetsSourceProvider#updateClientVisibility - */ - boolean mImeInsetsFrozenUntilStartInput; - - /** * A flag to determine if this AR is in the process of closing or entering PIP. This is needed * to help AR know that the app is in the process of closing but hasn't yet started closing on * the WM side. @@ -1175,8 +1166,6 @@ final class ActivityRecord extends WindowToken { pw.print(" launchMode="); pw.println(launchMode); pw.print(prefix); pw.print("mActivityType="); pw.println(activityTypeToString(getActivityType())); - pw.print(prefix); pw.print("mImeInsetsFrozenUntilStartInput="); - pw.println(mImeInsetsFrozenUntilStartInput); if (requestedVrComponent != null) { pw.print(prefix); pw.print("requestedVrComponent="); @@ -5239,7 +5228,8 @@ final class ActivityRecord extends WindowToken { pendingOptions.getWidth(), pendingOptions.getHeight()); options = AnimationOptions.makeScaleUpAnimOptions( pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight()); + pendingOptions.getWidth(), pendingOptions.getHeight(), + pendingOptions.getOverrideTaskTransition()); if (intent.getSourceBounds() == null) { intent.setSourceBounds(new Rect(pendingOptions.getStartX(), pendingOptions.getStartY(), @@ -5771,19 +5761,16 @@ final class ActivityRecord extends WindowToken { return; } - final int windowsCount = mChildren.size(); - // With Shell-Transition, the activity will running a transition when it is visible. - // It won't be included when fromTransition is true means the call from finishTransition. - final boolean runningAnimation = sEnableShellTransitions ? visible - : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION); - for (int i = 0; i < windowsCount; i++) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAnimation); + if (!visible) { + for (int i = mChildren.size() - 1; i >= 0; --i) { + mChildren.get(i).onAppCommitInvisible(); + } } setVisible(visible); setVisibleRequested(visible); ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b" - + " visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s", - this, isVisible(), mVisibleRequested, isInTransition(), runningAnimation, + + " visibleRequested=%b, inTransition=%b, caller=%s", + this, visible, mVisibleRequested, inTransition(), Debug.getCallers(5)); if (visible) { // If we are being set visible, and the starting window is not yet displayed, @@ -5873,10 +5860,6 @@ final class ActivityRecord extends WindowToken { } final DisplayContent displayContent = getDisplayContent(); - if (!visible) { - mImeInsetsFrozenUntilStartInput = true; - } - if (!displayContent.mClosingApps.contains(this) && !displayContent.mOpeningApps.contains(this) && !fromTransition) { @@ -6224,13 +6207,8 @@ final class ActivityRecord extends WindowToken { return false; } - // Hide all activities on the presenting display so that malicious apps can't do tap - // jacking (b/391466268). - // For now, this should only be applied to external displays because presentations can only - // be shown on them. - // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that - // the presentation won't stop its controlling activity. - if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) { + // A presentation stopps all activities behind on the same display. + if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) { return false; } @@ -6952,14 +6930,6 @@ final class ActivityRecord extends WindowToken { // closing activity having to wait until idle timeout to be stopped or destroyed if the // next activity won't report idle (e.g. repeated view animation). mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); - - // If the activity is visible, but no windows are eligible to start input, unfreeze - // to avoid permanently frozen IME insets. - if (mImeInsetsFrozenUntilStartInput && getWindow( - win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags)) - == null) { - mImeInsetsFrozenUntilStartInput = false; - } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ddb9f178cb8b..254127dee7a8 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4324,10 +4324,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { task = mRootWindowContainer.getDefaultTaskDisplayArea().getRootTask( t -> t.isActivityTypeStandard()); } - if (task != null && task.getTopMostActivity() != null - && !task.getTopMostActivity().isState(FINISHING, DESTROYING, DESTROYED)) { + final ActivityRecord topActivity = task != null + ? task.getTopMostActivity() + : null; + if (topActivity != null && !topActivity.isState(FINISHING, DESTROYING, DESTROYED)) { mWindowManager.mAtmService.mActivityClientController - .onPictureInPictureUiStateChanged(task.getTopMostActivity(), pipState); + .onPictureInPictureUiStateChanged(topActivity, pipState); } } } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 79bed3d8453d..e76a83453a9d 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -388,8 +388,7 @@ class BackNavigationController { removedWindowContainer); mBackAnimationInProgress = builder != null; if (mBackAnimationInProgress) { - if (removedWindowContainer.mTransitionController.inTransition() - || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) { + if (removedWindowContainer.mTransitionController.inTransition()) { ProtoLog.w(WM_DEBUG_BACK_PREVIEW, "Pending back animation due to another animation is running"); mPendingAnimationBuilder = builder; @@ -817,6 +816,8 @@ class BackNavigationController { if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) && ar.mTransitionController.isCollecting(ar)) { final TransitionController controller = ar.mTransitionController; + final Transition transition = controller.getCollectingTransition(); + final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType; boolean collectTask = false; ActivityRecord changedActivity = null; for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { @@ -829,8 +830,16 @@ class BackNavigationController { changedActivity = next; } } - if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType - == AnimationHandler.TASK_SWITCH) { + if (Flags.unifyBackNavigationTransition()) { + for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget, + false /* isTop */); + } + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */); + } + if (collectTask && switchType == AnimationHandler.TASK_SWITCH) { final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); if (topTask != null) { WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); @@ -848,6 +857,18 @@ class BackNavigationController { } } + private static void collectAnimatableTarget(Transition transition, int switchType, + WindowContainer animatingTarget, boolean isTop) { + if ((switchType == AnimationHandler.ACTIVITY_SWITCH + && (animatingTarget.asActivityRecord() != null + || animatingTarget.asTaskFragment() != null)) + || (switchType == AnimationHandler.TASK_SWITCH + && animatingTarget.asTask() != null)) { + transition.collect(animatingTarget); + transition.setBackGestureAnimation(animatingTarget, isTop); + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -992,8 +1013,8 @@ class BackNavigationController { return; } - if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) { - Slog.v(TAG, "Skip predictive back transition, another transition is collecting"); + if (mWindowManagerService.mRoot.mTransitionController.inTransition()) { + Slog.v(TAG, "Skip predictive back transition, another transition is playing"); cancelPendingAnimation(); return; } @@ -1098,7 +1119,7 @@ class BackNavigationController { } final Transition prepareTransition = builder.prepareTransitionIfNeeded( - openingActivities); + openingActivities, close, open); final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction(); final SurfaceControl.Transaction ct = prepareTransition != null ? st : close.getPendingTransaction(); @@ -1790,7 +1811,8 @@ class BackNavigationController { return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget); } - private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) { + private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities, + WindowContainer promoteToClose, WindowContainer[] promoteToOpen) { if (Flags.unifyBackNavigationTransition()) { if (mCloseTarget.asWindowState() != null) { return null; @@ -1806,11 +1828,11 @@ class BackNavigationController { final TransitionController tc = visibleOpenActivities[0].mTransitionController; final Transition prepareOpen = tc.createTransition( TRANSIT_PREPARE_BACK_NAVIGATION); - tc.collect(mCloseTarget); - prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */); - for (int i = mOpenTargets.length - 1; i >= 0; --i) { - tc.collect(mOpenTargets[i]); - prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */); + tc.collect(promoteToClose); + prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */); + for (int i = promoteToOpen.length - 1; i >= 0; --i) { + tc.collect(promoteToOpen[i]); + prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */); } if (!makeVisibles.isEmpty()) { setLaunchBehind(visibleOpenActivities); diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index a1faa7573a0c..f35930700653 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -51,8 +51,13 @@ public final class DesktopModeHelper { } /** - * Return {@code true} if the current device supports desktop mode. + * Return {@code true} if the current device can hosts desktop sessions on its internal display. */ + @VisibleForTesting + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + // TODO(b/337819319): use a companion object instead. private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); @@ -67,12 +72,12 @@ public final class DesktopModeHelper { */ private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( - context) || isDeviceEligibleForDesktopMode(context)); + context) || isInternalDisplayEligibleToHostDesktops(context)); } @VisibleForTesting - static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context) || ( + static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { + return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( context)); } @@ -81,12 +86,14 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context)) + return (isInternalDisplayEligibleToHostDesktops(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() + && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions())) || isDesktopModeEnabledByDevOption(context); } /** Returns {@code true} if desktop experience wallpaper is supported on this device. */ public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) { - return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context); + return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 1dd7c4d4adbd..c87087f84399 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -547,9 +547,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // TODO(multi-display): remove some of the usages. boolean isDefaultDisplay; - /** Indicates whether any presentation is shown on this display. */ - boolean mIsPresenting; - /** Save allocating when calculating rects */ private final Rect mTmpRect = new Rect(); private final Region mTmpRegion = new Region(); @@ -4661,35 +4658,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } - /** - * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to - * judge whether or not to notify the IME insets provider to dispatch this reported IME client - * visibility state to the app clients when needed. - */ - boolean onImeInsetsClientVisibilityUpdate() { - boolean[] changed = new boolean[1]; - - // Unlike the IME layering target or the control target can be updated during the layout - // change, the IME input target requires to be changed after gaining the input focus. - // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze - // when activities going to be visible until the input target changed, or the - // activity was the current input target that has to unfreeze after updating the IME - // client visibility. - final ActivityRecord inputTargetActivity = - mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null; - final boolean targetChanged = mImeInputTarget != mLastImeInputTarget; - if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested() - && inputTargetActivity.mImeInsetsFrozenUntilStartInput) { - forAllActivities(r -> { - if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) { - r.mImeInsetsFrozenUntilStartInput = false; - changed[0] = true; - } - }); - } - return changed[0]; - } - void updateImeControlTarget() { updateImeControlTarget(false /* forceUpdateImeParent */); } @@ -7141,14 +7109,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { - int newRequestedVisibleTypes = + @InsetsType int updateRequestedVisibleTypes( + @InsetsType int visibleTypes, @InsetsType int mask) { + final int newRequestedVisibleTypes = (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask); if (mRequestedVisibleTypes != newRequestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes; mRequestedVisibleTypes = newRequestedVisibleTypes; + return changedTypes; } + return 0; } } diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 907d0dc2e183..7b6fc9e5694d 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -34,6 +34,7 @@ import android.util.proto.ProtoOutputStream; import android.view.InputApplicationHandle; import android.view.InputChannel; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; @@ -260,7 +261,7 @@ class EmbeddedWindowController { // The EmbeddedWindow can only request the IME. All other insets types are requested by // the host window. - private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0; + private @InsetsType int mRequestedVisibleTypes = 0; /** Whether the gesture is transferred to embedded window. */ boolean mGestureToEmbedded = false; @@ -354,24 +355,28 @@ class EmbeddedWindowController { } @Override - public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) { + public boolean isRequestedVisible(@InsetsType int types) { return (mRequestedVisibleTypes & types) != 0; } @Override - public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() { + public @InsetsType int getRequestedVisibleTypes() { return mRequestedVisibleTypes; } /** * Only the IME can be requested from the EmbeddedWindow. - * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are + * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are * not sent to system server via WindowlessWindowManager. + * @return an integer as the changed requested visible insets types. */ - void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @Override diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index cf16204f93a1..f52446ff494c 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -282,7 +282,14 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // TODO(b/353463205) investigate if we should fail the statsToken, or if it's only // temporary null. if (target != null) { - invokeOnImeRequestedChangedListener(target.getWindow(), statsToken); + // If insets target is not available (e.g. RemoteInsetsControlTarget), use current + // IME input target to update IME request state. For example, switch from a task + // with showing IME to a split-screen task without showing IME. + InsetsTarget insetsTarget = target.getWindow(); + if (insetsTarget == null && mServerVisible) { + insetsTarget = mDisplayContent.getImeInputTarget(); + } + invokeOnImeRequestedChangedListener(insetsTarget, statsToken); } } } @@ -314,7 +321,6 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { reportImeDrawnForOrganizerIfNeeded((InsetsControlTarget) caller); } } - changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); if (Flags.refactorInsetsController()) { if (changed) { ImeTracker.forLogging().onProgress(statsToken, diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 4bcba13448e9..b4d55a160631 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -387,22 +387,6 @@ class InsetsPolicy { state.addSource(navSource); } return state; - } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) { - // During switching tasks with gestural navigation, before the next IME input target - // starts the input, we should adjust and freeze the last IME visibility of the window - // in case delivering obsoleted IME insets state during transitioning. - final InsetsSource originalImeSource = originalState.peekSource(ID_IME); - - if (originalImeSource != null) { - final boolean imeVisibility = w.isRequestedVisible(Type.ime()); - final InsetsState state = copyState - ? new InsetsState(originalState) - : originalState; - final InsetsSource imeSource = new InsetsSource(originalImeSource); - imeSource.setVisible(imeVisibility); - state.addSource(imeSource); - return state; - } } else if (w.mImeInsetsConsumed) { // Set the IME source (if there is one) to be invisible if it has been consumed. final InsetsSource originalImeSource = originalState.peekSource(ID_IME); @@ -453,9 +437,9 @@ class InsetsPolicy { return originalState; } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { - mStateController.onRequestedVisibleTypesChanged(caller, statsToken); + mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken); checkAbortTransient(caller); updateBarControlTarget(mFocusedWin); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 9202cf2d5792..164abab992d8 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -219,14 +219,20 @@ class InsetsStateController { } } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { boolean changed = false; for (int i = mProviders.size() - 1; i >= 0; i--) { final InsetsSourceProvider provider = mProviders.valueAt(i); - final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime(); - changed |= provider.updateClientVisibility(caller, - isImeProvider ? statsToken : null); + final @InsetsType int type = provider.getSource().getType(); + if ((type & changedTypes) != 0) { + final boolean isImeProvider = type == WindowInsets.Type.ime(); + changed |= provider.updateClientVisibility( + caller, isImeProvider ? statsToken : null) + // Fake control target cannot change the client visibility, but it should + // change the insets with its newly requested visibility. + || (caller == provider.getFakeControlTarget()); + } } if (changed) { notifyInsetsChanged(); @@ -435,7 +441,8 @@ class InsetsStateController { for (int i = newControlTargets.size() - 1; i >= 0; i--) { // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here - onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */); + onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), + WindowInsets.Type.all(), null /* statsToken */); } newControlTargets.clear(); if (!android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java new file mode 100644 index 000000000000..9630b8fd524b --- /dev/null +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; + +import android.annotation.NonNull; +import android.util.IntArray; + +/** + * Manages presentation windows. + */ +class PresentationController { + + // TODO(b/395475549): Add support for display add/remove, and activity move across displays. + private final IntArray mPresentingDisplayIds = new IntArray(); + + PresentationController() {} + + private boolean isPresenting(int displayId) { + return mPresentingDisplayIds.contains(displayId); + } + + boolean shouldOccludeActivities(int displayId) { + // All activities on the presenting display must be hidden so that malicious apps can't do + // tap jacking (b/391466268). + // For now, this should only be applied to external displays because presentations can only + // be shown on them. + // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that + // the presentation won't stop its controlling activity. + return enablePresentationForConnectedDisplays() && isPresenting(displayId); + } + + void onPresentationAdded(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (isPresenting(displayId)) { + return; + } + mPresentingDisplayIds.add(displayId); + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); + } + + void onPresentationRemoved(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (!isPresenting(displayId)) { + return; + } + // TODO(b/393945496): Make sure that there's one presentation at most per display. + final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId); + if (displayIdIndex != -1) { + mPresentingDisplayIds.remove(displayIdIndex); + } + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); + } +} diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 1ad5988e3c2e..8d198b26f396 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -704,9 +704,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); } - win.setRequestedVisibleTypes(requestedVisibleTypes); + final @InsetsType int changedTypes = + win.setRequestedVisibleTypes(requestedVisibleTypes); win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win, - imeStatsToken); + changedTypes, imeStatsToken); final Task task = win.getTask(); if (task != null) { task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true); @@ -723,10 +724,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { // TODO(b/353463205) Use different phase here ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); - embeddedWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes( requestedVisibleTypes & WindowInsets.Type.ime()); embeddedWindow.getDisplayContent().getInsetsPolicy() - .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken); + .onRequestedVisibleTypesChanged( + embeddedWindow, changedTypes, imeStatsToken); } else { ImeTracker.forLogging().onFailed(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index cc14383fc9f9..ae3a015a690d 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -460,7 +460,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // If the previous front-most task is moved to the back, then notify of the new // front-most task. - final ActivityRecord topMost = getTopMostActivity(); + final ActivityRecord topMost = getTopNonFinishingActivity(); if (topMost != null) { mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront( topMost.getTask().getTaskInfo()); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 64105f634f84..324852d1a410 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1224,6 +1224,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */); } + @Override ActivityRecord getTopNonFinishingActivity() { return getTopNonFinishingActivity( true /* includeOverlays */, true /* includeLaunchedFromBubble */); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 563bcb771212..25b513d85384 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -555,6 +555,23 @@ class TransitionController { return null; } + /** + * @return The playing transition that is transiently-hiding the given {@param container}, or + * null if there isn't one + * @param container A participant of a transient-hide transition + */ + @Nullable + Transition getTransientHideTransitionForContainer( + @NonNull WindowContainer container) { + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + final Transition transition = mPlayingTransitions.get(i); + if (transition.isInTransientHide(container)) { + return transition; + } + } + return null; + } + /** Returns {@code true} if the display contains a transient-launch transition. */ boolean hasTransientLaunch(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch() diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 225951dbd345..55c2668f62d0 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2079,6 +2079,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity(alwaysTruePredicate(), true /* traverseTopToBottom */); } + ActivityRecord getTopNonFinishingActivity() { + return getActivity(r -> !r.finishing, true /* traverseTopToBottom */); + } + ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) { // Break down into 4 calls to avoid object creation due to capturing input params. if (includeFinishing) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d5626661725e..bb669915e366 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,7 +157,6 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -503,6 +502,8 @@ public class WindowManagerService extends IWindowManager.Stub final StartingSurfaceController mStartingSurfaceController; + final PresentationController mPresentationController; + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -1433,6 +1434,7 @@ public class WindowManagerService extends IWindowManager.Stub setGlobalShadowSettings(); mAnrController = new AnrController(this); mStartingSurfaceController = new StartingSurfaceController(this); + mPresentationController = new PresentationController(); mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); @@ -1937,16 +1939,8 @@ public class WindowManagerService extends IWindowManager.Stub } outSizeCompatScale[0] = win.getCompatScaleForClient(); - if (res >= ADD_OKAY - && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) { - displayContent.mIsPresenting = true; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - displayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } - mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(), - /*isShown=*/ true); + if (res >= ADD_OKAY && win.isPresentation()) { + mPresentationController.onPresentationAdded(win); } } @@ -4732,11 +4726,13 @@ public class WindowManagerService extends IWindowManager.Stub } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES); - dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask); + final @InsetsType int changedTypes = + dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes( + visibleTypes, mask); // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here, if null. dc.getInsetsStateController().onRequestedVisibleTypesChanged( - dc.mRemoteInsetsControlTarget, statsToken); + dc.mRemoteInsetsControlTarget, changedTypes, statsToken); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index a11f4b1f3fc3..924b9de5a562 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -702,9 +702,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if ((entry.getValue().getChangeMask() & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) { - // Disable entering pip (eg. when recents pretends to finish itself) - if (chain.mTransition != null) { - chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + if (com.android.wm.shell.Flags.enableRecentsBookendTransition()) { + // If we are using a bookend transition, then the transition that we need + // to disable pip on finish is the original transient transition, not the + // bookend transition + final Transition transientHideTransition = + mTransitionController.getTransientHideTransitionForContainer(wc); + if (transientHideTransition != null) { + transientHideTransition.setCanPipOnFinish(false); + } else { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + "Set do-not-pip: no task"); + } + } else { + // Disable entering pip (eg. when recents pretends to finish itself) + if (chain.mTransition != null) { + chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); + } } } // A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 84d8f840d849..589724182980 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -822,17 +821,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @VisibleForTesting - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) { - setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); + @InsetsType int setRequestedVisibleTypes( + @InsetsType int requestedVisibleTypes, @InsetsType int mask) { + return setRequestedVisibleTypes( + mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } /** @@ -2069,38 +2074,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP super.onMovedByResize(); } - void onAppVisibilityChanged(boolean visible, boolean runningAppAnimation) { + void onAppCommitInvisible() { for (int i = mChildren.size() - 1; i >= 0; --i) { - mChildren.get(i).onAppVisibilityChanged(visible, runningAppAnimation); + mChildren.get(i).onAppCommitInvisible(); } - - final boolean isVisibleNow = isVisibleNow(); - if (mAttrs.type == TYPE_APPLICATION_STARTING) { - // Starting window that's exiting will be removed when the animation finishes. - // Mark all relevant flags for that onExitAnimationDone will proceed all the way - // to actually remove it. - if (!visible && isVisibleNow && mActivityRecord.isAnimating(PARENTS | TRANSITION)) { - ProtoLog.d(WM_DEBUG_ANIM, - "Set animatingExit: reason=onAppVisibilityChanged win=%s", this); - mAnimatingExit = true; - mRemoveOnExit = true; - mWindowRemovalAllowed = true; - } - } else if (visible != isVisibleNow) { - // Run exit animation if: - // 1. App visibility and WS visibility are different - // 2. App is not running an animation - // 3. WS is currently visible - if (!runningAppAnimation && isVisibleNow) { - final AccessibilityController accessibilityController = - mWmService.mAccessibilityController; - final int winTransit = TRANSIT_EXIT; - mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */); - if (accessibilityController.hasCallbacks()) { - accessibilityController.onWindowTransition(this, winTransit); - } - } - setDisplayLayoutNeeded(); + if (mAttrs.type != TYPE_APPLICATION_STARTING + && mWmService.mAccessibilityController.hasCallbacks() + // It is a change only if App visibility and WS visibility are different. + && isVisible()) { + mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT); } } @@ -2317,15 +2299,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int type = mAttrs.type; - if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { - // TODO(b/393945496): Make sure that there's one presentation at most per display. - dc.mIsPresenting = false; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - } - mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(), - /*isShown=*/ false); + if (isPresentation()) { + mWmService.mPresentationController.onPresentationRemoved(this); } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -3354,6 +3329,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + boolean isPresentation() { + return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION; + } + private boolean isOnVirtualDisplay() { return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL; } diff --git a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java index 2e4b97ef7dd2..371b0c926039 100644 --- a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; +import android.app.PropertyInvalidatedCache; import android.content.Context; import android.multiuser.Flags; import android.os.UserManager; @@ -75,6 +76,8 @@ public class StorageManagerServiceTest { @Before public void setFixtures() { + PropertyInvalidatedCache.disableForTestMode(); + // Called when WatchedUserStates is constructed doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS index c824c3948e2d..c7c23f081044 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS +++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS @@ -1,3 +1,6 @@ -# Bug component: 44215 +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 44215. include /core/java/android/view/accessibility/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS new file mode 100644 index 000000000000..9592bfdfa73b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS @@ -0,0 +1,6 @@ +# Bug component: 1530954 +# +# The above component is for automated test bugs. If you are a human looking to report +# a bug in this codebase then please use component 770744. + +include /services/accessibility/java/com/android/server/accessibility/magnification/OWNERS 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 65150e7b48fc..440f43e9b926 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -49,17 +49,13 @@ import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.InsetsSource.ID_IME; -import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -125,7 +121,6 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PauseActivityItem; -import android.app.servertransaction.WindowStateResizeItem; import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Intent; @@ -149,8 +144,6 @@ import android.view.DisplayInfo; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner.Stub; import android.view.IWindowManager; -import android.view.InsetsSource; -import android.view.InsetsState; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.Surface; @@ -171,7 +164,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; @@ -3370,178 +3362,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - makeWindowVisibleAndDrawn(app, mImeWindow); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity has no IME focusable window. - app.mActivityRecord.forAllWindows(w -> { - w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; - return true; - }, true); - - app.mActivityRecord.commitVisibility(true, false); - app.mActivityRecord.onWindowsVisible(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { - final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - InsetsSource imeSource = new InsetsSource(ID_IME, ime()); - app.mAboveInsetsState.addSource(imeSource); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.updateImeInputAndControlTarget(app); - - InsetsState state = app.getInsetsState(); - assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible()); - assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty()); - - // Simulate app is closing and expect IME insets is frozen. - mDisplayContent.mOpeningApps.clear(); - app.mActivityRecord.commitVisibility(false, false); - app.mActivityRecord.onWindowsGone(); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate app re-start input or turning screen off/on then unlocked by un-secure - // keyguard to back to the app, expect IME insets is not frozen - app.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - imeSource.setVisible(true); - imeSource.setFrame(new Rect(100, 400, 500, 500)); - app.mAboveInsetsState.addSource(imeSource); - - // Verify when IME is visible and the app can receive the right IME insets from policy. - makeWindowVisibleAndDrawn(app, mImeWindow); - state = app.getInsetsState(); - assertTrue(state.peekSource(ID_IME).isVisible()); - assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame()); - } - - @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) - @Test - public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest() - throws RemoteException { - final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build(); - final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); - - mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer( - mImeWindow, null, null); - mImeWindow.getControllableInsetProvider().setServerVisible(true); - - // Simulate app2 is closing and let app1 is visible to be IME targets. - makeWindowVisibleAndDrawn(app1, mImeWindow); - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - app2.mActivityRecord.commitVisibility(false, false); - - // app1 requests IME visible. - app1.setRequestedVisibleTypes(ime(), ime()); - mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1, - null /* statsToken */); - - // Verify app1's IME insets is visible and app2's IME insets frozen flag set. - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Simulate switching to app2 to make it visible to be IME targets. - spyOn(app2); - spyOn(app2.mClient); - spyOn(app2.getProcess()); - ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); - doReturn(true).when(app2).isReadyToDispatchInsetsState(); - mDisplayContent.setImeLayeringTarget(app2); - app2.mActivityRecord.commitVisibility(true, false); - mDisplayContent.updateImeInputAndControlTarget(app2); - performSurfacePlacementAndWaitForWindowAnimator(); - - // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets - // to client if the app didn't request IME visible. - assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem( - isA(WindowStateResizeItem.class)); - assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); - } - - @Test - public void testImeInsetsFrozenFlag_multiWindowActivities() { - final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); - final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken( - imeToken).build(); - makeWindowVisibleAndDrawn(ime); - - // Create a split-screen root task with activity1 and activity 2. - final Task task = new TaskBuilder(mSupervisor) - .setCreateParentTask(true).setCreateActivity(true).build(); - task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - final ActivityRecord activity1 = task.getTopNonFinishingActivity(); - activity1.getTask().setResumedActivity(activity1, "testApp1"); - - final ActivityRecord activity2 = new TaskBuilder(mSupervisor) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .setCreateActivity(true).build().getTopMostActivity(); - activity2.getTask().setResumedActivity(activity2, "testApp2"); - activity2.getTask().setParent(task.getRootTask()); - - // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when - // invisible to user. - activity1.mImeInsetsFrozenUntilStartInput = true; - activity2.mImeInsetsFrozenUntilStartInput = true; - - 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(); - controller.getImeSourceProvider().setWindowContainer( - ime, null, null); - ime.getControllableInsetProvider().setServerVisible(true); - - // app1 starts input and expect IME insets for all activities in split-screen will be - // frozen until the input started. - mDisplayContent.setImeLayeringTarget(app1); - mDisplayContent.updateImeInputAndControlTarget(app1); - mDisplayContent.computeImeTarget(true /* updateImeTarget */); - performSurfacePlacementAndWaitForWindowAnimator(); - - assertEquals(app1, mDisplayContent.getImeInputTarget()); - assertFalse(activity1.mImeInsetsFrozenUntilStartInput); - assertFalse(activity2.mImeInsetsFrozenUntilStartInput); - - app1.setRequestedVisibleTypes(ime()); - controller.onRequestedVisibleTypesChanged(app1, null /* statsToken */); - - // Expect all activities in split-screen will get IME insets visible state - assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible()); - assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible()); - } - @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index e0b700a4ffe3..eaffc481098e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -97,6 +97,7 @@ public class DesktopModeHelperTest { public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse() throws Exception { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); doReturn(true).when(mMockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported)); disableEnforceDeviceRestriction(); @@ -148,6 +149,7 @@ public class DesktopModeHelperTest { @Test public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue(); } @@ -176,21 +178,21 @@ public class DesktopModeHelperTest { @Test public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { - doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @@ -200,7 +202,7 @@ public class DesktopModeHelperTest { eq(R.bool.config_isDesktopModeDevOptionSupported) ); - assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); } private void resetEnforceDeviceRestriction() throws Exception { @@ -234,4 +236,4 @@ public class DesktopModeHelperTest { Settings.Global.putInt(mContext.getContentResolver(), DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); } -} +}
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index fdde3b38f19f..d305c2f54456 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -1345,7 +1345,7 @@ public class DesktopModeLaunchParamsModifierTests extends private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported, boolean enforceDeviceRestrictions) { doReturn(isDesktopModeSupported) - .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any())); + .when(() -> DesktopModeHelper.canEnterDesktopMode(any())); doReturn(enforceDeviceRestrictions) .when(DesktopModeHelper::shouldEnforceDeviceRestrictions); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java index 285a5e246e0c..ea21bb34597d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java @@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.any; /** Robot for changing desktop windowing properties. */ class DesktopWindowingRobot { void allowEnterDesktopMode(boolean isAllowed) { - doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); + doReturn(isAllowed).when(() -> + DesktopModeHelper.canEnterDesktopMode(any())); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 6c5fe1d8551e..71e34ef220d3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -53,6 +53,7 @@ import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -400,9 +401,9 @@ public class InsetsPolicyTest extends WindowTestsBase { assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); - mAppWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); - policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */); + policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */); waitUntilWindowAnimatorIdle(); controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 973c8d0a8464..5525bae89138 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -52,6 +52,7 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -201,8 +202,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); getController().onImeControlTargetChanged(base); - base.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(base, null /* statsToken */); + final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */); if (android.view.inputmethod.Flags.refactorInsetsController()) { // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called. mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN; @@ -509,8 +510,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app); mDisplayContent.updateImeInputAndControlTarget(app); - app.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(app, null /* statsToken */); + final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); if (android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java new file mode 100644 index 000000000000..db90c28ec7df --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.Display.FLAG_PRESENTATION; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +import android.graphics.Rect; +import android.os.UserHandle; +import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; +import android.view.IWindow; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:PresentationControllerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class PresentationControllerTests extends WindowTestsBase { + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationHidesActivitiesBehind() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); + final int uid = 100000; // uid for non-system user + final Session session = createTestSession(mAtm, 1234 /* pid */, uid); + final int userId = UserHandle.getUserId(uid); + doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_PRESENTATION); + + final IWindow clientWindow = new TestIWindow(); + final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), + new InsetsSourceControl.Array(), new Rect(), new float[1]); + assertTrue(result >= WindowManagerGlobal.ADD_OKAY); + assertFalse(activity.isVisible()); + + final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); + window.removeImmediately(); + assertTrue(activity.isVisible()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 1323d8a59cef..71e84c0f1821 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -26,7 +26,6 @@ import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCRE import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; -import static android.view.Display.FLAG_PRESENTATION; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; @@ -55,7 +54,6 @@ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; import static com.google.common.truth.Truth.assertThat; @@ -102,7 +100,6 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; -import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1409,38 +1406,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { assertEquals(activityWindowInfo2, activityWindowInfo3); } - @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) - @Test - public void testPresentationHidesActivitiesBehind() { - DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - DisplayContent dc = createNewDisplay(displayInfo); - int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); - ActivityRecord activity = createActivityRecord(createTask(dc)); - assertTrue(activity.isVisible()); - - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - int uid = 100000; // uid for non-system user - Session session = createTestSession(mAtm, 1234 /* pid */, uid); - int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - LayoutParams.TYPE_PRESENTATION); - - final IWindow clientWindow = new TestIWindow(); - int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, - userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), - new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - - final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); - } - @Test public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() { doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index cff172f55601..a718c06cc2fa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -1282,7 +1282,6 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app plays closing transition to app2. app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is visible on app, but not for app2 during app task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); @@ -1305,7 +1304,7 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app2 in multi-window mode is going to background to switch to the fullscreen // app which requests IME with updating all windows Insets State when IME is above app. - app2.mActivityRecord.mImeInsetsFrozenUntilStartInput = true; + app2.mActivityRecord.setVisibleRequested(false); mDisplayContent.setImeLayeringTarget(app); mDisplayContent.setImeInputTarget(app); app.setRequestedVisibleTypes(ime(), ime()); @@ -1324,7 +1323,6 @@ public class WindowStateTests extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app2); app.mActivityRecord.commitVisibility(false, false); assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Verify the IME insets is still visible on app, but not for app2 during task switching. assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index e0af22369182..d2741ac7ee9f 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -4821,10 +4821,14 @@ public class SubscriptionManager { + "Invalid subscriptionId: " + subscriptionId); } + String contextPkg = mContext != null ? mContext.getOpPackageName() : "<unknown>"; + String contextAttributionTag = mContext != null ? mContext.getAttributionTag() : null; + try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId); + return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId, contextPkg, + contextAttributionTag); } else { throw new IllegalStateException("subscription service unavailable."); } diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 1bfec29a3cf4..a974c615a4ae 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -347,13 +347,17 @@ interface ISub { * Returns whether the given subscription is associated with the calling user. * * @param subscriptionId the subscription ID of the subscription + * @param callingPackage The package maing the call + * @param callingFeatureId The feature in the package + * @return {@code true} if the subscription is associated with the user that the current process * is running in; {@code false} otherwise. * * @throws IllegalArgumentException if subscription doesn't exist. * @throws SecurityException if the caller doesn't have permissions required. */ - boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId); + boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId, String callingPackage, + String callingFeatureId); /** * Check if subscription and user are associated with each other. diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index f00a6cad6b46..20315561cceb 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,9 +54,7 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -67,9 +65,7 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -80,9 +76,7 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); - } + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); return true; }; @@ -93,9 +87,7 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - if (value) { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); - } + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); return true; }; @@ -106,9 +98,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - value->emplace(arg); - } + value->emplace(arg); return true; }; @@ -118,9 +108,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - if (value) { - *value = true; - } + *value = true; return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index ad167c979662..2a3cb2a0c65d 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,22 +159,4 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } -TEST(CommandTest, OptionsWithNullptrToAcceptValues) { - TestCommand command; - command.AddRequiredFlag("--rflag", "", nullptr); - command.AddRequiredFlagList("--rlflag", "", nullptr); - command.AddOptionalFlag("--oflag", "", nullptr); - command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); - command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); - command.AddOptionalSwitch("--switch", "", nullptr); - - ASSERT_EQ(0, command.Execute({ - "--rflag"s, "1"s, - "--rlflag"s, "1"s, - "--oflag"s, "1"s, - "--olflag"s, "1"s, - "--olflag2"s, "1"s, - "--switch"s}, &std::cerr)); -} - } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 060bc5fa2242..6c3eae11eab9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,6 +425,9 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } + if (enable_sparse_encoding_) { + table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; + } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 98c8f5ff89c0..9452e588953e 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,9 +36,11 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &enable_sparse_encoding_); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -85,6 +87,7 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; + bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 4718fbf085f8..ff4d8ef2ec25 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2505,6 +2505,9 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } + if (options_.use_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index b5bd905c02be..2f17853718ec 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,6 +75,7 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; + bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -162,11 +163,9 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch( - "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + AddOptionalSwitch("--enable-sparse-encoding", + "This decreases APK size at the cost of resource retrieval performance.", + &options_.use_sparse_encoding); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index f218307af578..762441ee1872 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,6 +406,9 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } + if (options_.enable_sparse_encoding) { + options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; + } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index e3af584cbbd9..012b0f230ca2 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,6 +61,9 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; + // Whether sparse encoding should be used for O+ resources. + bool enable_sparse_encoding = false; + // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -103,9 +106,11 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" - "enabled if minSdk of the APK is >= 32.", - nullptr); + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " + "the APK is O+", + &options_.enable_sparse_encoding); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index b8ac7925d44e..1a82021bce71 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index f1c4c3512ed3..0633bc81cb25 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,7 +37,8 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). + // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less + // than O only applies sparse encoding for resource configuration available on O+. Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -46,7 +47,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index e3d589eb078b..0f1168514c4a 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Disabled; + options.sparse_entries = SparseEntriesMode::Enabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 664d8412a3be..5c3dfdcadfec 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -3,8 +3,6 @@ ## Version 2.20 - Too many features, bug fixes, and improvements to list since the last minor version update in 2017. This README will be updated more frequently in the future. -- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The - `--enable-sparse-encoding` flag still exists, but is a no-op. ## Version 2.19 - Added navigation resource type. |