diff options
147 files changed, 3543 insertions, 1107 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java index ce381b6699ea..e08200b055d8 100644 --- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java +++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java @@ -22,7 +22,6 @@ import android.app.ActivityManagerInternal.AppBackgroundRestrictionListener; import android.app.AppOpsManager; import android.app.AppOpsManager.PackageOps; import android.app.IActivityManager; -import android.app.UidObserver; import android.app.usage.UsageStatsManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -54,6 +53,7 @@ import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; import com.android.internal.util.ArrayUtils; import com.android.internal.util.StatLogger; +import com.android.modules.expresslog.Counter; import com.android.server.AppStateTrackerProto.ExemptedPackage; import com.android.server.AppStateTrackerProto.RunAnyInBackgroundRestrictedPackages; import com.android.server.usage.AppStandbyInternal; @@ -79,6 +79,9 @@ import java.util.Set; public class AppStateTrackerImpl implements AppStateTracker { private static final boolean DEBUG = false; + private static final String APP_RESTRICTION_COUNTER_METRIC_ID = + "battery.value_app_background_restricted"; + private final Object mLock = new Object(); private final Context mContext; @@ -748,6 +751,9 @@ public class AppStateTrackerImpl implements AppStateTracker { } catch (RemoteException e) { // Shouldn't happen } + if (restricted) { + Counter.logIncrementWithUid(APP_RESTRICTION_COUNTER_METRIC_ID, uid); + } synchronized (mLock) { if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) { mHandler.notifyRunAnyAppOpsChanged(uid, packageName); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index c5405171edd0..0a7bffc786cc 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -49,8 +49,10 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.BitUtils; +import com.android.modules.expresslog.Histogram; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.AppSchedulingModuleThread; import com.android.server.IoThread; import com.android.server.job.JobSchedulerInternal.JobStorePersistStats; import com.android.server.job.controllers.JobStatus; @@ -94,6 +96,7 @@ public final class JobStore { /** Threshold to adjust how often we want to write to the db. */ private static final long JOB_PERSIST_DELAY = 2000L; + private static final long SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS = 30 * 60_000L; @VisibleForTesting static final String JOB_FILE_SPLIT_PREFIX = "jobs_"; private static final int ALL_UIDS = -1; @@ -131,6 +134,30 @@ public final class JobStore { private JobStorePersistStats mPersistInfo = new JobStorePersistStats(); + /** + * Separately updated value of the JobSet size to avoid recalculating it frequently for logging + * purposes. Continue to use {@link JobSet#size()} for the up-to-date and accurate value. + */ + private int mCurrentJobSetSize = 0; + private int mScheduledJob30MinHighWaterMark = 0; + private static final Histogram sScheduledJob30MinHighWaterMarkLogger = new Histogram( + "job_scheduler.value_hist_scheduled_job_30_min_high_water_mark", + new Histogram.ScaledRangeOptions(15, 1, 99, 1.5f)); + private final Runnable mScheduledJobHighWaterMarkLoggingRunnable = new Runnable() { + @Override + public void run() { + AppSchedulingModuleThread.getHandler().removeCallbacks(this); + synchronized (mLock) { + sScheduledJob30MinHighWaterMarkLogger.logSample(mScheduledJob30MinHighWaterMark); + mScheduledJob30MinHighWaterMark = mJobSet.size(); + } + // The count doesn't need to be logged at exact times. Logging based on system uptime + // should be fine. + AppSchedulingModuleThread.getHandler() + .postDelayed(this, SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS); + } + }; + /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ static JobStore get(JobSchedulerService jobManagerService) { synchronized (sSingletonLock) { @@ -183,6 +210,9 @@ public final class JobStore { mXmlTimestamp = mJobsFile.exists() ? mJobsFile.getLastModifiedTime() : mJobFileDirectory.lastModified(); mRtcGood = (sSystemClock.millis() > mXmlTimestamp); + + AppSchedulingModuleThread.getHandler().postDelayed( + mScheduledJobHighWaterMarkLoggingRunnable, SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS); } private void init() { @@ -252,7 +282,10 @@ public final class JobStore { * @param jobStatus Job to add. */ public void add(JobStatus jobStatus) { - mJobSet.add(jobStatus); + if (mJobSet.add(jobStatus)) { + mCurrentJobSetSize++; + maybeUpdateHighWaterMark(); + } if (jobStatus.isPersisted()) { mPendingJobWriteUids.put(jobStatus.getUid(), true); maybeWriteStatusToDiskAsync(); @@ -267,7 +300,10 @@ public final class JobStore { */ @VisibleForTesting public void addForTesting(JobStatus jobStatus) { - mJobSet.add(jobStatus); + if (mJobSet.add(jobStatus)) { + mCurrentJobSetSize++; + maybeUpdateHighWaterMark(); + } if (jobStatus.isPersisted()) { mPendingJobWriteUids.put(jobStatus.getUid(), true); } @@ -303,6 +339,7 @@ public final class JobStore { } return false; } + mCurrentJobSetSize--; if (removeFromPersisted && jobStatus.isPersisted()) { mPendingJobWriteUids.put(jobStatus.getUid(), true); maybeWriteStatusToDiskAsync(); @@ -315,7 +352,9 @@ public final class JobStore { */ @VisibleForTesting public void removeForTesting(JobStatus jobStatus) { - mJobSet.remove(jobStatus); + if (mJobSet.remove(jobStatus)) { + mCurrentJobSetSize--; + } if (jobStatus.isPersisted()) { mPendingJobWriteUids.put(jobStatus.getUid(), true); } @@ -327,6 +366,7 @@ public final class JobStore { */ public void removeJobsOfUnlistedUsers(int[] keepUserIds) { mJobSet.removeJobsOfUnlistedUsers(keepUserIds); + mCurrentJobSetSize = mJobSet.size(); } /** Note a change in the specified JobStatus that necessitates writing job state to disk. */ @@ -342,6 +382,7 @@ public final class JobStore { public void clear() { mJobSet.clear(); mPendingJobWriteUids.put(ALL_UIDS, true); + mCurrentJobSetSize = 0; maybeWriteStatusToDiskAsync(); } @@ -352,6 +393,7 @@ public final class JobStore { public void clearForTesting() { mJobSet.clear(); mPendingJobWriteUids.put(ALL_UIDS, true); + mCurrentJobSetSize = 0; } void setUseSplitFiles(boolean useSplitFiles) { @@ -442,6 +484,12 @@ public final class JobStore { mJobSet.forEachJobForSourceUid(sourceUid, functor); } + private void maybeUpdateHighWaterMark() { + if (mScheduledJob30MinHighWaterMark < mCurrentJobSetSize) { + mScheduledJob30MinHighWaterMark = mCurrentJobSetSize; + } + } + /** Version of the db schema. */ private static final int JOBS_FILE_VERSION = 1; /** @@ -1125,6 +1173,12 @@ public final class JobStore { if (needFileMigration) { migrateJobFilesAsync(); } + + // Log the count immediately after loading from boot. + mCurrentJobSetSize = numJobs; + mScheduledJob30MinHighWaterMark = mCurrentJobSetSize; + mScheduledJobHighWaterMarkLoggingRunnable.run(); + if (mCompletionLatch != null) { mCompletionLatch.countDown(); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 934622305a05..e42e52619320 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2367,6 +2367,7 @@ package android.os { method public static boolean isApp(int); field public static final int MIN_SECONDARY_USER_ID = 10; // 0xa field public static final int USER_ALL = -1; // 0xffffffff + field public static final int USER_CURRENT = -2; // 0xfffffffe field public static final int USER_NULL = -10000; // 0xffffd8f0 field public static final int USER_SYSTEM = 0; // 0x0 } @@ -2375,10 +2376,12 @@ package android.os { method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createProfileForUser(@Nullable String, @NonNull String, int, int, @Nullable String[]); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getAliveUsers(); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.os.UserHandle getBootUser(); method public int getMainDisplayIdAssignedToUser(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.Set<java.lang.String> getPreInstallableSystemPackages(@NonNull String); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public String getUserType(); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(); method @Deprecated @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 9e59ee496de1..0e5cbe228c1b 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -60,13 +60,16 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import android.util.LongSparseArray; import android.util.LongSparseLongArray; import android.util.Pools; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -179,6 +182,8 @@ import java.util.function.Supplier; */ @SystemService(Context.APP_OPS_SERVICE) public class AppOpsManager { + private static final String LOG_TAG = "AppOpsManager"; + /** * This is a subtle behavior change to {@link #startWatchingMode}. * @@ -7517,6 +7522,7 @@ public class AppOpsManager { */ @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setUidMode(int code, int uid, @Mode int mode) { + logAnySeriousModeChanges(code, uid, null, mode); try { mService.setUidMode(code, uid, mode); } catch (RemoteException e) { @@ -7537,6 +7543,7 @@ public class AppOpsManager { @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setUidMode(@NonNull String appOp, int uid, @Mode int mode) { + logAnySeriousModeChanges(strOpToOp(appOp), uid, null, mode); try { mService.setUidMode(AppOpsManager.strOpToOp(appOp), uid, mode); } catch (RemoteException e) { @@ -7572,11 +7579,32 @@ public class AppOpsManager { } } + private void logAnySeriousModeChanges(int code, int uid, String packageName, @Mode int mode) { + // TODO (b/280869337): Remove this once we have the required data. + if (code != OP_RUN_ANY_IN_BACKGROUND || mode == MODE_ALLOWED) { + return; + } + final StringBuilder log = new StringBuilder("Attempt to change RUN_ANY_IN_BACKGROUND to ") + .append(modeToName(mode)) + .append(" for uid: ") + .append(UserHandle.formatUid(uid)) + .append(" package: ") + .append(packageName) + .append(" by: ") + .append(mContext.getOpPackageName()); + if (Process.myUid() == Process.SYSTEM_UID) { + Slog.wtfStack(LOG_TAG, log.toString()); + } else { + Log.w(LOG_TAG, log.toString()); + } + } + /** @hide */ @UnsupportedAppUsage @TestApi @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setMode(int code, int uid, String packageName, @Mode int mode) { + logAnySeriousModeChanges(code, uid, packageName, mode); try { mService.setMode(code, uid, packageName, mode); } catch (RemoteException e) { @@ -7599,6 +7627,7 @@ public class AppOpsManager { @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setMode(@NonNull String op, int uid, @Nullable String packageName, @Mode int mode) { + logAnySeriousModeChanges(strOpToOp(op), uid, packageName, mode); try { mService.setMode(strOpToOp(op), uid, packageName, mode); } catch (RemoteException e) { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 99ef315676c4..e9fbf6b178fb 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -102,6 +102,43 @@ interface IActivityManager { void registerUidObserver(in IUidObserver observer, int which, int cutpoint, String callingPackage); void unregisterUidObserver(in IUidObserver observer); + + /** + * Registers a UidObserver with a uid filter. + * + * @param observer The UidObserver implementation to register. + * @param which A bitmask of events to observe. See ActivityManager.UID_OBSERVER_*. + * @param cutpoint The cutpoint for onUidStateChanged events. When the state crosses this + * threshold in either direction, onUidStateChanged will be called. + * @param callingPackage The name of the calling package. + * @param uids A list of uids to watch. If all uids are to be watched, use + * registerUidObserver instead. + * @throws RemoteException + * @return Returns A binder token identifying the UidObserver registration. + */ + IBinder registerUidObserverForUids(in IUidObserver observer, int which, int cutpoint, + String callingPackage, in int[] uids); + + /** + * Adds a uid to the list of uids that a UidObserver will receive updates about. + * + * @param observerToken The binder token identifying the UidObserver registration. + * @param callingPackage The name of the calling package. + * @param uid The uid to watch. + * @throws RemoteException + */ + void addUidToObserver(in IBinder observerToken, String callingPackage, int uid); + + /** + * Removes a uid from the list of uids that a UidObserver will receive updates about. + * + * @param observerToken The binder token identifying the UidObserver registration. + * @param callingPackage The name of the calling package. + * @param uid The uid to stop watching. + * @throws RemoteException + */ + void removeUidFromObserver(in IBinder observerToken, String callingPackage, int uid); + boolean isUidActive(int uid, String callingPackage); @JavaPassthrough(annotation= "@android.annotation.RequiresPermission(allOf = {android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional = true)") diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 2b1558937d21..4d308d90ce2d 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -259,6 +259,14 @@ interface IWallpaperManager { boolean lockScreenWallpaperExists(); /** + * Return true if there is a static wallpaper on the specified screen. With which=FLAG_LOCK, + * always return false if the lock screen doesn't run its own wallpaper engine. + * + * @hide + */ + boolean isStaticWallpaper(int which); + + /** * Temporary method for project b/197814683. * Return true if the lockscreen wallpaper always uses a WallpaperService, not a static image. * @hide diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 70c42d8f3b8a..c0106ab0bc11 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -658,15 +658,8 @@ public class WallpaperManager { return currentWallpaper; } } - if (returnDefault) { - Bitmap defaultWallpaper = mDefaultWallpaper; - if (defaultWallpaper == null || defaultWallpaper.isRecycled()) { - defaultWallpaper = getDefaultWallpaper(context, which); - synchronized (this) { - mDefaultWallpaper = defaultWallpaper; - } - } - return defaultWallpaper; + if (returnDefault || (which == FLAG_LOCK && isStaticWallpaper(FLAG_LOCK))) { + return getDefaultWallpaper(context, which); } return null; } @@ -705,7 +698,7 @@ public class WallpaperManager { } // If user wallpaper is unavailable, may be the default one instead. if ((dimensions == null || dimensions.width() == 0 || dimensions.height() == 0) - && returnDefault) { + && (returnDefault || (which == FLAG_LOCK && isStaticWallpaper(FLAG_LOCK)))) { InputStream is = openDefaultWallpaper(context, which); if (is != null) { try { @@ -769,18 +762,39 @@ public class WallpaperManager { } private Bitmap getDefaultWallpaper(Context context, @SetWallpaperFlags int which) { - InputStream is = openDefaultWallpaper(context, which); - if (is != null) { - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - return BitmapFactory.decodeStream(is, null, options); - } catch (OutOfMemoryError e) { + Bitmap defaultWallpaper = mDefaultWallpaper; + if (defaultWallpaper == null || defaultWallpaper.isRecycled()) { + defaultWallpaper = null; + try (InputStream is = openDefaultWallpaper(context, which)) { + if (is != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + defaultWallpaper = BitmapFactory.decodeStream(is, null, options); + } + } catch (OutOfMemoryError | IOException e) { Log.w(TAG, "Can't decode stream", e); - } finally { - IoUtils.closeQuietly(is); } } - return null; + synchronized (this) { + mDefaultWallpaper = defaultWallpaper; + } + return defaultWallpaper; + } + + /** + * Return true if there is a static wallpaper on the specified screen. + * With {@code which=}{@link #FLAG_LOCK}, always return false if the lockscreen doesn't run + * its own wallpaper engine. + */ + private boolean isStaticWallpaper(@SetWallpaperFlags int which) { + if (mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } + try { + return mService.isStaticWallpaper(which); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -882,21 +896,14 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Retrieve the current system wallpaper; if - * no wallpaper is set, the system built-in static wallpaper is returned. - * This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. * <p> - * This method can return null if there is no system wallpaper available, if - * wallpapers are not supported in the current user, or if the calling app is not - * permitted to access the system wallpaper. + * Equivalent to {@link #getDrawable(int)} with {@code which=}{@link #FLAG_SYSTEM}. + * </p> + * + * @return A Drawable object for the requested wallpaper. * - * @return Returns a Drawable object that will draw the system wallpaper, - * or {@code null} if no system wallpaper exists or if the calling application - * is not able to access the wallpaper. + * @see #getDrawable(int) * * @throws SecurityException as described in the note */ @@ -919,23 +926,29 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Retrieve the requested wallpaper; if - * no wallpaper is set, the requested built-in static wallpaper is returned. - * This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. * <p> - * This method can return null if the requested wallpaper is not available, if - * wallpapers are not supported in the current user, or if the calling app is not - * permitted to access the requested wallpaper. + * Retrieve the requested wallpaper for the specified wallpaper type if the wallpaper is not + * a live wallpaper. This method should not be used to display the user wallpaper on an app: + * {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WALLPAPER} should be used instead. + * </p> + * <p> + * When called with {@code which=}{@link #FLAG_SYSTEM}, + * if there is a live wallpaper on home screen, the built-in default wallpaper is returned. + * </p> + * <p> + * When called with {@code which=}{@link #FLAG_LOCK}, if there is a live wallpaper + * on lock screen, or if the lock screen and home screen share the same wallpaper engine, + * {@code null} is returned. + * </p> + * <p> + * {@link #getWallpaperInfo(int)} can be used to determine whether there is a live wallpaper + * on a specified screen type. + * </p> * - * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws + * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws * IllegalArgumentException if an invalid wallpaper is requested. - * @return Returns a Drawable object that will draw the requested wallpaper, - * or {@code null} if the requested wallpaper does not exist or if the calling application - * is not able to access the wallpaper. + * @return A Drawable object for the requested wallpaper. * * @throws SecurityException as described in the note */ @@ -943,7 +956,8 @@ public class WallpaperManager { @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable getDrawable(@SetWallpaperFlags int which) { final ColorManagementProxy cmProxy = getColorManagementProxy(); - Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, which, cmProxy); + boolean returnDefault = which != FLAG_LOCK; + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, returnDefault, which, cmProxy); if (bm != null) { Drawable dr = new BitmapDrawable(mContext.getResources(), bm); dr.setDither(false); @@ -1175,15 +1189,14 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Retrieve the current system wallpaper; if there is no wallpaper set, - * a null pointer is returned. This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. + * <p> + * Equivalent to {@link #getDrawable()}. + * </p> + * + * @return A Drawable object for the requested wallpaper. * - * @return Returns a Drawable object that will draw the wallpaper or a - * null pointer if wallpaper is unset. + * @see #getDrawable() * * @throws SecurityException as described in the note */ @@ -1206,31 +1219,23 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Retrieve the requested wallpaper; if there is no wallpaper set, - * a null pointer is returned. This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. + * <p> + * Equivalent to {@link #getDrawable(int)}. + * </p> * - * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws + * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws * IllegalArgumentException if an invalid wallpaper is requested. - * @return Returns a Drawable object that will draw the wallpaper or a null pointer if - * wallpaper is unset. + * @return A Drawable object for the requested wallpaper. + * + * @see #getDrawable(int) * * @throws SecurityException as described in the note */ @Nullable @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable peekDrawable(@SetWallpaperFlags int which) { - final ColorManagementProxy cmProxy = getColorManagementProxy(); - Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, which, cmProxy); - if (bm != null) { - Drawable dr = new BitmapDrawable(mContext.getResources(), bm); - dr.setDither(false); - return dr; - } - return null; + return getDrawable(which); } /** @@ -1246,19 +1251,14 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Like {@link #getDrawable()}, but the returned Drawable has a number - * of limitations to reduce its overhead as much as possible. It will - * never scale the wallpaper (only centering it if the requested bounds - * do match the bitmap bounds, which should not be typical), doesn't - * allow setting an alpha, color filter, or other attributes, etc. The - * bounds of the returned drawable will be initialized to the same bounds - * as the wallpaper, so normally you will not need to touch it. The - * drawable also assumes that it will be used in a context running in - * the same density as the screen (not in density compatibility mode). + * <p> + * Equivalent to {@link #getFastDrawable(int)} with {@code which=}{@link #FLAG_SYSTEM}. + * </p> + * + * @return A Drawable object for the requested wallpaper. * - * @return Returns a Drawable object that will draw the wallpaper. + * @see #getFastDrawable(int) * * @throws SecurityException as described in the note */ @@ -1295,7 +1295,8 @@ public class WallpaperManager { * * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws * IllegalArgumentException if an invalid wallpaper is requested. - * @return Returns a Drawable object that will draw the wallpaper. + * @return An optimized Drawable object for the requested wallpaper, or {@code null} + * in some cases as specified in {@link #getDrawable(int)}. * * @throws SecurityException as described in the note */ @@ -1303,7 +1304,8 @@ public class WallpaperManager { @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable getFastDrawable(@SetWallpaperFlags int which) { final ColorManagementProxy cmProxy = getColorManagementProxy(); - Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, which, cmProxy); + boolean returnDefault = which != FLAG_LOCK; + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, returnDefault, which, cmProxy); if (bm != null) { return new FastBitmapDrawable(bm); } @@ -1323,13 +1325,14 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Like {@link #getFastDrawable()}, but if there is no wallpaper set, - * a null pointer is returned. + * <p> + * Equivalent to {@link #getFastDrawable()}. + * </p> * - * @return Returns an optimized Drawable object that will draw the - * wallpaper or a null pointer if these is none. + * @return An optimized Drawable object for the requested wallpaper. + * + * @see #getFastDrawable() * * @throws SecurityException as described in the note */ @@ -1352,31 +1355,29 @@ public class WallpaperManager { * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} * can still access the real wallpaper on all versions. </li> * </ul> - * <br> * - * Like {@link #getFastDrawable()}, but if there is no wallpaper set, - * a null pointer is returned. + * <p> + * Equivalent to {@link #getFastDrawable(int)}. + * </p> * * @param which The {@code FLAG_*} identifier of a valid wallpaper type. Throws * IllegalArgumentException if an invalid wallpaper is requested. - * @return Returns an optimized Drawable object that will draw the - * wallpaper or a null pointer if these is none. + * @return An optimized Drawable object for the requested wallpaper. * * @throws SecurityException as described in the note */ @Nullable @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable peekFastDrawable(@SetWallpaperFlags int which) { - final ColorManagementProxy cmProxy = getColorManagementProxy(); - Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, which, cmProxy); - if (bm != null) { - return new FastBitmapDrawable(bm); - } - return null; + return getFastDrawable(which); } /** - * Whether the wallpaper supports Wide Color Gamut or not. + * Whether the wallpaper supports Wide Color Gamut or not. This is only meant to be used by + * ImageWallpaper, and will always return false if the wallpaper for the specified screen + * is not an ImageWallpaper. This will also return false when called with {@link #FLAG_LOCK} if + * the lock and home screen share the same wallpaper engine. + * * @param which The wallpaper whose image file is to be retrieved. Must be a single * defined kind of wallpaper, either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. * @return true when supported. @@ -1422,7 +1423,7 @@ public class WallpaperManager { } /** - * Like {@link #getDrawable()} but returns a Bitmap. + * Like {@link #getDrawable(int)} but returns a Bitmap. * * @param hardware Asks for a hardware backed bitmap. * @param which Specifies home or lock screen @@ -1445,7 +1446,7 @@ public class WallpaperManager { } /** - * Like {@link #getDrawable()} but returns a Bitmap for the provided user. + * Like {@link #getDrawable(int)} but returns a Bitmap for the provided user. * * @param which Specifies home or lock screen * @hide @@ -1453,12 +1454,29 @@ public class WallpaperManager { @TestApi @Nullable public Bitmap getBitmapAsUser(int userId, boolean hardware, @SetWallpaperFlags int which) { + boolean returnDefault = which != FLAG_LOCK; + return getBitmapAsUser(userId, hardware, which, returnDefault); + } + + /** + * Overload of {@link #getBitmapAsUser(int, boolean, int)} with a returnDefault argument. + * + * @param returnDefault If true, return the default static wallpaper if no custom static + * wallpaper is set on the specified screen. + * If false, return {@code null} in that case. + * @hide + */ + @Nullable + public Bitmap getBitmapAsUser(int userId, boolean hardware, + @SetWallpaperFlags int which, boolean returnDefault) { final ColorManagementProxy cmProxy = getColorManagementProxy(); - return sGlobals.peekWallpaperBitmap(mContext, true, which, userId, hardware, cmProxy); + return sGlobals.peekWallpaperBitmap(mContext, returnDefault, + which, userId, hardware, cmProxy); } /** * Peek the dimensions of system wallpaper of the user without decoding it. + * Equivalent to {@link #peekBitmapDimensions(int)} with {@code which=}{@link #FLAG_SYSTEM}. * * @return the dimensions of system wallpaper * @hide @@ -1472,16 +1490,45 @@ public class WallpaperManager { /** * Peek the dimensions of given wallpaper of the user without decoding it. * - * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or - * {@link #FLAG_LOCK}. - * @return the dimensions of system wallpaper + * <p> + * When called with {@code which=}{@link #FLAG_SYSTEM}, if there is a live wallpaper on + * home screen, the built-in default wallpaper dimensions are returned. + * </p> + * <p> + * When called with {@code which=}{@link #FLAG_LOCK}, if there is a live wallpaper + * on lock screen, or if the lock screen and home screen share the same wallpaper engine, + * {@code null} is returned. + * </p> + * <p> + * {@link #getWallpaperInfo(int)} can be used to determine whether there is a live wallpaper + * on a specified screen type. + * </p> + * + * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @return the dimensions of specified wallpaper * @hide */ @TestApi @Nullable public Rect peekBitmapDimensions(@SetWallpaperFlags int which) { + boolean returnDefault = which != FLAG_LOCK; + return peekBitmapDimensions(which, returnDefault); + } + + /** + * Overload of {@link #peekBitmapDimensions(int)} with a returnDefault argument. + * + * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}. + * @param returnDefault If true, always return the default static wallpaper dimensions + * if no custom static wallpaper is set on the specified screen. + * If false, always return {@code null} in that case. + * @return the dimensions of specified wallpaper + * @hide + */ + @Nullable + public Rect peekBitmapDimensions(@SetWallpaperFlags int which, boolean returnDefault) { checkExactlyOneWallpaperFlagSet(which); - return sGlobals.peekWallpaperDimensions(mContext, true /* returnDefault */, which, + return sGlobals.peekWallpaperDimensions(mContext, returnDefault, which, mContext.getUserId()); } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 27270d9f378f..d6592d572726 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -765,10 +765,6 @@ public class LauncherApps { @Nullable public PendingIntent getMainActivityLaunchIntent(@NonNull ComponentName component, @Nullable Bundle startActivityOptions, @NonNull UserHandle user) { - if (mContext.checkSelfPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) - != PackageManager.PERMISSION_GRANTED) { - Log.w(TAG, "Only allowed for recents."); - } logErrorForInvalidProfileAccess(user); if (DEBUG) { Log.i(TAG, "GetMainActivityLaunchIntent " + component + " " + user); diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 9ebb0585afd0..3fc3be58f022 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -52,6 +52,12 @@ public final class RequestInfo implements Parcelable { @NonNull public static final String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED"; /** Type value for a getCredential request. */ @NonNull public static final String TYPE_GET = "android.credentials.ui.TYPE_GET"; + /** Type value for a getCredential request that utilizes the credential registry. + * + * @hide + **/ + @NonNull public static final String TYPE_GET_VIA_REGISTRY = + "android.credentials.ui.TYPE_GET_VIA_REGISTRY"; /** Type value for a createCredential request. */ @NonNull public static final String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE"; diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index 069c264313e7..dcf1a47c3f70 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -1136,7 +1136,10 @@ public final class SQLiteConnectionPool implements Closeable { Printer indentedPrinter = PrefixPrinter.create(printer, " "); synchronized (mLock) { if (directories != null) { - directories.add(new File(mConfiguration.path).getParent()); + String parent = new File(mConfiguration.path).getParent(); + if (parent != null) { + directories.add(parent); + } } boolean isCompatibilityWalEnabled = mConfiguration.isLegacyCompatibilityWalEnabled(); printer.println("Connection pool for " + mConfiguration.path + ":"); diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java index 22f59021bca1..f9a2dbb1bdb4 100644 --- a/core/java/android/hardware/radio/TunerCallbackAdapter.java +++ b/core/java/android/hardware/radio/TunerCallbackAdapter.java @@ -144,6 +144,9 @@ final class TunerCallbackAdapter extends ITunerCallback.Stub { int errorCode; switch (status) { + case RadioTuner.TUNER_RESULT_CANCELED: + errorCode = RadioTuner.ERROR_CANCELLED; + break; case RadioManager.STATUS_PERMISSION_DENIED: case RadioManager.STATUS_DEAD_OBJECT: errorCode = RadioTuner.ERROR_SERVER_DIED; @@ -152,10 +155,16 @@ final class TunerCallbackAdapter extends ITunerCallback.Stub { case RadioManager.STATUS_NO_INIT: case RadioManager.STATUS_BAD_VALUE: case RadioManager.STATUS_INVALID_OPERATION: + case RadioTuner.TUNER_RESULT_INTERNAL_ERROR: + case RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS: + case RadioTuner.TUNER_RESULT_INVALID_STATE: + case RadioTuner.TUNER_RESULT_NOT_SUPPORTED: + case RadioTuner.TUNER_RESULT_UNKNOWN_ERROR: Log.i(TAG, "Got an error with no mapping to the legacy API (" + status + "), doing a best-effort conversion to ERROR_SCAN_TIMEOUT"); // fall through case RadioManager.STATUS_TIMED_OUT: + case RadioTuner.TUNER_RESULT_TIMEOUT: default: errorCode = RadioTuner.ERROR_SCAN_TIMEOUT; } diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index 4ce9184f0b96..ef3901082833 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -56,6 +56,7 @@ public final class UserHandle implements Parcelable { /** @hide A user id to indicate the currently active user */ @UnsupportedAppUsage + @TestApi public static final @UserIdInt int USER_CURRENT = -2; /** @hide A user handle to indicate the current user of the device */ diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 5bcbaa10e95b..8606687d8c68 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -4224,7 +4224,8 @@ public class UserManager { android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS }) - public List<UserInfo> getUsers() { + @TestApi + public @NonNull List<UserInfo> getUsers() { return getUsers(/*excludePartial= */ true, /* excludeDying= */ false, /* excludePreCreated= */ true); } @@ -4245,6 +4246,7 @@ public class UserManager { android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS }) + @TestApi public @NonNull List<UserInfo> getAliveUsers() { return getUsers(/*excludePartial= */ true, /* excludeDying= */ true, /* excludePreCreated= */ true); @@ -4271,8 +4273,7 @@ public class UserManager { * Returns information for all users on this device, based on the filtering parameters. * * @deprecated Pre-created users are deprecated and no longer supported. - * Use {@link #getUsers()}, {@link #getUsers(boolean)}, or {@link #getAliveUsers()} - * instead. + * Use {@link #getUsers()}, or {@link #getAliveUsers()} instead. * @hide */ @Deprecated diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index 77c00676878c..ac6b2b23cdf5 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -1293,7 +1293,8 @@ public class CallLog { USER_MISSED_NO_VIBRATE, USER_MISSED_CALL_SCREENING_SERVICE_SILENCED, USER_MISSED_CALL_FILTERS_TIMEOUT, - USER_MISSED_NEVER_RANG + USER_MISSED_NEVER_RANG, + USER_MISSED_NOT_RUNNING }) @Retention(RetentionPolicy.SOURCE) public @interface MissedReason {} @@ -1391,6 +1392,13 @@ public class CallLog { public static final long USER_MISSED_NEVER_RANG = 1 << 23; /** + * When {@link CallLog.Calls#TYPE} is {@link CallLog.Calls#MISSED_TYPE}, set this bit when + * the user receiving the call is not running (i.e. work profile paused). + * @hide + */ + public static final long USER_MISSED_NOT_RUNNING = 1 << 24; + + /** * Where the {@link CallLog.Calls#TYPE} is {@link CallLog.Calls#MISSED_TYPE}, * indicates factors which may have lead the user to miss the call. * <P>Type: INTEGER</P> diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8cdb568b407c..73c29d4058cd 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3036,9 +3036,7 @@ public final class Settings { public void destroy() { try { - // If this process is the system server process, mArray is the same object as - // the memory int array kept inside SettingsProvider, so skipping the close() - if (!Settings.isInSystemServer() && !mArray.isClosed()) { + if (!mArray.isClosed()) { mArray.close(); } } catch (IOException e) { @@ -3218,8 +3216,9 @@ public final class Settings { @UnsupportedAppUsage public String getStringForUser(ContentResolver cr, String name, final int userHandle) { final boolean isSelf = (userHandle == UserHandle.myUserId()); + final boolean useCache = isSelf && !isInSystemServer(); boolean needsGenerationTracker = false; - if (isSelf) { + if (useCache) { synchronized (NameValueCache.this) { final GenerationTracker generationTracker = mGenerationTrackers.get(name); if (generationTracker != null) { @@ -3365,9 +3364,12 @@ public final class Settings { } } } else { - if (LOCAL_LOGV) Log.i(TAG, "call-query of user " + userHandle - + " by " + UserHandle.myUserId() - + " so not updating cache"); + if (DEBUG || LOCAL_LOGV) { + Log.i(TAG, "call-query of user " + userHandle + + " by " + UserHandle.myUserId() + + (isInSystemServer() ? " in system_server" : "") + + " so not updating cache"); + } } return value; } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 8d84e4413635..230f51191ee8 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -184,6 +184,7 @@ public abstract class WallpaperService extends Service { private static final long DIMMING_ANIMATION_DURATION_MS = 300L; + @GuardedBy("itself") private final ArrayMap<IBinder, IWallpaperEngineWrapper> mActiveEngines = new ArrayMap<>(); private Handler mBackgroundHandler; @@ -2514,10 +2515,12 @@ public abstract class WallpaperService extends Service { // if they are visible, so we need to toggle the state to get their attention. if (!mEngine.mDestroyed) { mEngine.detach(); - for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { - if (engineWrapper.mEngine != null && engineWrapper.mEngine.mVisible) { - engineWrapper.mEngine.doVisibilityChanged(false); - engineWrapper.mEngine.doVisibilityChanged(true); + synchronized (mActiveEngines) { + for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { + if (engineWrapper.mEngine != null && engineWrapper.mEngine.mVisible) { + engineWrapper.mEngine.doVisibilityChanged(false); + engineWrapper.mEngine.doVisibilityChanged(true); + } } } } @@ -2699,7 +2702,9 @@ public abstract class WallpaperService extends Service { IWallpaperEngineWrapper engineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight, padding, displayId, which); - mActiveEngines.put(windowToken, engineWrapper); + synchronized (mActiveEngines) { + mActiveEngines.put(windowToken, engineWrapper); + } if (DEBUG) { Slog.v(TAG, "IWallpaperServiceWrapper Attaching window token " + windowToken); } @@ -2708,7 +2713,10 @@ public abstract class WallpaperService extends Service { @Override public void detach(IBinder windowToken) { - IWallpaperEngineWrapper engineWrapper = mActiveEngines.remove(windowToken); + IWallpaperEngineWrapper engineWrapper; + synchronized (mActiveEngines) { + engineWrapper = mActiveEngines.remove(windowToken); + } if (engineWrapper == null) { Log.w(TAG, "Engine for window token " + windowToken + " already detached"); return; @@ -2734,10 +2742,12 @@ public abstract class WallpaperService extends Service { public void onDestroy() { Trace.beginSection("WPMS.onDestroy"); super.onDestroy(); - for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { - engineWrapper.destroy(); + synchronized (mActiveEngines) { + for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { + engineWrapper.destroy(); + } + mActiveEngines.clear(); } - mActiveEngines.clear(); if (mBackgroundThread != null) { // onDestroy might be called without a previous onCreate if WallpaperService was // instantiated manually. While this is a misuse of the API, some things break @@ -2768,14 +2778,18 @@ public abstract class WallpaperService extends Service { @Override protected void dump(FileDescriptor fd, PrintWriter out, String[] args) { out.print("State of wallpaper "); out.print(this); out.println(":"); - for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { - Engine engine = engineWrapper.mEngine; - if (engine == null) { - Slog.w(TAG, "Engine for wrapper " + engineWrapper + " not attached"); - continue; - } - out.print(" Engine "); out.print(engine); out.println(":"); - engine.dump(" ", fd, out, args); + synchronized (mActiveEngines) { + for (IWallpaperEngineWrapper engineWrapper : mActiveEngines.values()) { + Engine engine = engineWrapper.mEngine; + if (engine == null) { + Slog.w(TAG, "Engine for wrapper " + engineWrapper + " not attached"); + continue; + } + out.print(" Engine "); + out.print(engine); + out.println(":"); + engine.dump(" ", fd, out, args); + } } } } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 196bac21ad74..cd767541e13a 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -1106,6 +1106,16 @@ public class DynamicLayout extends Layout { mTransformedTextUpdate.before = before; mTransformedTextUpdate.after = after; } + // When there is a transformed text, we have to reflow the DynamicLayout based on + // the transformed indices instead of the range in base text. + // For example, + // base text: abcd > abce + // updated range: where = 3, before = 1, after = 1 + // transformed text: abxxcd > abxxce + // updated range: where = 5, before = 1, after = 1 + // + // Because the transformedText is udapted simultaneously with the base text, + // the range must be transformed before the base text changes. transformedText.originalToTransformed(mTransformedTextUpdate); } } @@ -1113,9 +1123,20 @@ public class DynamicLayout extends Layout { public void onTextChanged(CharSequence s, int where, int before, int after) { final DynamicLayout dynamicLayout = mLayout.get(); if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { - where = mTransformedTextUpdate.where; - before = mTransformedTextUpdate.before; - after = mTransformedTextUpdate.after; + if (mTransformedTextUpdate != null && mTransformedTextUpdate.where >= 0) { + where = mTransformedTextUpdate.where; + before = mTransformedTextUpdate.before; + after = mTransformedTextUpdate.after; + // Set where to -1 so that we know if beforeTextChanged is called. + mTransformedTextUpdate.where = -1; + } else { + // onTextChanged is called without beforeTextChanged. Reflow the entire text. + where = 0; + // We can't get the before length from the text, use the line end of the + // last line instead. + before = dynamicLayout.getLineEnd(dynamicLayout.getLineCount() - 1); + after = dynamicLayout.mDisplay.length(); + } } reflow(s, where, before, after); } diff --git a/core/java/android/text/method/InsertModeTransformationMethod.java b/core/java/android/text/method/InsertModeTransformationMethod.java index 0c933d996ff3..59b80f3a6468 100644 --- a/core/java/android/text/method/InsertModeTransformationMethod.java +++ b/core/java/android/text/method/InsertModeTransformationMethod.java @@ -37,6 +37,8 @@ import android.view.View; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import java.lang.reflect.Array; + /** * The transformation method used by handwriting insert mode. * This transformation will insert a placeholder string to the original text at the given @@ -309,26 +311,51 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex return ArrayUtils.emptyArray(type); } - final T[] spansOriginal; + T[] spansOriginal = null; if (mSpannedOriginal != null) { final int originalStart = transformedToOriginal(start, OffsetMapping.MAP_STRATEGY_CURSOR); final int originalEnd = transformedToOriginal(end, OffsetMapping.MAP_STRATEGY_CURSOR); + // We can't simply call SpannedString.getSpans(originalStart, originalEnd) here. + // When start == end SpannedString.getSpans returns spans whose spanEnd == start. + // For example, + // text: abcd span: [1, 3) + // getSpan(3, 3) will return the span [1, 3) but getSpan(3, 4) returns no span. + // + // This creates some special cases when originalStart == originalEnd. + // For example: + // original text: abcd span1: [1, 3) span2: [3, 4) span3: [3, 3) + // transformed text: abc\n\nd span1: [1, 3) span2: [5, 6) span3: [3, 3) + // Case 1: + // When start = 3 and end = 4, transformedText#getSpan(3, 4) should return span3. + // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3) + // returns span1, span2 and span3. + // + // Case 2: + // When start == end == 4, transformedText#getSpan(4, 4) should return nothing. + // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3) + // return span1, span2 and span3. + // + // Case 3: + // When start == end == 5, transformedText#getSpan(5, 5) should return span2. + // However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3) + // return span1, span2 and span3. + // + // To handle the issue, we need to filter out the invalid spans. spansOriginal = mSpannedOriginal.getSpans(originalStart, originalEnd, type); - } else { - spansOriginal = null; + spansOriginal = ArrayUtils.filter(spansOriginal, + size -> (T[]) Array.newInstance(type, size), + span -> intersect(getSpanStart(span), getSpanEnd(span), start, end)); } - final T[] spansPlaceholder; + T[] spansPlaceholder = null; if (mSpannedPlaceholder != null && intersect(start, end, mEnd, mEnd + mPlaceholder.length())) { - final int placeholderStart = Math.max(start - mEnd, 0); - final int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length()); + int placeholderStart = Math.max(start - mEnd, 0); + int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length()); spansPlaceholder = mSpannedPlaceholder.getSpans(placeholderStart, placeholderEnd, type); - } else { - spansPlaceholder = null; } // TODO: sort the spans based on their priority. @@ -340,7 +367,10 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex if (mSpannedOriginal != null) { final int index = mSpannedOriginal.getSpanStart(tag); if (index >= 0) { - if (index < mEnd) { + // When originalSpanStart == originalSpanEnd == mEnd, the span should be + // considered "before" the placeholder text. So we return the originalSpanStart. + if (index < mEnd + || (index == mEnd && mSpannedOriginal.getSpanEnd(tag) == index)) { return index; } return index + mPlaceholder.length(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 153bfde07758..3208b6281110 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -301,6 +301,14 @@ public final class ViewRootImpl implements ViewParent, SystemProperties.getBoolean("persist.wm.debug.caption_on_shell", true); /** + * Whether the client (system UI) is handling the transient gesture and the corresponding + * animation. + * @hide + */ + public static final boolean CLIENT_TRANSIENT = + SystemProperties.getBoolean("persist.wm.debug.client_transient", false); + + /** * Whether the client should compute the window frame on its own. * @hide */ diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index a6e9d4d2094f..5d121ad2957f 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -1464,6 +1464,13 @@ public final class AutofillManager { } synchronized (mLock) { + if (mAllTrackedViews.contains(id)) { + // The id is tracked and will not trigger pre-fill request again. + return; + } + + // Add the id as tracked to avoid triggering fill request again and again. + mAllTrackedViews.add(id); if (mTrackedViews != null) { // To support the fill dialog can show for the autofillable Views in // different pages but in the same Activity. We need to reset the @@ -4064,11 +4071,6 @@ public final class AutofillManager { } void checkViewState(AutofillId id) { - if (mAllTrackedViews.contains(id)) { - return; - } - // Add the id as tracked to avoid triggering fill request again and again. - mAllTrackedViews.add(id); if (mHasNewTrackedView) { return; } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 3165654d806d..c28950662fb3 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -726,6 +726,12 @@ public class RemoteViews implements Parcelable, Filter { mActions.get(i).visitUris(visitor); } } + if (mLandscape != null) { + mLandscape.visitUris(visitor); + } + if (mPortrait != null) { + mPortrait.visitUris(visitor); + } } private static void visitIconUri(Icon icon, @NonNull Consumer<Uri> visitor) { diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 617519b1b540..fdcb87ff5e3f 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -1468,6 +1468,11 @@ public class BatteryStatsHistory { mHistoryLastLastWritten.setTo(mHistoryLastWritten); final boolean hasTags = mHistoryLastWritten.tagsFirstOccurrence || cur.tagsFirstOccurrence; mHistoryLastWritten.setTo(mHistoryBaseTimeMs + elapsedRealtimeMs, cmd, cur); + if (mHistoryLastWritten.time < mHistoryLastLastWritten.time - 60000) { + Slog.wtf(TAG, "Significantly earlier event written to battery history:" + + " time=" + mHistoryLastWritten.time + + " previous=" + mHistoryLastLastWritten.time); + } mHistoryLastWritten.tagsFirstOccurrence = hasTags; writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten); mLastHistoryElapsedRealtimeMs = elapsedRealtimeMs; @@ -1908,12 +1913,6 @@ public class BatteryStatsHistory { in.setDataPosition(curPos + bufSize); } - if (DEBUG) { - StringBuilder sb = new StringBuilder(128); - sb.append("****************** OLD mHistoryBaseTimeMs: "); - TimeUtils.formatDuration(mHistoryBaseTimeMs, sb); - Slog.i(TAG, sb.toString()); - } mHistoryBaseTimeMs = historyBaseTime; if (DEBUG) { StringBuilder sb = new StringBuilder(128); @@ -1922,11 +1921,10 @@ public class BatteryStatsHistory { Slog.i(TAG, sb.toString()); } - // We are just arbitrarily going to insert 1 minute from the sample of - // the last run until samples in this run. if (mHistoryBaseTimeMs > 0) { - long oldnow = mClock.elapsedRealtime(); - mHistoryBaseTimeMs = mHistoryBaseTimeMs - oldnow + 1; + long elapsedRealtimeMs = mClock.elapsedRealtime(); + mLastHistoryElapsedRealtimeMs = elapsedRealtimeMs; + mHistoryBaseTimeMs = mHistoryBaseTimeMs - elapsedRealtimeMs + 1; if (DEBUG) { StringBuilder sb = new StringBuilder(128); sb.append("****************** ADJUSTED mHistoryBaseTimeMs: "); diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java index c1445035ca33..f2776353fd1b 100644 --- a/core/java/com/android/internal/util/LatencyTracker.java +++ b/core/java/com/android/internal/util/LatencyTracker.java @@ -305,10 +305,17 @@ public class LatencyTracker { private final SparseArray<ActionProperties> mActionPropertiesMap = new SparseArray<>(); @GuardedBy("mLock") private boolean mEnabled; + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = + this::updateProperties; // Wrapping this in a holder class achieves lazy loading behavior private static final class SLatencyTrackerHolder { - private static final LatencyTracker sLatencyTracker = new LatencyTracker(); + private static final LatencyTracker sLatencyTracker; + + static { + sLatencyTracker = new LatencyTracker(); + sLatencyTracker.startListeningForLatencyTrackerConfigChanges(); + } } public static LatencyTracker getInstance(Context context) { @@ -319,31 +326,16 @@ public class LatencyTracker { * Constructor for LatencyTracker * * <p>This constructor is only visible for test classes to inject their own consumer callbacks + * + * @param startListeningForPropertyChanges If set, constructor will register for device config + * property updates prior to returning. If not set, + * {@link #startListeningForLatencyTrackerConfigChanges} must be called + * to start listening. */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) @VisibleForTesting public LatencyTracker() { mEnabled = DEFAULT_ENABLED; - - final Context context = ActivityThread.currentApplication(); - if (context != null - && context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) == PERMISSION_GRANTED) { - // Post initialization to the background in case we're running on the main thread. - BackgroundThread.getHandler().post(() -> this.updateProperties( - DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER))); - DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER, - BackgroundThread.getExecutor(), this::updateProperties); - } else { - if (DEBUG) { - if (context == null) { - Log.d(TAG, "No application for " + ActivityThread.currentActivityThread()); - } else { - Log.d(TAG, "Initialized the LatencyTracker." - + " (No READ_DEVICE_CONFIG permission to change configs)" - + " enabled=" + mEnabled + ", package=" + context.getPackageName()); - } - } - } } private void updateProperties(DeviceConfig.Properties properties) { @@ -366,6 +358,54 @@ public class LatencyTracker { } /** + * Test method to start listening to {@link DeviceConfig} properties changes. + * + * <p>During testing, a {@link LatencyTracker} it is desired to stop and start listening for + * config updates. + * + * <p>This is not used for production usages of this class outside of testing as we are + * using a single static object. + */ + @VisibleForTesting + public void startListeningForLatencyTrackerConfigChanges() { + final Context context = ActivityThread.currentApplication(); + if (context != null + && context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) == PERMISSION_GRANTED) { + // Post initialization to the background in case we're running on the main thread. + BackgroundThread.getHandler().post(() -> this.updateProperties( + DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER))); + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER, + BackgroundThread.getExecutor(), mOnPropertiesChangedListener); + } else { + if (DEBUG) { + if (context == null) { + Log.d(TAG, "No application for " + ActivityThread.currentActivityThread()); + } else { + synchronized (mLock) { + Log.d(TAG, "Initialized the LatencyTracker." + + " (No READ_DEVICE_CONFIG permission to change configs)" + + " enabled=" + mEnabled + ", package=" + context.getPackageName()); + } + } + } + } + } + + /** + * Test method to stop listening to {@link DeviceConfig} properties changes. + * + * <p>During testing, a {@link LatencyTracker} it is desired to stop and start listening for + * config updates. + * + * <p>This is not used for production usages of this class outside of testing as we are + * using a single static object. + */ + @VisibleForTesting + public void stopListeningForLatencyTrackerConfigChanges() { + DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); + } + + /** * A helper method to translate action type to name. * * @param atomsProtoAction the action type defined in AtomsProto.java diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index fefa79f65201..7e0a36d30771 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4976,11 +4976,11 @@ android:protectionLevel="signature" /> <!-- Allows an application to subscribe to keyguard locked (i.e., showing) state. - <p>Protection level: internal|role - <p>Intended for use by ROLE_ASSISTANT only. + <p>Protection level: signature|role + <p>Intended for use by ROLE_ASSISTANT and signature apps only. --> <permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" - android:protectionLevel="internal|role"/> + android:protectionLevel="signature|role"/> <!-- Must be required by a {@link android.service.autofill.AutofillService}, to ensure that only the system can bind to it. diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 7de36a7113ae..6f7bc53e891c 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2791,7 +2791,7 @@ <flag name="noExcludeDescendants" value="0x8" /> </attr> - <!-- Boolean that hints the Android System that the view is credntial and associated with + <!-- Boolean that hints the Android System that the view is credential and associated with CredentialManager --> <attr name="isCredential" format="boolean" /> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java index c7b82b1caca6..6a6a951c94c8 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java @@ -750,6 +750,15 @@ public final class TunerAdapterTest { } @Test + public void onTuneFailed_withCanceledResult() throws Exception { + mTunerCallback.onTuneFailed(RadioTuner.TUNER_RESULT_CANCELED, FM_SELECTOR); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed( + RadioTuner.TUNER_RESULT_CANCELED, FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_CANCELLED); + } + + @Test public void onProgramListChanged_forTunerCallbackAdapter() throws Exception { mTunerCallback.onProgramListChanged(); diff --git a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java index 76f417151001..5939c0609e18 100644 --- a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java +++ b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java @@ -119,6 +119,86 @@ public class DynamicLayoutOffsetMappingTest { assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8); } + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_deletion() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = + new TestOffsetMapping(spannable, 5, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + // delete "cd", original text becomes "abef" + spannable.delete(2, 4); + assertThat(transformedText.toString()).isEqualTo("abe\n\nf"); + assertLineRange(layout, /* lineBreaks */ 0, 4, 5, 6); + + // delete "abe", original text becomes "f" + spannable.delete(0, 3); + assertThat(transformedText.toString()).isEqualTo("\n\nf"); + assertLineRange(layout, /* lineBreaks */ 0, 1, 2, 3); + } + + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_insertion() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + spannable.insert(3, "x"); + assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9); + + spannable.insert(5, "x"); + assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10); + } + + @Test + public void textWithOffsetMapping_blockBeforeTextChanged_replace() { + final String text = "abcdef"; + final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text); + final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + spannable.replace(2, 4, "xx"); + assertThat(transformedText.toString()).isEqualTo("abxx\n\nef"); + assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8); + } + + @Test + public void textWithOffsetMapping_onlyCallOnTextChanged_notCrash() { + String text = "abcdef"; + SpannableStringBuilder spannable = new SpannableStringBuilder(text); + CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n"); + + DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(false) + .setDisplayText(transformedText) + .build(); + + TextWatcher[] textWatcher = spannable.getSpans(0, spannable.length(), TextWatcher.class); + assertThat(textWatcher.length).isEqualTo(1); + + textWatcher[0].onTextChanged(spannable, 0, 2, 2); + } + private void assertLineRange(Layout layout, int... lineBreaks) { final int lineCount = lineBreaks.length - 1; assertThat(layout.getLineCount()).isEqualTo(lineCount); @@ -129,6 +209,50 @@ public class DynamicLayoutOffsetMappingTest { } /** + * A test SpannableStringBuilder that doesn't call beforeTextChanged. It's used to test + * DynamicLayout against some special cases where beforeTextChanged callback is not properly + * called. + */ + private static class TestNoBeforeTextChangeSpannableString extends SpannableStringBuilder { + + TestNoBeforeTextChangeSpannableString(CharSequence text) { + super(text); + } + + @Override + public void setSpan(Object what, int start, int end, int flags) { + if (what instanceof TextWatcher) { + super.setSpan(new TestNoBeforeTextChangeWatcherWrapper((TextWatcher) what), start, + end, flags); + } else { + super.setSpan(what, start, end, flags); + } + } + } + + /** A TextWatcherWrapper that blocks beforeTextChanged callback. */ + private static class TestNoBeforeTextChangeWatcherWrapper implements TextWatcher { + private final TextWatcher mTextWatcher; + + TestNoBeforeTextChangeWatcherWrapper(TextWatcher textWatcher) { + mTextWatcher = textWatcher; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mTextWatcher.onTextChanged(s, start, before, count); + } + + @Override + public void afterTextChanged(Editable s) { + mTextWatcher.afterTextChanged(s); + } + } + + /** * A test TransformedText that inserts some text at the given offset. */ private static class TestOffsetMapping implements OffsetMapping, CharSequence { diff --git a/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java index 7706d9aebdcc..9ef137beebb2 100644 --- a/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java +++ b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java @@ -224,6 +224,12 @@ public class InsertModeTransformationMethodTest { assertThat(spans0to2.length).isEqualTo(1); assertThat(spans0to2[0]).isEqualTo(span1); + // only span2 is in the range of [3, 4). + // note: span1 [0, 3) is not in the range because [3, 4) is not collapsed. + final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class); + assertThat(spans3to4.length).isEqualTo(1); + assertThat(spans3to4[0]).isEqualTo(span2); + // span1 and span2 are in the range of [1, 6). final TestSpan[] spans1to6 = transformedText.getSpans(1, 6, TestSpan.class); assertThat(spans1to6.length).isEqualTo(2); @@ -262,7 +268,7 @@ public class InsertModeTransformationMethodTest { text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - // In the transformedText, the new ranges of the spans are: + // In the transformedText "abc\uFFFD def", the new ranges of the spans are: // span1: [0, 3) // span2: [2, 5) // span3: [5, 6) @@ -277,6 +283,12 @@ public class InsertModeTransformationMethodTest { assertThat(spans0to2.length).isEqualTo(1); assertThat(spans0to2[0]).isEqualTo(span1); + // only span2 is in the range of [3, 4). + // note: span1 [0, 3) is not in the range because [3, 4) is not collapsed. + final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class); + assertThat(spans3to4.length).isEqualTo(1); + assertThat(spans3to4[0]).isEqualTo(span2); + // span1 and span2 are in the range of [1, 5). final TestSpan[] spans1to4 = transformedText.getSpans(1, 4, TestSpan.class); assertThat(spans1to4.length).isEqualTo(2); @@ -318,20 +330,143 @@ public class InsertModeTransformationMethodTest { } @Test + public void transformedText_getSpans_collapsedRange() { + final SpannableString text = new SpannableString(TEXT); + final TestSpan span1 = new TestSpan(); + final TestSpan span2 = new TestSpan(); + final TestSpan span3 = new TestSpan(); + + text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span2, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span3, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + // In the transformedText "abc\n\n def", the new ranges of the spans are: + // span1: [0, 3) + // span2: [3, 3) + // span3: [5, 6) + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, false, null); + final Spanned transformedText = + (Spanned) transformationMethod.getTransformation(text, sView); + + // only span1 is in the range of [0, 0). + final TestSpan[] spans0to0 = transformedText.getSpans(0, 0, TestSpan.class); + assertThat(spans0to0.length).isEqualTo(1); + assertThat(spans0to0[0]).isEqualTo(span1); + + // span1 and span 2 are in the range of [3, 3). + final TestSpan[] spans3to3 = transformedText.getSpans(3, 3, TestSpan.class); + assertThat(spans3to3.length).isEqualTo(2); + assertThat(spans3to3[0]).isEqualTo(span1); + assertThat(spans3to3[1]).isEqualTo(span2); + + // only the span2 with collapsed range is in the range of [3, 4). + final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class); + assertThat(spans3to4.length).isEqualTo(1); + assertThat(spans3to4[0]).isEqualTo(span2); + + // no span is in the range of [4, 5). (span2 is not mistakenly included.) + final TestSpan[] spans4to5 = transformedText.getSpans(4, 5, TestSpan.class); + assertThat(spans4to5).isEmpty(); + + // only span3 is in the range of [4, 6). (span2 is not mistakenly included.) + final TestSpan[] spans4to6 = transformedText.getSpans(4, 6, TestSpan.class); + assertThat(spans4to6.length).isEqualTo(1); + assertThat(spans4to6[0]).isEqualTo(span3); + + // no span is in the range of [4, 4). + final TestSpan[] spans4to4 = transformedText.getSpans(4, 4, TestSpan.class); + assertThat(spans4to4.length).isEqualTo(0); + + // span3 is in the range of [5, 5). + final TestSpan[] spans5to5 = transformedText.getSpans(5, 5, TestSpan.class); + assertThat(spans5to5.length).isEqualTo(1); + assertThat(spans5to5[0]).isEqualTo(span3); + + // span3 is in the range of [6, 6). + final TestSpan[] spans6to6 = transformedText.getSpans(6, 6, TestSpan.class); + assertThat(spans6to6.length).isEqualTo(1); + assertThat(spans6to6[0]).isEqualTo(span3); + } + + @Test + public void transformedText_getSpans_collapsedRange_singleLine() { + final SpannableString text = new SpannableString(TEXT); + final TestSpan span1 = new TestSpan(); + final TestSpan span2 = new TestSpan(); + final TestSpan span3 = new TestSpan(); + + text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span2, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span3, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + // In the transformedText "abc\uFFFD def", the new ranges of the spans are: + // span1: [0, 3) + // span2: [3, 3) + // span3: [4, 5) + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, true, null); + final Spanned transformedText = + (Spanned) transformationMethod.getTransformation(text, sView); + + // only span1 is in the range of [0, 0). + final TestSpan[] spans0to0 = transformedText.getSpans(0, 0, TestSpan.class); + assertThat(spans0to0.length).isEqualTo(1); + assertThat(spans0to0[0]).isEqualTo(span1); + + // span1 and span2 are in the range of [3, 3). + final TestSpan[] spans3to3 = transformedText.getSpans(3, 3, TestSpan.class); + assertThat(spans3to3.length).isEqualTo(2); + assertThat(spans3to3[0]).isEqualTo(span1); + assertThat(spans3to3[1]).isEqualTo(span2); + + // only the span2 with collapsed range is in the range of [3, 4). + final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class); + assertThat(spans3to4.length).isEqualTo(1); + assertThat(spans3to4[0]).isEqualTo(span2); + + // span3 is in the range of [4, 5). (span2 is not mistakenly included.) + final TestSpan[] spans4to5 = transformedText.getSpans(4, 5, TestSpan.class); + assertThat(spans4to5.length).isEqualTo(1); + assertThat(spans4to5[0]).isEqualTo(span3); + + // only span3 is in the range of [4, 6). (span2 is not mistakenly included.) + final TestSpan[] spans4to6 = transformedText.getSpans(4, 6, TestSpan.class); + assertThat(spans4to6.length).isEqualTo(1); + assertThat(spans4to6[0]).isEqualTo(span3); + + // span3 is in the range of [4, 4). + final TestSpan[] spans4to4 = transformedText.getSpans(4, 4, TestSpan.class); + assertThat(spans4to4.length).isEqualTo(1); + assertThat(spans4to4[0]).isEqualTo(span3); + + // span3 is in the range of [5, 5). + final TestSpan[] spans5to5 = transformedText.getSpans(5, 5, TestSpan.class); + assertThat(spans5to5.length).isEqualTo(1); + assertThat(spans5to5[0]).isEqualTo(span3); + } + + @Test public void transformedText_getSpanStartAndEnd() { final SpannableString text = new SpannableString(TEXT); final TestSpan span1 = new TestSpan(); final TestSpan span2 = new TestSpan(); final TestSpan span3 = new TestSpan(); + final TestSpan span4 = new TestSpan(); + final TestSpan span5 = new TestSpan(); text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span4, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span5, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // In the transformedText, the new ranges of the spans are: // span1: [0, 3) // span2: [2, 6) // span3: [6, 7) + // span4: [3, 3) + // span5: [5, 6) final InsertModeTransformationMethod transformationMethod = new InsertModeTransformationMethod(3, false, null); final Spanned transformedText = @@ -345,6 +480,12 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getSpanStart(span3)).isEqualTo(6); assertThat(transformedText.getSpanEnd(span3)).isEqualTo(7); + + assertThat(transformedText.getSpanStart(span4)).isEqualTo(3); + assertThat(transformedText.getSpanEnd(span4)).isEqualTo(3); + + assertThat(transformedText.getSpanStart(span5)).isEqualTo(5); + assertThat(transformedText.getSpanEnd(span5)).isEqualTo(6); } @Test @@ -353,15 +494,21 @@ public class InsertModeTransformationMethodTest { final TestSpan span1 = new TestSpan(); final TestSpan span2 = new TestSpan(); final TestSpan span3 = new TestSpan(); + final TestSpan span4 = new TestSpan(); + final TestSpan span5 = new TestSpan(); text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span4, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + text.setSpan(span5, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // In the transformedText, the new ranges of the spans are: // span1: [0, 3) // span2: [2, 5) // span3: [5, 6) + // span4: [3. 3) + // span5: [4, 5) final InsertModeTransformationMethod transformationMethod = new InsertModeTransformationMethod(3, true, null); final Spanned transformedText = @@ -376,6 +523,12 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getSpanStart(span3)).isEqualTo(5); assertThat(transformedText.getSpanEnd(span3)).isEqualTo(6); + assertThat(transformedText.getSpanStart(span4)).isEqualTo(3); + assertThat(transformedText.getSpanEnd(span4)).isEqualTo(3); + + assertThat(transformedText.getSpanStart(span5)).isEqualTo(4); + assertThat(transformedText.getSpanEnd(span5)).isEqualTo(5); + final ReplacementSpan[] replacementSpans = transformedText.getSpans(0, 8, ReplacementSpan.class); assertThat(transformedText.getSpanStart(replacementSpans[0])).isEqualTo(3); diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 31c5a761ac1f..963014e0bb50 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -24,6 +24,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.app.ActivityOptions; import android.app.PendingIntent; @@ -33,6 +37,8 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Looper; @@ -58,6 +64,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; /** * Tests for RemoteViews. @@ -703,4 +710,61 @@ public class RemoteViewsTest { return null; } } + + @Test + public void visitUris() { + RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test); + + final Uri imageUri = Uri.parse("content://media/image"); + final Icon icon1 = Icon.createWithContentUri("content://media/icon1"); + final Icon icon2 = Icon.createWithContentUri("content://media/icon2"); + final Icon icon3 = Icon.createWithContentUri("content://media/icon3"); + final Icon icon4 = Icon.createWithContentUri("content://media/icon4"); + views.setImageViewUri(R.id.image, imageUri); + views.setTextViewCompoundDrawables(R.id.text, icon1, icon2, icon3, icon4); + + Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class); + views.visitUris(visitor); + verify(visitor, times(1)).accept(eq(imageUri)); + verify(visitor, times(1)).accept(eq(icon1.getUri())); + verify(visitor, times(1)).accept(eq(icon2.getUri())); + verify(visitor, times(1)).accept(eq(icon3.getUri())); + verify(visitor, times(1)).accept(eq(icon4.getUri())); + } + + @Test + public void visitUris_separateOrientation() { + final RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test); + final Uri imageUriL = Uri.parse("content://landscape/image"); + final Icon icon1L = Icon.createWithContentUri("content://landscape/icon1"); + final Icon icon2L = Icon.createWithContentUri("content://landscape/icon2"); + final Icon icon3L = Icon.createWithContentUri("content://landscape/icon3"); + final Icon icon4L = Icon.createWithContentUri("content://landscape/icon4"); + landscape.setImageViewUri(R.id.image, imageUriL); + landscape.setTextViewCompoundDrawables(R.id.text, icon1L, icon2L, icon3L, icon4L); + + final RemoteViews portrait = new RemoteViews(mPackage, 33); + final Uri imageUriP = Uri.parse("content://portrait/image"); + final Icon icon1P = Icon.createWithContentUri("content://portrait/icon1"); + final Icon icon2P = Icon.createWithContentUri("content://portrait/icon2"); + final Icon icon3P = Icon.createWithContentUri("content://portrait/icon3"); + final Icon icon4P = Icon.createWithContentUri("content://portrait/icon4"); + portrait.setImageViewUri(R.id.image, imageUriP); + portrait.setTextViewCompoundDrawables(R.id.text, icon1P, icon2P, icon3P, icon4P); + + RemoteViews views = new RemoteViews(landscape, portrait); + + Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class); + views.visitUris(visitor); + verify(visitor, times(1)).accept(eq(imageUriL)); + verify(visitor, times(1)).accept(eq(icon1L.getUri())); + verify(visitor, times(1)).accept(eq(icon2L.getUri())); + verify(visitor, times(1)).accept(eq(icon3L.getUri())); + verify(visitor, times(1)).accept(eq(icon4L.getUri())); + verify(visitor, times(1)).accept(eq(imageUriP)); + verify(visitor, times(1)).accept(eq(icon1P.getUri())); + verify(visitor, times(1)).accept(eq(icon2P.getUri())); + verify(visitor, times(1)).accept(eq(icon3P.getUri())); + verify(visitor, times(1)).accept(eq(icon4P.getUri())); + } } diff --git a/core/tests/coretests/src/com/android/internal/util/LatencyTrackerTest.java b/core/tests/coretests/src/com/android/internal/util/LatencyTrackerTest.java index 645324d57ea9..584ad205a55d 100644 --- a/core/tests/coretests/src/com/android/internal/util/LatencyTrackerTest.java +++ b/core/tests/coretests/src/com/android/internal/util/LatencyTrackerTest.java @@ -28,12 +28,12 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.provider.DeviceConfig; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; import com.android.internal.util.LatencyTracker.ActionProperties; import com.google.common.truth.Expect; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,7 +48,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -@SmallTest @RunWith(AndroidJUnit4.class) public class LatencyTrackerTest { private static final String ENUM_NAME_PREFIX = "UIACTION_LATENCY_REPORTED__ACTION__"; @@ -65,6 +64,11 @@ public class LatencyTrackerTest { mLatencyTracker = FakeLatencyTracker.create(); } + @After + public void tearDown() { + mLatencyTracker.stopListeningForLatencyTrackerConfigChanges(); + } + @Test public void testCujsMapToEnumsCorrectly() { List<Field> actions = getAllActionFields(); diff --git a/core/tests/coretests/testdoubles/src/com/android/internal/util/FakeLatencyTracker.java b/core/tests/coretests/testdoubles/src/com/android/internal/util/FakeLatencyTracker.java index 61e976bee35e..76e69bf35aaf 100644 --- a/core/tests/coretests/testdoubles/src/com/android/internal/util/FakeLatencyTracker.java +++ b/core/tests/coretests/testdoubles/src/com/android/internal/util/FakeLatencyTracker.java @@ -25,8 +25,6 @@ import android.provider.DeviceConfig; import android.util.Log; import android.util.SparseArray; -import androidx.annotation.Nullable; - import com.android.internal.annotations.GuardedBy; import com.google.common.collect.ImmutableMap; @@ -51,15 +49,17 @@ public final class FakeLatencyTracker extends LatencyTracker { private final List<String> mPerfettoTraceNamesTriggered; private final AtomicReference<SparseArray<ActionProperties>> mLastPropertiesUpdate = new AtomicReference<>(); - @Nullable - @GuardedBy("mLock") - private Callable<Boolean> mShouldClosePropertiesUpdatedCallable = null; + private final AtomicReference<Callable<Boolean>> mShouldClosePropertiesUpdatedCallable = + new AtomicReference<>(); private final ConditionVariable mDeviceConfigPropertiesUpdated = new ConditionVariable(); public static FakeLatencyTracker create() throws Exception { Log.i(TAG, "create"); disableForAllActions(); + Log.i(TAG, "done disabling all actions"); FakeLatencyTracker fakeLatencyTracker = new FakeLatencyTracker(); + Log.i(TAG, "done creating tracker object"); + fakeLatencyTracker.startListeningForLatencyTrackerConfigChanges(); // always return the fake in the disabled state and let the client control the desired state fakeLatencyTracker.waitForGlobalEnabledState(false); fakeLatencyTracker.waitForAllPropertiesEnableState(false); @@ -131,27 +131,25 @@ public final class FakeLatencyTracker extends LatencyTracker { @Override public void onDeviceConfigPropertiesUpdated(SparseArray<ActionProperties> actionProperties) { Log.d(TAG, "onDeviceConfigPropertiesUpdated: " + actionProperties); + mLastPropertiesUpdate.set(actionProperties); - synchronized (mLock) { - if (mShouldClosePropertiesUpdatedCallable != null) { - try { - boolean shouldClosePropertiesUpdated = - mShouldClosePropertiesUpdatedCallable.call(); - Log.i(TAG, "shouldClosePropertiesUpdatedCallable callable result=" - + shouldClosePropertiesUpdated); - if (shouldClosePropertiesUpdated) { - Log.i(TAG, "shouldClosePropertiesUpdatedCallable=true, opening condition"); - mShouldClosePropertiesUpdatedCallable = null; - mDeviceConfigPropertiesUpdated.open(); - } - } catch (Exception e) { - Log.e(TAG, "exception when calling callable", e); - throw new RuntimeException(e); + Callable<Boolean> shouldClosePropertiesUpdated = + mShouldClosePropertiesUpdatedCallable.get(); + if (shouldClosePropertiesUpdated != null) { + try { + boolean result = shouldClosePropertiesUpdated.call(); + Log.i(TAG, "shouldClosePropertiesUpdatedCallable callable result=" + result); + if (result) { + mShouldClosePropertiesUpdatedCallable.set(null); + mDeviceConfigPropertiesUpdated.open(); } - } else { - Log.i(TAG, "no conditional callable set, opening condition"); - mDeviceConfigPropertiesUpdated.open(); + } catch (Exception e) { + Log.e(TAG, "exception when calling callable", e); + throw new RuntimeException(e); } + } else { + Log.i(TAG, "no conditional callable set, opening condition"); + mDeviceConfigPropertiesUpdated.open(); } } @@ -175,107 +173,82 @@ public final class FakeLatencyTracker extends LatencyTracker { public void waitForAllPropertiesEnableState(boolean enabledState) throws Exception { Log.i(TAG, "waitForAllPropertiesEnableState: enabledState=" + enabledState); - synchronized (mLock) { - Log.i(TAG, "closing condition"); - mDeviceConfigPropertiesUpdated.close(); - // Update the callable to only close the properties updated condition when all the - // desired properties have been updated. The DeviceConfig callbacks may happen multiple - // times so testing the resulting updates is required. - mShouldClosePropertiesUpdatedCallable = () -> { - Log.i(TAG, "verifying if last properties update has all properties enable=" - + enabledState); - SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); - if (newProperties != null) { - for (int i = 0; i < newProperties.size(); i++) { - if (newProperties.get(i).isEnabled() != enabledState) { - return false; - } + // Update the callable to only close the properties updated condition when all the + // desired properties have been updated. The DeviceConfig callbacks may happen multiple + // times so testing the resulting updates is required. + waitForPropertiesCondition(() -> { + Log.i(TAG, "verifying if last properties update has all properties enable=" + + enabledState); + SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); + if (newProperties != null) { + for (int i = 0; i < newProperties.size(); i++) { + if (newProperties.get(i).isEnabled() != enabledState) { + return false; } } - return true; - }; - if (mShouldClosePropertiesUpdatedCallable.call()) { - return; } - } - Log.i(TAG, "waiting for condition"); - mDeviceConfigPropertiesUpdated.block(); + return true; + }); } public void waitForMatchingActionProperties(ActionProperties actionProperties) throws Exception { Log.i(TAG, "waitForMatchingActionProperties: actionProperties=" + actionProperties); - synchronized (mLock) { - Log.i(TAG, "closing condition"); - mDeviceConfigPropertiesUpdated.close(); - // Update the callable to only close the properties updated condition when all the - // desired properties have been updated. The DeviceConfig callbacks may happen multiple - // times so testing the resulting updates is required. - mShouldClosePropertiesUpdatedCallable = () -> { - Log.i(TAG, "verifying if last properties update contains matching property =" - + actionProperties); - SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); - if (newProperties != null) { - if (newProperties.size() > 0) { - return newProperties.get(actionProperties.getAction()).equals( - actionProperties); - } + // Update the callable to only close the properties updated condition when all the + // desired properties have been updated. The DeviceConfig callbacks may happen multiple + // times so testing the resulting updates is required. + waitForPropertiesCondition(() -> { + Log.i(TAG, "verifying if last properties update contains matching property =" + + actionProperties); + SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); + if (newProperties != null) { + if (newProperties.size() > 0) { + return newProperties.get(actionProperties.getAction()).equals( + actionProperties); } - return false; - }; - if (mShouldClosePropertiesUpdatedCallable.call()) { - return; } - } - Log.i(TAG, "waiting for condition"); - mDeviceConfigPropertiesUpdated.block(); + return false; + }); } public void waitForActionEnabledState(int action, boolean enabledState) throws Exception { Log.i(TAG, "waitForActionEnabledState:" + " action=" + action + ", enabledState=" + enabledState); - synchronized (mLock) { - Log.i(TAG, "closing condition"); - mDeviceConfigPropertiesUpdated.close(); - // Update the callable to only close the properties updated condition when all the - // desired properties have been updated. The DeviceConfig callbacks may happen multiple - // times so testing the resulting updates is required. - mShouldClosePropertiesUpdatedCallable = () -> { - Log.i(TAG, "verifying if last properties update contains action=" + action - + ", enabledState=" + enabledState); - SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); - if (newProperties != null) { - if (newProperties.size() > 0) { - return newProperties.get(action).isEnabled() == enabledState; - } + // Update the callable to only close the properties updated condition when all the + // desired properties have been updated. The DeviceConfig callbacks may happen multiple + // times so testing the resulting updates is required. + waitForPropertiesCondition(() -> { + Log.i(TAG, "verifying if last properties update contains action=" + action + + ", enabledState=" + enabledState); + SparseArray<ActionProperties> newProperties = mLastPropertiesUpdate.get(); + if (newProperties != null) { + if (newProperties.size() > 0) { + return newProperties.get(action).isEnabled() == enabledState; } - return false; - }; - if (mShouldClosePropertiesUpdatedCallable.call()) { - return; } - } - Log.i(TAG, "waiting for condition"); - mDeviceConfigPropertiesUpdated.block(); + return false; + }); } public void waitForGlobalEnabledState(boolean enabledState) throws Exception { Log.i(TAG, "waitForGlobalEnabledState: enabledState=" + enabledState); - synchronized (mLock) { - Log.i(TAG, "closing condition"); - mDeviceConfigPropertiesUpdated.close(); - // Update the callable to only close the properties updated condition when all the - // desired properties have been updated. The DeviceConfig callbacks may happen multiple - // times so testing the resulting updates is required. - mShouldClosePropertiesUpdatedCallable = () -> { - //noinspection deprecation - return isEnabled() == enabledState; - }; - if (mShouldClosePropertiesUpdatedCallable.call()) { - return; - } + // Update the callable to only close the properties updated condition when all the + // desired properties have been updated. The DeviceConfig callbacks may happen multiple + // times so testing the resulting updates is required. + waitForPropertiesCondition(() -> { + //noinspection deprecation + return isEnabled() == enabledState; + }); + } + + public void waitForPropertiesCondition(Callable<Boolean> shouldClosePropertiesUpdatedCallable) + throws Exception { + mShouldClosePropertiesUpdatedCallable.set(shouldClosePropertiesUpdatedCallable); + mDeviceConfigPropertiesUpdated.close(); + if (!shouldClosePropertiesUpdatedCallable.call()) { + Log.i(TAG, "waiting for mDeviceConfigPropertiesUpdated condition"); + mDeviceConfigPropertiesUpdated.block(); } - Log.i(TAG, "waiting for condition"); - mDeviceConfigPropertiesUpdated.block(); + Log.i(TAG, "waitForPropertiesCondition: returning"); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 19eff0e43169..1793a3d0feb4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -255,7 +255,7 @@ class ActivityEmbeddingAnimationSpec { private boolean shouldShowBackdrop(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change) { final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE, - mTransitionAnimation); + mTransitionAnimation, false); return a != null && a.getShowBackdrop(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index e8014af463bd..adc0c9c4322a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -479,7 +479,7 @@ public class BubbleExpandedView extends LinearLayout { void applyThemeAttrs() { final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ android.R.attr.dialogCornerRadius, - android.R.attr.colorBackgroundFloating}); + com.android.internal.R.attr.materialColorSurfaceBright}); boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( mContext.getResources()); mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 00cc57f0b99c..3ab175d3b68a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -23,6 +23,8 @@ import android.util.SparseArray import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.util.KtProtoLog import java.util.concurrent.Executor import java.util.function.Consumer @@ -140,6 +142,12 @@ class DesktopModeTaskRepository { val added = displayData.getOrCreate(displayId).activeTasks.add(taskId) if (added) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: add active task=%d displayId=%d", + taskId, + displayId + ) activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } } return added @@ -158,6 +166,9 @@ class DesktopModeTaskRepository { result = true } } + if (result) { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove active task=%d", taskId) + } return result } @@ -221,6 +232,17 @@ class DesktopModeTaskRepository { displayData[displayId]?.visibleTasks?.remove(taskId) } val newCount = getVisibleTaskCount(displayId) + + if (prevCount != newCount) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: update task visibility taskId=%d visible=%b displayId=%d", + taskId, + visible, + displayId + ) + } + // Check if count changed and if there was no tasks or this is the first task if (prevCount != newCount && (prevCount == 0 || newCount == 0)) { notifyVisibleTaskListeners(displayId, newCount > 0) @@ -244,6 +266,11 @@ class DesktopModeTaskRepository { * Add (or move if it already exists) the task to the top of the ordered list. */ fun addOrMoveFreeformTaskToTop(taskId: Int) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: add or move task to top taskId=%d", + taskId + ) if (freeformTasksInZOrder.contains(taskId)) { freeformTasksInZOrder.remove(taskId) } @@ -254,6 +281,11 @@ class DesktopModeTaskRepository { * Remove the task from the ordered list. */ fun removeFreeformTask(taskId: Int) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: remove freeform task from ordered list taskId=%d", + taskId + ) freeformTasksInZOrder.remove(taskId) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index b3109388da2c..91bb155d9d01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -39,7 +39,6 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.annotation.BinderThread -import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController @@ -56,6 +55,7 @@ import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.KtProtoLog import java.util.concurrent.Executor import java.util.function.Consumer @@ -91,7 +91,7 @@ class DesktopTasksController( } private fun onInit() { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") shellController.addExternalInterface( ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, { createExternalInterface() }, @@ -102,7 +102,7 @@ class DesktopTasksController( /** Show all tasks, that are part of the desktop, on top of launcher */ fun showDesktopApps(displayId: Int) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps") + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps") val wct = WindowContainerTransaction() // TODO(b/278084491): pass in display id bringDesktopAppsToFront(displayId, wct) @@ -130,8 +130,11 @@ class DesktopTasksController( /** Move a task to desktop */ fun moveToDesktop(task: RunningTaskInfo) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDesktop: %d", task.taskId) - + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToDesktop taskId=%d", + task.taskId + ) val wct = WindowContainerTransaction() // Bring other apps to front first bringDesktopAppsToFront(task.displayId, wct) @@ -147,10 +150,12 @@ class DesktopTasksController( * Moves a single task to freeform and sets the taskBounds to the passed in bounds, * startBounds */ - fun moveToFreeform( - taskInfo: RunningTaskInfo, - startBounds: Rect - ) { + fun moveToFreeform(taskInfo: RunningTaskInfo, startBounds: Rect) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToFreeform with bounds taskId=%d", + taskInfo.taskId + ) val wct = WindowContainerTransaction() moveHomeTaskToFront(wct) addMoveToDesktopChanges(wct, taskInfo.getToken()) @@ -165,10 +170,12 @@ class DesktopTasksController( } /** Brings apps to front and sets freeform task bounds */ - private fun moveToDesktopWithAnimation( - taskInfo: RunningTaskInfo, - freeformBounds: Rect - ) { + private fun moveToDesktopWithAnimation(taskInfo: RunningTaskInfo, freeformBounds: Rect) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToDesktop with animation taskId=%d", + taskInfo.taskId + ) val wct = WindowContainerTransaction() bringDesktopAppsToFront(taskInfo.displayId, wct) addMoveToDesktopChanges(wct, taskInfo.getToken()) @@ -190,7 +197,11 @@ class DesktopTasksController( /** Move a task to fullscreen */ fun moveToFullscreen(task: RunningTaskInfo) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId) + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToFullscreen taskId=%d", + task.taskId + ) val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task.token) @@ -206,6 +217,11 @@ class DesktopTasksController( * status bar area */ fun cancelMoveToFreeform(task: RunningTaskInfo, position: Point) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: cancelMoveToFreeform taskId=%d", + task.taskId + ) val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task.token) if (Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -218,6 +234,11 @@ class DesktopTasksController( } private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveToFullscreen with animation taskId=%d", + task.taskId + ) val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task.token) @@ -230,8 +251,14 @@ class DesktopTasksController( } } - /** Move a task to the front **/ + /** Move a task to the front */ fun moveTaskToFront(taskInfo: RunningTaskInfo) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: moveTaskToFront taskId=%d", + taskInfo.taskId + ) + val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) if (Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -255,10 +282,10 @@ class DesktopTasksController( fun moveToNextDisplay(taskId: Int) { val task = shellTaskOrganizer.getRunningTaskInfo(taskId) if (task == null) { - ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) + KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) return } - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d", + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d", taskId, task.displayId) val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() @@ -269,7 +296,7 @@ class DesktopTasksController( newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } } if (newDisplayId == null) { - ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") + KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") return } moveToDisplay(task, newDisplayId) @@ -281,17 +308,17 @@ class DesktopTasksController( * No-op if task is already on that display per [RunningTaskInfo.displayId]. */ private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d", + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d", task.taskId, displayId) if (task.displayId == displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") return } val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) if (displayAreaInfo == null) { - ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") + KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") return } @@ -316,7 +343,7 @@ class DesktopTasksController( } private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront") + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) // First move home to front and then other tasks on top of it @@ -397,9 +424,9 @@ class DesktopTasksController( if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) { // If there are any visible desktop tasks, switch the task to freeform if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { - ProtoLog.d( + KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController#handleRequest: switch fullscreen task to freeform," + + "DesktopTasksController: switch fullscreen task to freeform on transition" + " taskId=%d", task.taskId ) @@ -414,9 +441,9 @@ class DesktopTasksController( // If no visible desktop tasks, switch this task to freeform as the transition came // outside of this controller if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { - ProtoLog.d( + KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController#handleRequest: switch freeform task to fullscreen," + + "DesktopTasksController: switch freeform task to fullscreen oon transition" + " taskId=%d", task.taskId ) @@ -627,8 +654,6 @@ class DesktopTasksController( } } - - /** The interface for calls from outside the host process. */ @BinderThread private class IDesktopModeImpl(private var controller: DesktopTasksController?) : diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS index ccbb9cf298a2..a3803ed82844 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS @@ -1,3 +1,4 @@ # WM shell sub-module freeform owners atsjenk@google.com +jorgegil@google.com madym@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index 24d0b996a3cb..f51eb5299dd9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -180,6 +180,35 @@ public class PipBoundsAlgorithm { return null; } + + /** + * Returns the source hint rect if it is valid (if provided and is contained by the current + * task bounds, while not smaller than the destination bounds). + */ + @Nullable + public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds, + Rect destinationBounds) { + Rect sourceRectHint = getValidSourceHintRect(params, sourceBounds); + if (!isSourceRectHintValidForEnterPip(sourceRectHint, destinationBounds)) { + sourceRectHint = null; + } + return sourceRectHint; + } + + /** + * This is a situation in which the source rect hint on at least one axis is smaller + * than the destination bounds, which represents a problem because we would have to scale + * up that axis to fit the bounds. So instead, just fallback to the non-source hint + * animation in this case. + * + * @return {@code false} if the given source is too small to use for the entering animation. + */ + static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { + return sourceRectHint != null + && sourceRectHint.width() > destinationBounds.width() + && sourceRectHint.height() > destinationBounds.height(); + } + public float getDefaultAspectRatio() { return mDefaultAspectRatio; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 6cedcf534f3b..363d6759f8d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1657,8 +1657,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: Abort animation, invalid leash", TAG); return null; } - if (isInPipDirection(direction) - && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { + if (isInPipDirection(direction) && !PipBoundsAlgorithm + .isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { // The given source rect hint is too small for enter PiP animation, reset it to null. sourceHintRect = null; } @@ -1757,20 +1757,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * This is a situation in which the source rect hint on at least one axis is smaller - * than the destination bounds, which represents a problem because we would have to scale - * up that axis to fit the bounds. So instead, just fallback to the non-source hint - * animation in this case. - * - * @return {@code false} if the given source is too small to use for the entering animation. - */ - private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { - return sourceRectHint != null - && sourceRectHint.width() > destinationBounds.width() - && sourceRectHint.height() > destinationBounds.height(); - } - - /** * Sync with {@link SplitScreenController} on destination bounds if PiP is going to * split screen. * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 98db707d1105..046d6fce443b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -470,6 +470,7 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) { TransitionInfo.Change pipChange = pipTaskChange; + SurfaceControl activitySc = null; if (mCurrentPipTaskToken == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG); @@ -482,6 +483,7 @@ public class PipTransition extends PipTransitionController { if (mCurrentPipTaskToken.equals(change.getLastParent())) { // Find the activity that is exiting PiP. pipChange = change; + activitySc = change.getLeash(); break; } } @@ -498,17 +500,36 @@ public class PipTransition extends PipTransitionController { // case it may not be in the screen coordinate. // Reparent the pip leash to the root with max layer so that we can animate it outside of // parent crop, and make sure it is not covered by other windows. - final SurfaceControl pipLeash = pipChange.getLeash(); - final int rootIdx = TransitionUtil.rootIndexFor(pipChange, info); - startTransaction.reparent(pipLeash, info.getRoot(rootIdx).getLeash()); + final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info); + final SurfaceControl pipLeash; + if (activitySc != null) { + // Use a local leash to animate activity in case the activity has letterbox which may + // be broken by PiP animation, e.g. always end at 0,0 in parent and unable to include + // letterbox area in crop bounds. + final SurfaceControl activitySurface = pipChange.getLeash(); + pipLeash = new SurfaceControl.Builder() + .setName(activitySc + "_pip-leash") + .setContainerLayer() + .setHidden(false) + .setParent(root.getLeash()) + .build(); + startTransaction.reparent(activitySurface, pipLeash); + // Put the activity at local position with offset in case it is letterboxed. + final Point activityOffset = pipChange.getEndRelOffset(); + startTransaction.setPosition(activitySc, activityOffset.x, activityOffset.y); + } else { + pipLeash = pipChange.getLeash(); + startTransaction.reparent(pipLeash, root.getLeash()); + } startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); // Note: because of this, the bounds to animate should be translated to the root coordinate. - final Point offset = info.getRoot(rootIdx).getOffset(); + final Point offset = root.getOffset(); final Rect currentBounds = mPipBoundsState.getBounds(); currentBounds.offset(-offset.x, -offset.y); startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top); final WindowContainerToken pipTaskToken = pipChange.getContainer(); + final boolean useLocalLeash = activitySc != null; final boolean toFullscreen = pipChange.getEndAbsBounds().equals( mPipBoundsState.getDisplayBounds()); mFinishCallback = (wct, wctCB) -> { @@ -518,6 +539,14 @@ public class PipTransition extends PipTransitionController { wct.setBounds(pipTaskToken, null); mPipOrganizer.applyWindowingModeChangeOnExit(wct, TRANSITION_DIRECTION_LEAVE_PIP); } + if (useLocalLeash) { + if (mPipAnimationController.isAnimating()) { + mPipAnimationController.getCurrentAnimator().end(); + } + // Make sure the animator don't use the released leash, e.g. mergeAnimation. + mPipAnimationController.resetAnimatorState(); + finishTransaction.remove(pipLeash); + } finishCallback.onTransitionFinished(wct, wctCB); }; mFinishTransaction = finishTransaction; @@ -545,7 +574,8 @@ public class PipTransition extends PipTransitionController { // Set the initial frame as scaling the end to the start. final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds()); destinationBounds.offset(-offset.x, -offset.y); - startTransaction.setWindowCrop(pipLeash, destinationBounds); + startTransaction.setWindowCrop(pipLeash, destinationBounds.width(), + destinationBounds.height()); mSurfaceTransactionHelper.scale(startTransaction, pipLeash, destinationBounds, currentBounds); startTransaction.apply(); @@ -764,7 +794,7 @@ public class PipTransition extends PipTransitionController { final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); int rotationDelta = deltaRotation(startRotation, endRotation); Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( - taskInfo.pictureInPictureParams, currentBounds); + taskInfo.pictureInPictureParams, currentBounds, destinationBounds); if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) { // Need to get the bounds of new rotation in old rotation for fixed rotation, computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 94190c74f3e9..d27933e2f800 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -453,7 +453,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { return; } - finishTransaction.reparent(mTaskLeash, null).apply(); + finishTransaction.reparent(mTaskLeash, null); if (mListener != null) { final int taskId = mTaskInfo.taskId; @@ -490,13 +490,11 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { if (mSurfaceCreated) { // Surface is ready, so just reparent the task to this surface control startTransaction.reparent(mTaskLeash, mSurfaceControl) - .show(mTaskLeash) - .apply(); + .show(mTaskLeash); // Also reparent on finishTransaction since the finishTransaction will reparent back // to its "original" parent by default. finishTransaction.reparent(mTaskLeash, mSurfaceControl) - .setPosition(mTaskLeash, 0, 0) - .apply(); + .setPosition(mTaskLeash, 0, 0); mTaskViewTransitions.updateBoundsState(this, mTaskViewBase.getCurrentBoundsOnScreen()); mTaskViewTransitions.updateVisibilityState(this, true /* visible */); wct.setBounds(mTaskToken, mTaskViewBase.getCurrentBoundsOnScreen()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 689f9e1a3eda..fe2faaf79a1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -363,7 +363,8 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { continue; } startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); - finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); + finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) + .setPosition(chg.getLeash(), 0, 0); changesHandled++; } } @@ -377,7 +378,6 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } // No animation, just show it immediately. startTransaction.apply(); - finishTransaction.apply(); finishCallback.onTransitionFinished(wct, null /* wctCB */); startNextTransition(); return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 1ee52ae00645..21dca95d056a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -24,6 +24,7 @@ import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE; @@ -36,12 +37,8 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; @@ -92,7 +89,6 @@ import android.view.Choreographer; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; -import android.view.WindowManager.TransitionType; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Transformation; @@ -329,6 +325,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @ColorInt int backgroundColorForTransition = 0; final int wallpaperTransit = getWallpaperTransitType(info); boolean isDisplayRotationAnimationStarted = false; + final boolean isDreamTransition = isDreamTransition(info); + for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.hasAllFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY @@ -343,9 +341,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { continue; } final boolean isTask = change.getTaskInfo() != null; + final int mode = change.getMode(); boolean isSeamlessDisplayChange = false; - if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { + if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) { if (info.getType() == TRANSIT_CHANGE) { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; @@ -361,7 +360,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } - if (change.getMode() == TRANSIT_CHANGE) { + if (mode == TRANSIT_CHANGE) { // If task is child task, only set position in parent and update crop when needed. if (isTask && change.getParent() != null && info.getChange(change.getParent()).getTaskInfo() != null) { @@ -410,8 +409,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Hide the invisible surface directly without animating it if there is a display // rotation animation playing. - if (isDisplayRotationAnimationStarted && TransitionUtil.isClosingType( - change.getMode())) { + if (isDisplayRotationAnimationStarted && TransitionUtil.isClosingType(mode)) { startTransaction.hide(change.getLeash()); continue; } @@ -424,16 +422,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; - Animation a = loadAnimation(info, change, wallpaperTransit); + Animation a = loadAnimation(info, change, wallpaperTransit, isDreamTransition); if (a != null) { if (isTask) { - final @TransitionType int type = info.getType(); - final boolean isOpenOrCloseTransition = type == TRANSIT_OPEN - || type == TRANSIT_CLOSE - || type == TRANSIT_TO_FRONT - || type == TRANSIT_TO_BACK; final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0; - if (isOpenOrCloseTransition && !isTranslucent + if (!isTranslucent && TransitionUtil.isOpenOrCloseMode(mode) + && TransitionUtil.isOpenOrCloseMode(info.getType()) && wallpaperTransit == WALLPAPER_TRANSITION_NONE) { // Use the overview background as the background for the animation final Context uiContext = ActivityThread.currentActivityThread() @@ -458,7 +452,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition); if (!isTask && a.hasExtension()) { - if (!TransitionUtil.isOpeningType(change.getMode())) { + if (!TransitionUtil.isOpeningType(mode)) { // Can screenshot now (before startTransaction is applied) edgeExtendWindow(change, a, startTransaction, finishTransaction); } else { @@ -469,7 +463,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } - final Rect clipRect = TransitionUtil.isClosingType(change.getMode()) + final Rect clipRect = TransitionUtil.isClosingType(mode) ? new Rect(mRotator.getEndBoundsInStartRotation(change)) : new Rect(change.getEndAbsBounds()); clipRect.offsetTo(0, 0); @@ -519,6 +513,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + private static boolean isDreamTransition(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() != null + && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM) { + return true; + } + } + + return false; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -572,7 +578,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @Nullable private Animation loadAnimation(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change change, int wallpaperTransit) { + @NonNull TransitionInfo.Change change, int wallpaperTransit, + boolean isDreamTransition) { Animation a; final int type = info.getType(); @@ -630,7 +637,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // If there's a scene-transition, then jump-cut. return null; } else { - a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation); + a = loadAttributeAnimation( + info, change, wallpaperTransit, mTransitionAnimation, isDreamTransition); } if (a != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 0f4645c0fdab..19d8384ace41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -18,7 +18,6 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -63,7 +62,7 @@ public class TransitionAnimationHelper { @Nullable public static Animation loadAttributeAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, int wallpaperTransit, - @NonNull TransitionAnimation transitionAnimation) { + @NonNull TransitionAnimation transitionAnimation, boolean isDreamTransition) { final int type = info.getType(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); @@ -71,11 +70,9 @@ public class TransitionAnimationHelper { final boolean isTask = change.getTaskInfo() != null; final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); final int overrideType = options != null ? options.getType() : ANIM_NONE; - final boolean isDream = - isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; int animAttr = 0; boolean translucent = false; - if (isDream) { + if (isDreamTransition) { if (type == TRANSIT_OPEN) { animAttr = enter ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index fda943d7dc02..ab27c55f20dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -501,7 +501,9 @@ public class Transitions implements RemoteCallable<Transitions>, */ private static void setupAnimHierarchy(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { - boolean isOpening = isOpeningType(info.getType()); + final int type = info.getType(); + final boolean isOpening = isOpeningType(type); + final boolean isClosing = isClosingType(type); for (int i = 0; i < info.getRootCount(); ++i) { t.show(info.getRoot(i).getLeash()); } @@ -554,7 +556,13 @@ public class Transitions implements RemoteCallable<Transitions>, layer = zSplitLine + numChanges - i; } } else { // CHANGE or other - layer = zSplitLine + numChanges - i; + if (isClosing) { + // Put below CLOSE mode. + layer = zSplitLine - i; + } else { + // Put above CLOSE mode. + layer = zSplitLine + numChanges - i; + } } t.setLayer(leash, layer); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt new file mode 100644 index 000000000000..9b48a542720c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt @@ -0,0 +1,74 @@ +/* + * 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.wm.shell.util + +import android.util.Log +import com.android.internal.protolog.common.IProtoLogGroup +import com.android.wm.shell.protolog.ShellProtoLogImpl + +/** + * Log messages using an API similar to [com.android.internal.protolog.common.ProtoLog]. Useful for + * logging from Kotlin classes as ProtoLog does not have support for Kotlin. + * + * All messages are logged to logcat if logging is enabled for that [IProtoLogGroup]. + */ +// TODO(b/168581922): remove once ProtoLog adds support for Kotlin +class KtProtoLog { + companion object { + /** @see [com.android.internal.protolog.common.ProtoLog.d] */ + fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.d(group.tag, String.format(messageString, *args)) + } + } + + /** @see [com.android.internal.protolog.common.ProtoLog.v] */ + fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.v(group.tag, String.format(messageString, *args)) + } + } + + /** @see [com.android.internal.protolog.common.ProtoLog.i] */ + fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.i(group.tag, String.format(messageString, *args)) + } + } + + /** @see [com.android.internal.protolog.common.ProtoLog.w] */ + fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.w(group.tag, String.format(messageString, *args)) + } + } + + /** @see [com.android.internal.protolog.common.ProtoLog.e] */ + fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.e(group.tag, String.format(messageString, *args)) + } + } + + /** @see [com.android.internal.protolog.common.ProtoLog.wtf] */ + fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) { + if (ShellProtoLogImpl.isEnabled(group)) { + Log.wtf(group.tag, String.format(messageString, *args)) + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java index 7d05c0e62e13..402b0ce7c87c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -68,6 +68,12 @@ public class TransitionUtil { return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; } + /** Returns {@code true} if the transition is opening or closing mode. */ + public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) { + return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE + || mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK; + } + /** Returns {@code true} if the transition has a display change. */ public static boolean hasDisplayChange(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { @@ -311,7 +317,7 @@ public class TransitionUtil { private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change, SurfaceControl leash) { - return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), leash, false /* isTranslucent */, null /* clipRect */, null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 64dfc3ef845d..deebad545c5e 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -8,3 +8,4 @@ madym@google.com hwwang@google.com chenghsiuchang@google.com atsjenk@google.com +jorgegil@google.com diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java index 0289aa3b203d..3394dd05ef8c 100644 --- a/media/java/android/media/audiopolicy/AudioProductStrategy.java +++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java @@ -203,10 +203,16 @@ public final class AudioProductStrategy implements Parcelable { AudioProductStrategy thatStrategy = (AudioProductStrategy) o; - return mName == thatStrategy.mName && mId == thatStrategy.mId + return mId == thatStrategy.mId + && Objects.equals(mName, thatStrategy.mName) && Arrays.equals(mAudioAttributesGroups, thatStrategy.mAudioAttributesGroups); } + @Override + public int hashCode() { + return Objects.hash(mId, mName, Arrays.hashCode(mAudioAttributesGroups)); + } + /** * @param name of the product strategy * @param id of the product strategy @@ -460,6 +466,12 @@ public final class AudioProductStrategy implements Parcelable { && Arrays.equals(mAudioAttributes, thatAag.mAudioAttributes); } + @Override + public int hashCode() { + return Objects.hash(mVolumeGroupId, mLegacyStreamType, + Arrays.hashCode(mAudioAttributes)); + } + public int getStreamType() { return mLegacyStreamType; } diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java index b66545a6558e..266faae489dd 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioProductStrategyTest.java @@ -29,11 +29,14 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; +import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.testing.EqualsTester; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -231,4 +234,26 @@ public class AudioProductStrategyTest { } } } + + @Test + public void testEquals() { + final EqualsTester equalsTester = new EqualsTester(); + + AudioProductStrategy.getAudioProductStrategies().forEach( + strategy -> equalsTester.addEqualityGroup(strategy, + writeToAndFromParcel(strategy))); + + equalsTester.testEquals(); + } + + private static AudioProductStrategy writeToAndFromParcel( + AudioProductStrategy audioProductStrategy) { + Parcel parcel = Parcel.obtain(); + audioProductStrategy.writeToParcel(parcel, /*flags=*/0); + parcel.setDataPosition(0); + AudioProductStrategy unmarshalledAudioProductStrategy = + AudioProductStrategy.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return unmarshalledAudioProductStrategy; + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 0f5280efcdc0..78ee81921f25 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -49,6 +49,7 @@ class CredentialManagerRepo( private val context: Context, intent: Intent, userConfigRepo: UserConfigRepo, + isNewActivity: Boolean, ) { val requestInfo: RequestInfo? private val providerEnabledList: List<ProviderData> @@ -125,7 +126,8 @@ class CredentialManagerRepo( isPasskeyFirstUse = isPasskeyFirstUse, )!!, getCredentialUiState = null, - cancelRequestState = cancelUiRequestState + cancelRequestState = cancelUiRequestState, + isInitialRender = isNewActivity, ) } RequestInfo.TYPE_GET -> { @@ -140,7 +142,8 @@ class CredentialManagerRepo( if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE else ProviderActivityState.READY_TO_LAUNCH, isAutoSelectFlow = autoSelectEntry != null, - cancelRequestState = cancelUiRequestState + cancelRequestState = cancelUiRequestState, + isInitialRender = isNewActivity, ) } else -> { @@ -149,6 +152,7 @@ class CredentialManagerRepo( createCredentialUiState = null, getCredentialUiState = null, cancelRequestState = cancelUiRequestState, + isInitialRender = isNewActivity, ) } else { throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}") diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 54a8678df9cc..c409ba6aa78e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -62,7 +62,8 @@ class CredentialSelectorActivity : ComponentActivity() { return } val userConfigRepo = UserConfigRepo(this) - val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo) + val credManRepo = CredentialManagerRepo( + this, intent, userConfigRepo, isNewActivity = true) val backPressedCallback = object : OnBackPressedCallback( true // default to enabled @@ -103,7 +104,8 @@ class CredentialSelectorActivity : ComponentActivity() { } } else { val userConfigRepo = UserConfigRepo(this) - val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo) + val credManRepo = CredentialManagerRepo( + this, intent, userConfigRepo, isNewActivity = false) viewModel.onNewCredentialManagerRepo(credManRepo) } } catch (e: Exception) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 081490e50907..e96e536c00d5 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -53,6 +53,7 @@ data class UiState( // launched immediately, and canceling it will cancel the whole UI flow. val isAutoSelectFlow: Boolean = false, val cancelRequestState: CancelUiRequestState?, + val isInitialRender: Boolean, ) data class CancelUiRequestState( @@ -82,6 +83,10 @@ class CredentialSelectorViewModel( uiState = uiState.copy(dialogState = DialogState.COMPLETE) } + fun onInitialRenderComplete() { + uiState = uiState.copy(isInitialRender = false) + } + fun onCancellationUiRequested(appDisplayName: String?) { uiState = uiState.copy(cancelRequestState = CancelUiRequestState(appDisplayName)) } @@ -96,7 +101,7 @@ class CredentialSelectorViewModel( fun onNewCredentialManagerRepo(credManRepo: CredentialManagerRepo) { this.credManRepo = credManRepo - uiState = credManRepo.initState() + uiState = credManRepo.initState().copy(isInitialRender = false) if (this.credManRepo.requestInfo?.token != credManRepo.requestInfo?.token) { this.uiMetrics.resetInstanceId() @@ -114,7 +119,12 @@ class CredentialSelectorViewModel( uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) .setFillInIntent(entry.fillInIntent).build() - launcher.launch(intentSenderRequest) + try { + launcher.launch(intentSenderRequest) + } catch (e: Exception) { + Log.w(Constants.LOG_TAG, "Failed to launch provider UI: $e") + onInternalError() + } } else { Log.d(Constants.LOG_TAG, "No provider UI to launch") onInternalError() diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index 339968158a91..bd4375f4513e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -20,10 +20,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,9 +36,10 @@ import kotlinx.coroutines.launch @Composable fun ModalBottomSheet( sheetContent: @Composable ColumnScope.() -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, + isInitialRender: Boolean, + onInitialRenderComplete: () -> Unit, ) { - var isInitialRender by remember { mutableStateOf(true) } val scope = rememberCoroutineScope() val state = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, @@ -64,7 +61,7 @@ fun ModalBottomSheet( LaunchedEffect(state.currentValue) { if (state.currentValue == ModalBottomSheetValue.Hidden) { if (isInitialRender) { - isInitialRender = false + onInitialRenderComplete() scope.launch { state.show() } } else { onDismiss() diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index a258984fd70b..a378f1c16753 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -164,7 +164,9 @@ fun CreateCredentialScreen( } } }, - onDismiss = viewModel::onUserCancel + onDismiss = viewModel::onUserCancel, + isInitialRender = viewModel.uiState.isInitialRender, + onInitialRenderComplete = viewModel::onInitialRenderComplete, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 31af7aa78c69..40c99ee07d69 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -148,6 +148,8 @@ fun GetCredentialScreen( } }, onDismiss = viewModel::onUserCancel, + isInitialRender = viewModel.uiState.isInitialRender, + onInitialRenderComplete = viewModel::onInitialRenderComplete, ) } } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp index e9000a8e4b0f..df43863fdd84 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp @@ -27,6 +27,7 @@ android_library { "//apex_available:platform", "com.android.adservices", "com.android.cellbroadcast", + "com.android.devicelock", "com.android.extservices", "com.android.permission", "com.android.healthfitness", diff --git a/packages/SettingsLib/SettingsTheme/Android.bp b/packages/SettingsLib/SettingsTheme/Android.bp index d26ad1c77f1b..e6fb720ba27f 100644 --- a/packages/SettingsLib/SettingsTheme/Android.bp +++ b/packages/SettingsLib/SettingsTheme/Android.bp @@ -16,6 +16,7 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.cellbroadcast", + "com.android.devicelock", "com.android.extservices", "com.android.permission", "com.android.adservices", diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp index 0f9f7816d1dd..48cc75d41285 100644 --- a/packages/SettingsLib/SettingsTransition/Android.bp +++ b/packages/SettingsLib/SettingsTransition/Android.bp @@ -22,6 +22,7 @@ android_library { "//apex_available:platform", "com.android.adservices", "com.android.cellbroadcast", + "com.android.devicelock", "com.android.extservices", "com.android.permission", "com.android.healthfitness", diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml index 5396de0ed70b..17c139bb3ff0 100644 --- a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml +++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml @@ -18,6 +18,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.spaprivileged"> <uses-permission android:name="android.permission.MANAGE_USERS" /> - <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" /> </manifest> diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt index 23270c147d85..eb3d7dc84dba 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt @@ -55,6 +55,7 @@ class AppOpsControllerTest { @Before fun setUp() { whenever(context.appOpsManager).thenReturn(appOpsManager) + whenever(context.packageManager).thenReturn(packageManager) doNothing().`when`(packageManager) .updatePermissionFlags(anyString(), anyString(), anyInt(), anyInt(), any()) } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index af59a5547db1..c54f4f87681d 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -19,6 +19,7 @@ package com.android.settingslib.spaprivileged.template.app import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import androidx.compose.runtime.State import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.MutableLiveData @@ -38,6 +39,10 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doNothing import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.Spy @@ -57,11 +62,16 @@ class AppOpPermissionAppListTest { @Mock private lateinit var appOpsManager: AppOpsManager + @Mock private lateinit var packageManager: PackageManager + private lateinit var listModel: TestAppOpPermissionAppListModel @Before fun setUp() { whenever(context.appOpsManager).thenReturn(appOpsManager) + whenever(context.packageManager).thenReturn(packageManager) + doNothing().`when`(packageManager) + .updatePermissionFlags(anyString(), anyString(), anyInt(), anyInt(), any()) listModel = TestAppOpPermissionAppListModel() } diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index eca116586fbe..5d09a1a926d8 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -23,6 +23,7 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.cellbroadcast", + "com.android.devicelock", "com.android.healthfitness", ], } diff --git a/packages/SettingsLib/Utils/Android.bp b/packages/SettingsLib/Utils/Android.bp index 33ba64ac2cb7..dc2b52d24462 100644 --- a/packages/SettingsLib/Utils/Android.bp +++ b/packages/SettingsLib/Utils/Android.bp @@ -25,6 +25,7 @@ android_library { "//apex_available:platform", "com.android.adservices", "com.android.cellbroadcast", + "com.android.devicelock", "com.android.extservices", "com.android.healthfitness", "com.android.permission", diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index e9aded0838d9..2372c802168c 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -67,6 +67,8 @@ <!-- SignalDrawable --> <dimen name="signal_icon_size">15dp</dimen> + <!-- The size of the roaming icon in the top-left corner of the signal icon. --> + <dimen name="signal_icon_size_roaming">8dp</dimen> <!-- Size of nearby icon --> <dimen name="bt_nearby_icon_size">24dp</dimen> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 214c9035fc8e..4e7579277aee 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -244,7 +244,7 @@ <!-- Bluetooth settings. The user-visible string that is used whenever referring to the Hearing Aid profile. --> <string name="bluetooth_profile_hearing_aid">Hearing Aids</string> <!-- Bluetooth settings. The user-visible string that is used whenever referring to the LE audio profile. --> - <string name="bluetooth_profile_le_audio">LE audio</string> + <string name="bluetooth_profile_le_audio">LE audio (experimental)</string> <!-- Bluetooth settings. Connection options screen. The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. --> <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string> <!-- Bluetooth settings. Connection options screen. The summary for the LE audio checkbox preference when LE audio is connected. --> diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java index ac9cdacec598..e034254e64ec 100644 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java @@ -95,9 +95,6 @@ public class QrDecorateView extends View { protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (!isLaidOut()) { - return; - } if (mMaskBitmap == null) { mMaskBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); mMaskCanvas = new Canvas(mMaskBitmap); diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java index adaf4a1d3ab5..c45d77471932 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java @@ -343,7 +343,12 @@ public class WifiStatusTracker { } @Nullable - private WifiInfo getMainOrUnderlyingWifiInfo(NetworkCapabilities networkCapabilities) { + private WifiInfo getMainOrUnderlyingWifiInfo( + @Nullable NetworkCapabilities networkCapabilities) { + if (networkCapabilities == null) { + return null; + } + WifiInfo mainWifiInfo = getMainWifiInfo(networkCapabilities); if (mainWifiInfo != null) { return mainWifiInfo; @@ -376,7 +381,10 @@ public class WifiStatusTracker { } @Nullable - private WifiInfo getMainWifiInfo(NetworkCapabilities networkCapabilities) { + private WifiInfo getMainWifiInfo(@Nullable NetworkCapabilities networkCapabilities) { + if (networkCapabilities == null) { + return null; + } boolean canHaveWifiInfo = networkCapabilities.hasTransport(TRANSPORT_WIFI) || networkCapabilities.hasTransport(TRANSPORT_CELLULAR); if (!canHaveWifiInfo) { @@ -402,7 +410,11 @@ public class WifiStatusTracker { getMainOrUnderlyingWifiInfo(networkCapabilities)); } - private boolean connectionIsWifi(NetworkCapabilities networkCapabilities, WifiInfo wifiInfo) { + private boolean connectionIsWifi( + @Nullable NetworkCapabilities networkCapabilities, WifiInfo wifiInfo) { + if (networkCapabilities == null) { + return false; + } return wifiInfo != null || networkCapabilities.hasTransport(TRANSPORT_WIFI); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java index 6e975cf9d8f3..5a9a9d154070 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiStatusTrackerTest.java @@ -305,4 +305,16 @@ public class WifiStatusTrackerTest { assertThat(mWifiStatusTracker.isDefaultNetwork).isTrue(); } + + /** Regression test for b/280169520. */ + @Test + public void networkCallbackNullCapabilities_noCrash() { + Network primaryNetwork = Mockito.mock(Network.class); + + // WHEN the network capabilities are null + mNetworkCallbackCaptor.getValue().onCapabilitiesChanged( + primaryNetwork, /* networkCapabilities= */ null); + + // THEN there's no crash (no assert needed) + } } diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml index 29832a081612..c85449d0c570 100644 --- a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml @@ -78,16 +78,10 @@ android:id="@+id/mobile_roaming" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="top|start" android:src="@drawable/stat_sys_roaming" android:contentDescription="@string/data_connection_roaming" android:visibility="gone" /> </FrameLayout> - <ImageView - android:id="@+id/mobile_roaming_large" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming_large" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> </com.android.keyguard.AlphaOptimizedLinearLayout> </merge> diff --git a/packages/SystemUI/res/drawable/stat_sys_roaming.xml b/packages/SystemUI/res/drawable/stat_sys_roaming.xml index 0dd9f5a39f91..2dd12ca4e21a 100644 --- a/packages/SystemUI/res/drawable/stat_sys_roaming.xml +++ b/packages/SystemUI/res/drawable/stat_sys_roaming.xml @@ -14,10 +14,10 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/signal_icon_size" - android:height="@dimen/signal_icon_size" - android:viewportWidth="17" - android:viewportHeight="17"> + android:width="@dimen/signal_icon_size_roaming" + android:height="@dimen/signal_icon_size_roaming" + android:viewportWidth="8" + android:viewportHeight="8"> <path android:fillColor="#FFFFFFFF" diff --git a/packages/SystemUI/res/layout/mobile_signal_group.xml b/packages/SystemUI/res/layout/mobile_signal_group.xml index 5552020f22cb..bfd079b59054 100644 --- a/packages/SystemUI/res/layout/mobile_signal_group.xml +++ b/packages/SystemUI/res/layout/mobile_signal_group.xml @@ -77,11 +77,4 @@ android:contentDescription="@string/data_connection_roaming" android:visibility="gone" /> </FrameLayout> - <ImageView - android:id="@+id/mobile_roaming_large" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming_large" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> </com.android.keyguard.AlphaOptimizedLinearLayout> diff --git a/packages/SystemUI/res/layout/udfps_keyguard_preview.xml b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml new file mode 100644 index 000000000000..c068b7bc46a9 --- /dev/null +++ b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/accessibility_fingerprint_label" + android:background="@drawable/fingerprint_bg"> + + <!-- LockScreen fingerprint icon from 0 stroke width to full width --> + <com.airbnb.lottie.LottieAnimationView + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="centerCrop" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHeight_percent="0.5" + app:layout_constraintWidth_percent="0.5" + app:lottie_autoPlay="false" + app:lottie_loop="false" + app:lottie_progress="1.0" + app:lottie_colorFilter="?android:attr/textColorPrimary" + app:lottie_rawRes="@raw/udfps_lockscreen_fp" /> +</androidx.constraintlayout.widget.ConstraintLayout >
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index d8bf570954df..676f342775ef 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -179,6 +179,20 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } /** + * Set alpha directly to mView will clip clock, so we set alpha to clock face instead + */ + public void setAlpha(float alpha) { + ClockController clock = getClock(); + if (clock != null) { + clock.getLargeClock().getView().setAlpha(alpha); + clock.getSmallClock().getView().setAlpha(alpha); + } + if (mStatusArea != null) { + mStatusArea.setAlpha(alpha); + } + } + + /** * Attach the controller to the view it relates to. */ @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 09820305f34e..58807e4bf3bd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_APPEAR; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_DISAPPEAR; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; @@ -184,6 +185,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { } mAppearAnimator.setDuration(ANIMATION_DURATION); mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction())); + mAppearAnimator.addListener(getAnimationListener(CUJ_LOCKSCREEN_PIN_APPEAR)); mAppearAnimator.start(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index d8e1eb0f0860..23136097f41f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -135,4 +135,31 @@ public class KeyguardStatusView extends GridLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Trace.endSection(); } + + /** + * Clock content will be clipped when goes beyond bounds, + * so we setAlpha for all views except clock + */ + public void setAlpha(float alpha, boolean excludeClock) { + if (!excludeClock) { + setAlpha(alpha); + return; + } + if (alpha == 1 || alpha == 0) { + setAlpha(alpha); + } + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child == mStatusViewContainer) { + for (int j = 0; j < mStatusViewContainer.getChildCount(); j++) { + View innerChild = mStatusViewContainer.getChildAt(j); + if (innerChild != mClockView) { + innerChild.setAlpha(alpha); + } + } + } else { + child.setAlpha(alpha); + } + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 794eeda86b0f..af474661a67d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -180,7 +180,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV */ public void setAlpha(float alpha) { if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) { - mView.setAlpha(alpha); + mView.setAlpha(alpha, true); + mKeyguardClockSwitchController.setAlpha(alpha); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt index b72801d3b5fe..8edccf166efe 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt @@ -21,7 +21,7 @@ constructor( fun enable(onPanelInteraction: Runnable) { if (action == null) { action = Action(onPanelInteraction) - shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + shadeExpansionStateManager.addShadeExpansionListener(this::onPanelExpansionChanged) } else { Log.e(TAG, "Already enabled") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index b5ddc2ea91b7..9e208fd60fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -16,6 +16,7 @@ package com.android.systemui.keyguard; +import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; @@ -101,6 +102,7 @@ import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; +import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.policy.IKeyguardExitCallback; import com.android.internal.policy.IKeyguardStateCallback; @@ -131,6 +133,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.dagger.KeyguardModule; +import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; @@ -1181,12 +1184,16 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private Lazy<ScrimController> mScrimControllerLazy; private FeatureFlags mFeatureFlags; + private final UiEventLogger mUiEventLogger; + private final SessionTracker mSessionTracker; /** * Injected constructor. See {@link KeyguardModule}. */ public KeyguardViewMediator( Context context, + UiEventLogger uiEventLogger, + SessionTracker sessionTracker, UserTracker userTracker, FalsingCollector falsingCollector, LockPatternUtils lockPatternUtils, @@ -1270,6 +1277,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS; mFeatureFlags = featureFlags; + mUiEventLogger = uiEventLogger; + mSessionTracker = sessionTracker; } public void userActivity() { @@ -1660,6 +1669,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence); notifyStartedWakingUp(); } + mUiEventLogger.logWithInstanceIdAndPosition( + BiometricUnlockController.BiometricUiEvent.STARTED_WAKING_UP, + 0, + null, + mSessionTracker.getSessionId(SESSION_KEYGUARD), + pmWakeReason + ); mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason); maybeSendUserPresentBroadcast(); Trace.endSection(); @@ -2941,9 +2957,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (mSurfaceBehindRemoteAnimationFinishedCallback != null) { try { mSurfaceBehindRemoteAnimationFinishedCallback.onAnimationFinished(); + } catch (Throwable t) { + // The surface may no longer be available. Just capture the exception + Log.w(TAG, "Surface behind remote animation callback failed, and it's probably ok: " + + t.getMessage()); + } finally { mSurfaceBehindRemoteAnimationFinishedCallback = null; - } catch (RemoteException e) { - e.printStackTrace(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 5e71458c8bb4..deb8f5d96cbd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -21,6 +21,7 @@ import android.content.Context; import android.os.PowerManager; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardDisplayManager; import com.android.keyguard.KeyguardUpdateMonitor; @@ -50,6 +51,7 @@ import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionMo import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger; import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl; +import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; @@ -63,12 +65,12 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; -import java.util.concurrent.Executor; - import dagger.Lazy; import dagger.Module; import dagger.Provides; +import java.util.concurrent.Executor; + /** * Dagger Module providing keyguard. */ @@ -93,6 +95,8 @@ public class KeyguardModule { @SysUISingleton public static KeyguardViewMediator newKeyguardViewMediator( Context context, + UiEventLogger uiEventLogger, + SessionTracker sessionTracker, UserTracker userTracker, FalsingCollector falsingCollector, LockPatternUtils lockPatternUtils, @@ -124,6 +128,8 @@ public class KeyguardModule { FeatureFlags featureFlags) { return new KeyguardViewMediator( context, + uiEventLogger, + sessionTracker, userTracker, falsingCollector, lockPatternUtils, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index ad11360160c1..3aa57dde3178 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -273,7 +273,7 @@ constructor( val finger = LayoutInflater.from(context) .inflate( - R.layout.udfps_keyguard_view_internal, + R.layout.udfps_keyguard_preview, parentView, false, ) as View diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index 2312c705cd6e..4bc7ec844794 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -343,22 +343,24 @@ public class LongScreenshotActivity extends Activity { } else { String editorPackage = getString(R.string.config_screenshotEditor); Intent intent = new Intent(Intent.ACTION_EDIT); - if (!TextUtils.isEmpty(editorPackage)) { - intent.setComponent(ComponentName.unflattenFromString(editorPackage)); - } intent.setDataAndType(uri, "image/png"); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + Bundle options = null; - mTransitionView.setImageBitmap(mOutputBitmap); - mTransitionView.setVisibility(View.VISIBLE); - mTransitionView.setTransitionName( - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); - // TODO: listen for transition completing instead of finishing onStop - mTransitionStarted = true; - startActivity(intent, - ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView, - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle()); + // Skip shared element transition for implicit edit intents + if (!TextUtils.isEmpty(editorPackage)) { + intent.setComponent(ComponentName.unflattenFromString(editorPackage)); + mTransitionView.setImageBitmap(mOutputBitmap); + mTransitionView.setVisibility(View.VISIBLE); + mTransitionView.setTransitionName( + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); + options = ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView, + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle(); + // TODO: listen for transition completing instead of finishing onStop + mTransitionStarted = true; + } + startActivity(intent, options); } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 20313c3df465..a048f543d476 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -54,12 +54,20 @@ class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents { * Listener will also be immediately notified with the current values. */ fun addExpansionListener(listener: ShadeExpansionListener) { - expansionListeners.add(listener) + addShadeExpansionListener(listener) listener.onPanelExpansionChanged( ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) ) } + /** + * Adds a listener that will be notified when the panel expansion fraction has changed. + * @see #addExpansionListener + */ + fun addShadeExpansionListener(listener: ShadeExpansionListener) { + expansionListeners.add(listener) + } + /** Removes an expansion listener. */ fun removeExpansionListener(listener: ShadeExpansionListener) { expansionListeners.remove(listener) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java index 12f2c22ab86a..f84b96c94a91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiSignalController.java @@ -15,13 +15,14 @@ */ package com.android.systemui.statusbar.connectivity; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN; import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT; import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT; import android.content.Context; import android.content.Intent; -import android.net.NetworkCapabilities; import android.net.wifi.WifiManager; import android.os.Handler; import android.text.Html; @@ -37,6 +38,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import java.io.PrintWriter; +import java.util.BitSet; /** */ public class WifiSignalController extends SignalController<WifiState, IconGroup> { @@ -56,8 +58,12 @@ public class WifiSignalController extends SignalController<WifiState, IconGroup> WifiManager wifiManager, WifiStatusTrackerFactory trackerFactory, @Background Handler bgHandler) { - super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, - callbackHandler, networkController); + super( + "WifiSignalController", + context, + TRANSPORT_WIFI, + callbackHandler, + networkController); mBgHandler = bgHandler; mWifiManager = wifiManager; mWifiTracker = trackerFactory.createTracker(this::handleStatusUpdated, bgHandler); @@ -160,7 +166,10 @@ public class WifiSignalController extends SignalController<WifiState, IconGroup> // The WiFi signal level returned by WifiManager#calculateSignalLevel start from 0, so // WifiManager#getMaxSignalLevel + 1 represents the total level buckets count. int totalLevel = mWifiManager.getMaxSignalLevel() + 1; - boolean noInternet = mCurrentState.inetCondition == 0; + // A carrier merged connection could come from a WIFI *or* CELLULAR transport, so we can't + // use [mCurrentState.inetCondition], which only checks the WIFI status. Instead, check if + // the default connection is validated at all. + boolean noInternet = !mCurrentState.isDefaultConnectionValidated; if (mCurrentState.connected) { return SignalDrawable.getState(level, totalLevel, noInternet); } else if (mCurrentState.enabled) { @@ -236,6 +245,18 @@ public class WifiSignalController extends SignalController<WifiState, IconGroup> && mCurrentState.isCarrierMerged && (mCurrentState.subId == subId); } + @Override + void updateConnectivity(BitSet connectedTransports, BitSet validatedTransports) { + mCurrentState.inetCondition = validatedTransports.get(mTransportType) ? 1 : 0; + // Because a carrier merged connection can come from either a CELLULAR *or* WIFI transport, + // we need to also store if either transport is validated to correctly display the carrier + // merged case. + mCurrentState.isDefaultConnectionValidated = + validatedTransports.get(TRANSPORT_CELLULAR) + || validatedTransports.get(TRANSPORT_WIFI); + notifyListenersIfNecessary(); + } + @VisibleForTesting void setActivity(int wifiActivity) { mCurrentState.activityIn = wifiActivity == DATA_ACTIVITY_INOUT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt index d32e34915c61..63a63de83d71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt @@ -24,6 +24,14 @@ internal class WifiState( @JvmField var isDefault: Boolean = false, @JvmField var statusLabel: String? = null, @JvmField var isCarrierMerged: Boolean = false, + /** + * True if the current default connection is validated for *any* transport, not just wifi. + * (Specifically TRANSPORT_CELLULAR *or* TRANSPORT_WIFI.) + * + * This should *only* be used when calculating information for the carrier merged connection and + * *not* for typical wifi connections. See b/225902574. + */ + @JvmField var isDefaultConnectionValidated: Boolean = false, @JvmField var subId: Int = 0 ) : ConnectivityState() { @@ -35,6 +43,7 @@ internal class WifiState( isDefault = state.isDefault statusLabel = state.statusLabel isCarrierMerged = state.isCarrierMerged + isDefaultConnectionValidated = state.isDefaultConnectionValidated subId = state.subId } @@ -45,6 +54,7 @@ internal class WifiState( .append(",isDefault=").append(isDefault) .append(",statusLabel=").append(statusLabel) .append(",isCarrierMerged=").append(isCarrierMerged) + .append(",isDefaultConnectionValidated=").append(isDefaultConnectionValidated) .append(",subId=").append(subId) } @@ -54,6 +64,7 @@ internal class WifiState( "isDefault", "statusLabel", "isCarrierMerged", + "isDefaultConnectionValidated", "subId") return super.tableColumns() + columns @@ -65,6 +76,7 @@ internal class WifiState( isDefault, statusLabel, isCarrierMerged, + isDefaultConnectionValidated, subId).map { it.toString() } @@ -84,6 +96,7 @@ internal class WifiState( if (isDefault != other.isDefault) return false if (statusLabel != other.statusLabel) return false if (isCarrierMerged != other.isCarrierMerged) return false + if (isDefaultConnectionValidated != other.isDefaultConnectionValidated) return false if (subId != other.subId) return false return true @@ -96,6 +109,7 @@ internal class WifiState( result = 31 * result + isDefault.hashCode() result = 31 * result + (statusLabel?.hashCode() ?: 0) result = 31 * result + isCarrierMerged.hashCode() + result = 31 * result + isDefaultConnectionValidated.hashCode() result = 31 * result + subId return result } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt index 92a8356b7f07..1aeb6b304ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt @@ -22,7 +22,6 @@ import android.os.Looper import android.view.Choreographer import android.view.InputEvent import android.view.MotionEvent -import com.android.systemui.settings.DisplayTracker import com.android.systemui.shared.system.InputChannelCompat import com.android.systemui.shared.system.InputMonitorCompat @@ -39,7 +38,7 @@ import com.android.systemui.shared.system.InputMonitorCompat */ abstract class GenericGestureDetector( private val tag: String, - private val displayTracker: DisplayTracker + private val displayId: Int, ) { /** * Active callbacks, each associated with a tag. Gestures will only be monitored if @@ -87,7 +86,7 @@ abstract class GenericGestureDetector( internal open fun startGestureListening() { stopGestureListening() - inputMonitor = InputMonitorCompat(tag, displayTracker.defaultDisplayId).also { + inputMonitor = InputMonitorCompat(tag, displayId).also { inputReceiver = it.getInputReceiver( Looper.getMainLooper(), Choreographer.getInstance(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt index 6d60f4a9affa..2fd0a5324d15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt @@ -36,7 +36,10 @@ abstract class SwipeUpGestureHandler( displayTracker: DisplayTracker, private val logger: SwipeUpGestureLogger, private val loggerTag: String, -) : GenericGestureDetector(SwipeUpGestureHandler::class.simpleName!!, displayTracker) { +) : GenericGestureDetector( + SwipeUpGestureHandler::class.simpleName!!, + displayTracker.defaultDisplayId +) { private var startY: Float = 0f private var startTime: Long = 0L diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt index a901d5979576..ed30f2fc2e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt @@ -32,7 +32,10 @@ import javax.inject.Inject class TapGestureDetector @Inject constructor( private val context: Context, displayTracker: DisplayTracker -) : GenericGestureDetector(TapGestureDetector::class.simpleName!!, displayTracker) { +) : GenericGestureDetector( + TapGestureDetector::class.simpleName!!, + displayTracker.defaultDisplayId +) { private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 6742e4f3041e..bd7840d447cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -190,7 +190,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } } - @VisibleForTesting public enum BiometricUiEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "A biometric event of type fingerprint succeeded.") @@ -221,7 +220,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp BIOMETRIC_IRIS_ERROR(404), @UiEvent(doc = "Bouncer was shown as a result of consecutive failed UDFPS attempts.") - BIOMETRIC_BOUNCER_SHOWN(916); + BIOMETRIC_BOUNCER_SHOWN(916), + + @UiEvent(doc = "Screen started waking up with the given PowerManager wake reason.") + STARTED_WAKING_UP(1378); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index 560ea8aae594..313410ac45df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -604,6 +604,7 @@ public class NotificationIconAreaController implements } updateAodIconsVisibility(animate, false /* force */); updateAodNotificationIcons(); + updateAodIconColors(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 7aa90336e2bf..49de5a232f30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -160,7 +160,7 @@ abstract class StatusBarPipelineModule { @SysUISingleton @SharedConnectivityInputLog fun provideSharedConnectivityTableLogBuffer(factory: LogBufferFactory): LogBuffer { - return factory.create("SharedConnectivityInputLog", 30) + return factory.create("SharedConnectivityInputLog", 60) } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 0e9b6c56437e..81a068d10a14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -38,6 +38,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -88,6 +89,7 @@ constructor( private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, + airplaneModeRepository: AirplaneModeRepository, // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi // repository is an input to the mobile repository. // See [CarrierMergedConnectionRepository] for details. @@ -106,10 +108,20 @@ constructor( context.getString(R.string.status_bar_network_name_separator) private val carrierMergedSubId: StateFlow<Int?> = - wifiRepository.wifiNetwork - .mapLatest { - if (it is WifiNetworkModel.CarrierMerged) { - it.subscriptionId + combine( + wifiRepository.wifiNetwork, + connectivityRepository.defaultConnections, + airplaneModeRepository.isAirplaneMode, + ) { wifiNetwork, defaultConnections, isAirplaneMode -> + // The carrier merged connection should only be used if it's also the default + // connection or mobile connections aren't available because of airplane mode. + val defaultConnectionIsNonMobile = + defaultConnections.carrierMerged.isDefault || + defaultConnections.wifi.isDefault || + isAirplaneMode + + if (wifiNetwork is WifiNetworkModel.CarrierMerged && defaultConnectionIsNonMobile) { + wifiNetwork.subscriptionId } else { null } @@ -269,12 +281,8 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val hasCarrierMergedConnection: StateFlow<Boolean> = - combine( - connectivityRepository.defaultConnections, - carrierMergedSubId, - ) { defaultConnections, carrierMergedSubId -> - defaultConnections.carrierMerged.isDefault || carrierMergedSubId != null - } + carrierMergedSubId + .map { it != null } .distinctUntilChanged() .logDiffsForTable( tableLogger, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index eec91a0bca82..e90f40c74cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -155,7 +155,8 @@ constructor( combine( unfilteredSubscriptions, mobileConnectionsRepo.activeMobileDataSubscriptionId, - ) { unfilteredSubs, activeId -> + connectivityRepository.vcnSubId, + ) { unfilteredSubs, activeId, vcnSubId -> // Based on the old logic, if (unfilteredSubs.size != 2) { return@combine unfilteredSubs @@ -182,7 +183,13 @@ constructor( // return the non-opportunistic info return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1) } else { - return@combine if (info1.subscriptionId == activeId) { + // It's possible for the subId of the VCN to disagree with the active subId in + // cases where the system has tried to switch but found no connection. In these + // scenarios, VCN will always have the subId that we want to use, so use that + // value instead of the activeId reported by telephony + val subIdToKeep = vcnSubId ?: activeId + + return@combine if (info1.subscriptionId == subIdToKeep) { listOf(info1) } else { listOf(info2) @@ -259,7 +266,7 @@ constructor( */ override val isDefaultConnectionFailed: StateFlow<Boolean> = combine( - mobileConnectionsRepo.mobileIsDefault, + mobileIsDefault, mobileConnectionsRepo.defaultConnectionIsValidated, forcingCellularValidation, ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt index 051f43f1059c..cac0ae3dbab4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt @@ -61,6 +61,10 @@ constructor( model::messagePrinter, ) } + + fun logVcnSubscriptionId(subId: Int) { + buffer.log(TAG, LogLevel.DEBUG, { int1 = subId }, { "vcnSubId changed: $int1" }) + } } private const val TAG = "ConnectivityInputLogger" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt index 731f1e028470..7076f345df97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt @@ -27,6 +27,7 @@ import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.annotation.ArrayRes import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable @@ -50,10 +51,13 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn /** @@ -66,6 +70,16 @@ interface ConnectivityRepository { /** Observable for which connection(s) are currently default. */ val defaultConnections: StateFlow<DefaultConnectionModel> + + /** + * Subscription ID of the [VcnTransportInfo] for the default connection. + * + * If the default network has a [VcnTransportInfo], then that transport info contains a subId of + * the VCN. When VCN is connected and default, this subId is what SystemUI will care about. In + * cases where telephony's activeDataSubscriptionId differs from this value, it is expected to + * eventually catch up and reflect what is represented here in the VcnTransportInfo. + */ + val vcnSubId: StateFlow<Int?> } @SuppressLint("MissingPermission") @@ -118,24 +132,13 @@ constructor( initialValue = defaultHiddenIcons ) - @SuppressLint("MissingPermission") - override val defaultConnections: StateFlow<DefaultConnectionModel> = + private val defaultNetworkCapabilities: SharedFlow<NetworkCapabilities?> = conflatedCallbackFlow { val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { override fun onLost(network: Network) { logger.logOnDefaultLost(network) - // The system no longer has a default network, so everything is - // non-default. - trySend( - DefaultConnectionModel( - Wifi(isDefault = false), - Mobile(isDefault = false), - CarrierMerged(isDefault = false), - Ethernet(isDefault = false), - isValidated = false, - ) - ) + trySend(null) } override fun onCapabilitiesChanged( @@ -143,30 +146,7 @@ constructor( networkCapabilities: NetworkCapabilities, ) { logger.logOnDefaultCapabilitiesChanged(network, networkCapabilities) - - val wifiInfo = - networkCapabilities.getMainOrUnderlyingWifiInfo(connectivityManager) - - val isWifiDefault = - networkCapabilities.hasTransport(TRANSPORT_WIFI) || wifiInfo != null - val isMobileDefault = - networkCapabilities.hasTransport(TRANSPORT_CELLULAR) - val isCarrierMergedDefault = wifiInfo?.isCarrierMerged == true - val isEthernetDefault = - networkCapabilities.hasTransport(TRANSPORT_ETHERNET) - - val isValidated = - networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) - - trySend( - DefaultConnectionModel( - Wifi(isWifiDefault), - Mobile(isMobileDefault), - CarrierMerged(isCarrierMergedDefault), - Ethernet(isEthernetDefault), - isValidated, - ) - ) + trySend(networkCapabilities) } } @@ -174,6 +154,61 @@ constructor( awaitClose { connectivityManager.unregisterNetworkCallback(callback) } } + .shareIn(scope, SharingStarted.WhileSubscribed()) + + override val vcnSubId: StateFlow<Int?> = + defaultNetworkCapabilities + .map { networkCapabilities -> + networkCapabilities?.run { + val subId = (transportInfo as? VcnTransportInfo)?.subId + // Never return an INVALID_SUBSCRIPTION_ID (-1) + if (subId != INVALID_SUBSCRIPTION_ID) { + subId + } else { + null + } + } + } + .distinctUntilChanged() + /* A note for logging: we use -2 here since -1 == INVALID_SUBSCRIPTION_ID */ + .onEach { logger.logVcnSubscriptionId(it ?: -2) } + .stateIn(scope, SharingStarted.Eagerly, null) + + @SuppressLint("MissingPermission") + override val defaultConnections: StateFlow<DefaultConnectionModel> = + defaultNetworkCapabilities + .map { networkCapabilities -> + if (networkCapabilities == null) { + // The system no longer has a default network, so everything is + // non-default. + DefaultConnectionModel( + Wifi(isDefault = false), + Mobile(isDefault = false), + CarrierMerged(isDefault = false), + Ethernet(isDefault = false), + isValidated = false, + ) + } else { + val wifiInfo = + networkCapabilities.getMainOrUnderlyingWifiInfo(connectivityManager) + + val isWifiDefault = + networkCapabilities.hasTransport(TRANSPORT_WIFI) || wifiInfo != null + val isMobileDefault = networkCapabilities.hasTransport(TRANSPORT_CELLULAR) + val isCarrierMergedDefault = wifiInfo?.isCarrierMerged == true + val isEthernetDefault = networkCapabilities.hasTransport(TRANSPORT_ETHERNET) + + val isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) + + DefaultConnectionModel( + Wifi(isWifiDefault), + Mobile(isMobileDefault), + CarrierMerged(isCarrierMergedDefault), + Ethernet(isEthernetDefault), + isValidated, + ) + } + } .distinctUntilChanged() .onEach { logger.logDefaultConnectionsChanged(it) } .stateIn(scope, SharingStarted.Eagerly, DefaultConnectionModel()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 5208064d2c0b..5cc3d52f4494 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -38,7 +38,11 @@ import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository; +import com.android.systemui.statusbar.policy.bluetooth.ConnectionStatusModel; import java.io.PrintWriter; import java.util.ArrayList; @@ -50,14 +54,20 @@ import java.util.concurrent.Executor; import javax.inject.Inject; /** + * Controller for information about bluetooth connections. + * + * Note: Right now, this class and {@link BluetoothRepository} co-exist. Any new code should go in + * {@link BluetoothRepository}, but external clients should query this file for now. */ @SysUISingleton public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback, CachedBluetoothDevice.Callback, LocalBluetoothProfileManager.ServiceListener { private static final String TAG = "BluetoothController"; + private final FeatureFlags mFeatureFlags; private final DumpManager mDumpManager; private final BluetoothLogger mLogger; + private final BluetoothRepository mBluetoothRepository; private final LocalBluetoothManager mLocalBluetoothManager; private final UserManager mUserManager; private final int mCurrentUser; @@ -79,14 +89,18 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa @Inject public BluetoothControllerImpl( Context context, + FeatureFlags featureFlags, UserTracker userTracker, DumpManager dumpManager, BluetoothLogger logger, + BluetoothRepository bluetoothRepository, @Main Looper mainLooper, @Nullable LocalBluetoothManager localBluetoothManager, @Nullable BluetoothAdapter bluetoothAdapter) { + mFeatureFlags = featureFlags; mDumpManager = dumpManager; mLogger = logger; + mBluetoothRepository = bluetoothRepository; mLocalBluetoothManager = localBluetoothManager; mHandler = new H(mainLooper); if (mLocalBluetoothManager != null) { @@ -229,6 +243,16 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa } private void updateConnected() { + if (mFeatureFlags.isEnabled(Flags.NEW_BLUETOOTH_REPOSITORY)) { + mBluetoothRepository.fetchConnectionStatusInBackground( + getDevices(), this::onConnectionStatusFetched); + } else { + updateConnectedOld(); + } + } + + /** Used only if {@link Flags.NEW_BLUETOOTH_REPOSITORY} is *not* enabled. */ + private void updateConnectedOld() { // Make sure our connection state is up to date. int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState(); List<CachedBluetoothDevice> newList = new ArrayList<>(); @@ -249,6 +273,12 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa // connected. state = BluetoothAdapter.STATE_DISCONNECTED; } + onConnectionStatusFetched(new ConnectionStatusModel(state, newList)); + } + + private void onConnectionStatusFetched(ConnectionStatusModel status) { + List<CachedBluetoothDevice> newList = status.getConnectedDevices(); + int state = status.getMaxConnectionState(); synchronized (mConnectedDevices) { mConnectedDevices.clear(); mConnectedDevices.addAll(newList); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt new file mode 100644 index 000000000000..80f3d76f0897 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.statusbar.policy.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothProfile +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Repository for information about bluetooth connections. + * + * Note: Right now, this class and [BluetoothController] co-exist. Any new code should go in this + * implementation, but external clients should query [BluetoothController] instead of this class for + * now. + */ +interface BluetoothRepository { + /** + * Fetches the connection statuses for the given [currentDevices] and invokes [callback] once + * those statuses have been fetched. The fetching occurs on a background thread because IPCs may + * be required to fetch the statuses (see b/271058380). + */ + fun fetchConnectionStatusInBackground( + currentDevices: Collection<CachedBluetoothDevice>, + callback: ConnectionStatusFetchedCallback, + ) +} + +/** Implementation of [BluetoothRepository]. */ +@SysUISingleton +class BluetoothRepositoryImpl +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val localBluetoothManager: LocalBluetoothManager?, +) : BluetoothRepository { + override fun fetchConnectionStatusInBackground( + currentDevices: Collection<CachedBluetoothDevice>, + callback: ConnectionStatusFetchedCallback, + ) { + scope.launch { + val result = fetchConnectionStatus(currentDevices) + callback.onConnectionStatusFetched(result) + } + } + + private suspend fun fetchConnectionStatus( + currentDevices: Collection<CachedBluetoothDevice>, + ): ConnectionStatusModel { + return withContext(bgDispatcher) { + val minimumMaxConnectionState = + localBluetoothManager?.bluetoothAdapter?.connectionState + ?: BluetoothProfile.STATE_DISCONNECTED + var maxConnectionState = + if (currentDevices.isEmpty()) { + minimumMaxConnectionState + } else { + currentDevices + .maxOf { it.maxConnectionState } + .coerceAtLeast(minimumMaxConnectionState) + } + + val connectedDevices = currentDevices.filter { it.isConnected } + + if ( + connectedDevices.isEmpty() && maxConnectionState == BluetoothAdapter.STATE_CONNECTED + ) { + // If somehow we think we are connected, but have no connected devices, we aren't + // connected. + maxConnectionState = BluetoothAdapter.STATE_DISCONNECTED + } + + ConnectionStatusModel(maxConnectionState, connectedDevices) + } + } +} + +data class ConnectionStatusModel( + /** The maximum connection state out of all current devices. */ + val maxConnectionState: Int, + /** A list of devices that are currently connected. */ + val connectedDevices: List<CachedBluetoothDevice>, +) + +/** Callback notified when the new status has been fetched. */ +fun interface ConnectionStatusFetchedCallback { + fun onConnectionStatusFetched(status: ConnectionStatusModel) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 1b7353923ada..e1a7b6d59f41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -62,15 +62,16 @@ import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.statusbar.policy.WalletControllerImpl; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeControllerImpl; - -import java.util.concurrent.Executor; - -import javax.inject.Named; +import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository; +import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepositoryImpl; import dagger.Binds; import dagger.Module; import dagger.Provides; +import java.util.concurrent.Executor; + +import javax.inject.Named; /** Dagger Module for code in the statusbar.policy package. */ @Module @@ -84,6 +85,10 @@ public interface StatusBarPolicyModule { /** */ @Binds + BluetoothRepository provideBluetoothRepository(BluetoothRepositoryImpl impl); + + /** */ + @Binds CastController provideCastController(CastControllerImpl controllerImpl); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index cd1ad1ba788f..316b54eb0c80 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -173,7 +173,7 @@ public class ImageWallpaper extends WallpaperService { .isLockscreenLiveWallpaperEnabled(); mSurfaceHolder = surfaceHolder; Rect dimensions = mIsLockscreenLiveWallpaperEnabled - ? mWallpaperManager.peekBitmapDimensions(getSourceFlag()) + ? mWallpaperManager.peekBitmapDimensions(getSourceFlag(), true) : mWallpaperManager.peekBitmapDimensions(); int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width()); int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height()); @@ -325,7 +325,7 @@ public class ImageWallpaper extends WallpaperService { try { bitmap = mIsLockscreenLiveWallpaperEnabled ? mWallpaperManager.getBitmapAsUser( - mUserTracker.getUserId(), false, getSourceFlag()) + mUserTracker.getUserId(), false, getSourceFlag(), true) : mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false); if (bitmap != null && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { @@ -347,7 +347,7 @@ public class ImageWallpaper extends WallpaperService { try { bitmap = mIsLockscreenLiveWallpaperEnabled ? mWallpaperManager.getBitmapAsUser( - mUserTracker.getUserId(), false, getSourceFlag()) + mUserTracker.getUserId(), false, getSourceFlag(), true) : mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false); } catch (RuntimeException | OutOfMemoryError e) { Log.w(TAG, "Unable to load default wallpaper!", e); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index fb738454fc71..d8e2a3842e85 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -134,6 +134,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private KeyguardClockSwitchController mController; private View mSliceView; + private LinearLayout mStatusArea; private FakeExecutor mExecutor; @Before @@ -195,8 +196,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSliceView = new View(getContext()); when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); - when(mView.findViewById(R.id.keyguard_status_area)).thenReturn( - new LinearLayout(getContext())); + mStatusArea = new LinearLayout(getContext()); + when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea); } @Test @@ -401,6 +402,15 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { assertNull(mController.getClock()); } + @Test + public void testSetAlpha_setClockAlphaForCLockFace() { + mController.onViewAttached(); + mController.setAlpha(0.5f); + verify(mLargeClockView).setAlpha(0.5f); + verify(mSmallClockView).setAlpha(0.5f); + assertEquals(0.5f, mStatusArea.getAlpha(), 0.0f); + } + private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt index 508aea51b666..a8c281c24700 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt @@ -24,6 +24,8 @@ class KeyguardStatusViewTest : SysuiTestCase() { get() = keyguardStatusView.findViewById(R.id.status_view_media_container) private val statusViewContainer: ViewGroup get() = keyguardStatusView.findViewById(R.id.status_view_container) + private val clockView: ViewGroup + get() = keyguardStatusView.findViewById(R.id.keyguard_clock_container) private val childrenExcludingMedia get() = statusViewContainer.children.filter { it != mediaView } @@ -56,4 +58,12 @@ class KeyguardStatusViewTest : SysuiTestCase() { assertThat(it.translationY).isEqualTo(translationY) } } + + @Test + fun setAlphaExcludeClock() { + keyguardStatusView.setAlpha(0.5f, /* excludeClock= */true) + assertThat(statusViewContainer.alpha).isNotEqualTo(0.5f) + assertThat(mediaView.alpha).isEqualTo(0.5f) + assertThat(clockView.alpha).isNotEqualTo(0.5f) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 8f58140bce43..d4c2bafe016f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -52,6 +52,8 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEventLogger; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardDisplayManager; import com.android.keyguard.KeyguardSecurityView; @@ -68,6 +70,7 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; @@ -77,6 +80,7 @@ import com.android.systemui.shade.ShadeWindowLogger; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -143,6 +147,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private FalsingCollectorFake mFalsingCollector; private @Mock CentralSurfaces mCentralSurfaces; + private @Mock UiEventLogger mUiEventLogger; + private @Mock SessionTracker mSessionTracker; private FakeFeatureFlags mFeatureFlags; @@ -543,9 +549,26 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { assertTrue(mViewMediator.isShowingAndNotOccluded()); } + @Test + public void testOnStartedWakingUp_logsUiEvent() { + final InstanceId instanceId = InstanceId.fakeInstanceId(8); + when(mSessionTracker.getSessionId((anyInt()))).thenReturn(instanceId); + mViewMediator.onStartedWakingUp(PowerManager.WAKE_REASON_LIFT, false); + + verify(mUiEventLogger).logWithInstanceIdAndPosition( + eq(BiometricUnlockController.BiometricUiEvent.STARTED_WAKING_UP), + anyInt(), + any(), + eq(instanceId), + eq(PowerManager.WAKE_REASON_LIFT) + ); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, + mUiEventLogger, + mSessionTracker, mUserTracker, mFalsingCollector, mLockPatternUtils, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java index 0e2a3acd3df4..190ee81cc55a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java @@ -24,13 +24,13 @@ import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; -import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -76,7 +76,7 @@ public class RemoteTransitionTest extends SysuiTestCase { .addChange(TRANSIT_CLOSE, 0 /* flags */, createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD)) .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */) - .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */) + .addChange(TRANSIT_CHANGE, FLAG_IS_DIVIDER_BAR, null /* taskInfo */) .build(); // Check apps extraction RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, @@ -107,7 +107,7 @@ public class RemoteTransitionTest extends SysuiTestCase { RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined, false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(1, nonApps.length); - assertTrue(nonApps[0].prefixOrderIndex < closeLayer); + assertTrue(nonApps[0].prefixOrderIndex == Integer.MAX_VALUE); assertEquals(MODE_CHANGING, nonApps[0].mode); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index c7ea09cb519d..2e5afa4a8b50 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -340,6 +340,11 @@ public class NetworkControllerBaseTest extends SysuiTestCase { } public void setConnectivityViaCallbackInNetworkController( + Network network, NetworkCapabilities networkCapabilities) { + mDefaultCallbackInNetworkController.onCapabilitiesChanged(network, networkCapabilities); + } + + public void setConnectivityViaCallbackInNetworkController( int networkType, boolean validated, boolean isConnected, WifiInfo wifiInfo) { final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(mNetCapabilities); @@ -351,6 +356,13 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mock(Network.class), builder.build()); } + public void setConnectivityViaDefaultAndNormalCallbackInWifiTracker( + Network network, NetworkCapabilities networkCapabilities) { + mNetworkCallback.onAvailable(network); + mNetworkCallback.onCapabilitiesChanged(network, networkCapabilities); + mDefaultCallbackInWifiTracker.onCapabilitiesChanged(network, networkCapabilities); + } + public void setConnectivityViaCallbackInWifiTracker( int networkType, boolean validated, boolean isConnected, WifiInfo wifiInfo) { final NetworkCapabilities.Builder builder = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java index 68170ea4b518..44a1c50e5a58 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.connectivity; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -25,6 +28,7 @@ import static org.mockito.Mockito.when; import android.content.Intent; import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.vcn.VcnTransportInfo; @@ -43,6 +47,8 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.util.Collections; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -269,6 +275,83 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest { } } + /** Test for b/225902574. */ + @Test + public void vcnOnlyOnUnderlyingNetwork() { + setWifiEnabled(true); + + // Set up a carrier merged network... + WifiInfo underlyingCarrierMergedInfo = Mockito.mock(WifiInfo.class); + when(underlyingCarrierMergedInfo.isCarrierMerged()).thenReturn(true); + when(underlyingCarrierMergedInfo.isPrimary()).thenReturn(true); + int zeroLevel = 0; + when(underlyingCarrierMergedInfo.getRssi()).thenReturn(calculateRssiForLevel(zeroLevel)); + + NetworkCapabilities underlyingNetworkCapabilities = Mockito.mock(NetworkCapabilities.class); + when(underlyingNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + .thenReturn(true); + when(underlyingNetworkCapabilities.getTransportInfo()) + .thenReturn(underlyingCarrierMergedInfo); + + Network underlyingNetwork = Mockito.mock(Network.class); + when(mMockCm.getNetworkCapabilities(underlyingNetwork)) + .thenReturn(underlyingNetworkCapabilities); + + NetworkCapabilities.Builder mainCapabilitiesBuilder = new NetworkCapabilities.Builder(); + mainCapabilitiesBuilder.addTransportType(TRANSPORT_CELLULAR); + mainCapabilitiesBuilder.setTransportInfo(null); + // And make the carrier merged network the underlying network, *not* the main network. + mainCapabilitiesBuilder.setUnderlyingNetworks(Collections.singletonList(underlyingNetwork)); + + Network primaryNetwork = Mockito.mock(Network.class); + int primaryNetworkId = 1; + when(primaryNetwork.getNetId()).thenReturn(primaryNetworkId); + + // WHEN this primary network with underlying carrier merged information is sent + setConnectivityViaDefaultAndNormalCallbackInWifiTracker( + primaryNetwork, mainCapabilitiesBuilder.build()); + + // THEN we see the mobile data indicators for carrier merged + verifyLastMobileDataIndicatorsForVcn( + /* visible= */ true, + /* level= */ zeroLevel, + TelephonyIcons.ICON_CWF, + /* inet= */ false); + + // For each level... + for (int testLevel = 0; testLevel < WifiIcons.WIFI_LEVEL_COUNT; testLevel++) { + int rssi = calculateRssiForLevel(testLevel); + when(underlyingCarrierMergedInfo.getRssi()).thenReturn(rssi); + // WHEN the new level is sent to the callbacks + setConnectivityViaDefaultAndNormalCallbackInWifiTracker( + primaryNetwork, mainCapabilitiesBuilder.build()); + + // WHEN the network is validated + mainCapabilitiesBuilder.addCapability(NET_CAPABILITY_VALIDATED); + setConnectivityViaCallbackInNetworkController( + primaryNetwork, mainCapabilitiesBuilder.build()); + + // THEN we see the mobile data indicators with inet=true (no exclamation mark) + verifyLastMobileDataIndicatorsForVcn( + /* visible= */ true, + testLevel, + TelephonyIcons.ICON_CWF, + /* inet= */ true); + + // WHEN the network is not validated + mainCapabilitiesBuilder.removeCapability(NET_CAPABILITY_VALIDATED); + setConnectivityViaCallbackInNetworkController( + primaryNetwork, mainCapabilitiesBuilder.build()); + + // THEN we see the mobile data indicators with inet=false (exclamation mark) + verifyLastMobileDataIndicatorsForVcn( + /* visible= */ true, + testLevel, + TelephonyIcons.ICON_CWF, + /* inet= */ false); + } + } + @Test public void testDisableWiFiWithVcnWithUnderlyingWifi() { String testSsid = "Test VCN SSID"; @@ -290,11 +373,7 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest { } protected void setWifiLevel(int level) { - float amountPerLevel = (MAX_RSSI - MIN_RSSI) / (WifiIcons.WIFI_LEVEL_COUNT - 1); - int rssi = (int) (MIN_RSSI + level * amountPerLevel); - // Put RSSI in the middle of the range. - rssi += amountPerLevel / 2; - when(mWifiInfo.getRssi()).thenReturn(rssi); + when(mWifiInfo.getRssi()).thenReturn(calculateRssiForLevel(level)); setConnectivityViaCallbackInWifiTracker( NetworkCapabilities.TRANSPORT_WIFI, false, true, mWifiInfo); } @@ -312,19 +391,23 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest { } protected void setWifiLevelForVcn(int level) { - float amountPerLevel = (MAX_RSSI - MIN_RSSI) / (WifiIcons.WIFI_LEVEL_COUNT - 1); - int rssi = (int) (MIN_RSSI + level * amountPerLevel); - // Put RSSI in the middle of the range. - rssi += amountPerLevel / 2; when(mVcnTransportInfo.getWifiInfo()).thenReturn(mWifiInfo); when(mVcnTransportInfo.makeCopy(anyLong())).thenReturn(mVcnTransportInfo); - when(mWifiInfo.getRssi()).thenReturn(rssi); + when(mWifiInfo.getRssi()).thenReturn(calculateRssiForLevel(level)); when(mWifiInfo.isCarrierMerged()).thenReturn(true); when(mWifiInfo.getSubscriptionId()).thenReturn(1); setConnectivityViaCallbackInWifiTrackerForVcn( NetworkCapabilities.TRANSPORT_CELLULAR, false, true, mVcnTransportInfo); } + private int calculateRssiForLevel(int level) { + float amountPerLevel = (MAX_RSSI - MIN_RSSI) / (WifiIcons.WIFI_LEVEL_COUNT - 1); + int rssi = (int) (MIN_RSSI + level * amountPerLevel); + // Put RSSI in the middle of the range. + rssi += amountPerLevel / 2; + return rssi; + } + protected void setWifiStateForVcn(boolean connected, String ssid) { when(mVcnTransportInfo.getWifiInfo()).thenReturn(mWifiInfo); when(mVcnTransportInfo.makeCopy(anyLong())).thenReturn(mVcnTransportInfo); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt index 746544a3563f..d3f5adeb05bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt @@ -118,7 +118,10 @@ class GenericGestureDetectorTest : SysuiTestCase() { assertThat(oldCallbackNotified).isFalse() } - inner class TestGestureDetector : GenericGestureDetector("fakeTag", displayTracker) { + inner class TestGestureDetector : GenericGestureDetector( + "fakeTag", + displayTracker.defaultDisplayId + ) { var isGestureListening = false override fun onInputEvent(ev: InputEvent) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index bde05b9f499e..5a887ebcee80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository @@ -131,6 +132,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { context, IMMEDIATE, scope, + FakeAirplaneModeRepository(), wifiRepository, mock(), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 7cc59b67414b..38c7432eb288 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -34,13 +34,16 @@ import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager +import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.internal.telephony.PhoneConstants import com.android.settingslib.R import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository @@ -51,25 +54,25 @@ import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManag import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl -import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository -import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.UUID -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.yield -import org.junit.After +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before @@ -83,6 +86,9 @@ import org.mockito.MockitoAnnotations @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +// This is required because our [SubscriptionManager.OnSubscriptionsChangedListener] uses a looper +// to run the callback and this makes the looper place nicely with TestScope etc. +@TestableLooper.RunWithLooper class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl @@ -90,7 +96,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory private lateinit var connectivityRepository: ConnectivityRepository - private lateinit var wifiRepository: FakeWifiRepository + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var wifiRepository: WifiRepository private lateinit var carrierConfigRepository: CarrierConfigRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @@ -102,7 +109,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private val mobileMappings = FakeMobileMappingsProxy() private val subscriptionManagerProxy = FakeSubscriptionManagerProxy() - private val scope = CoroutineScope(IMMEDIATE) + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) @Before fun setUp() { @@ -138,11 +146,23 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, mock(), mock(), - scope, + testScope.backgroundScope, mock(), ) - wifiRepository = FakeWifiRepository() + airplaneModeRepository = FakeAirplaneModeRepository() + + wifiRepository = + WifiRepositoryImpl( + fakeBroadcastDispatcher, + connectivityManager, + connectivityRepository, + mock(), + mock(), + FakeExecutor(FakeSystemClock()), + testScope.backgroundScope, + mock(), + ) carrierConfigRepository = CarrierConfigRepository( @@ -150,28 +170,28 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { mock(), mock(), logger, - scope, + testScope.backgroundScope, ) connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, telephonyManager = telephonyManager, - bgDispatcher = IMMEDIATE, + bgDispatcher = dispatcher, logger = logger, mobileMappingsProxy = mobileMappings, - scope = scope, + scope = testScope.backgroundScope, carrierConfigRepository = carrierConfigRepository, ) carrierMergedFactory = CarrierMergedConnectionRepository.Factory( telephonyManager, - scope, + testScope.backgroundScope, wifiRepository, ) fullConnectionFactory = FullMobileConnectionRepository.Factory( - scope = scope, + scope = testScope.backgroundScope, logFactory = logBufferFactory, mobileRepoFactory = connectionFactory, carrierMergedRepoFactory = carrierMergedFactory, @@ -188,46 +208,38 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { mobileMappings, fakeBroadcastDispatcher, context, - IMMEDIATE, - scope, + dispatcher, + testScope.backgroundScope, + airplaneModeRepository, wifiRepository, fullConnectionFactory, ) - } - @After - fun tearDown() { - scope.cancel() + testScope.runCurrent() } @Test fun testSubscriptions_initiallyEmpty() = - runBlocking(IMMEDIATE) { + testScope.runTest { assertThat(underTest.subscriptions.value).isEqualTo(listOf<SubscriptionModel>()) } @Test fun testSubscriptions_listUpdates() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionModel>? = null - - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.subscriptions) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2)) getSubscriptionCallback().onSubscriptionsChanged() assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) - - job.cancel() } @Test fun testSubscriptions_removingSub_updatesList() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionModel>? = null - - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.subscriptions) // WHEN 2 networks show up whenever(subscriptionManager.completeActiveSubscriptionInfoList) @@ -241,71 +253,55 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // THEN the subscriptions list represents the newest change assertThat(latest).isEqualTo(listOf(MODEL_2)) - - job.cancel() } @Test fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionModel>? = null + testScope.runTest { + val latest by collectLastValue(underTest.subscriptions) - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) - - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() assertThat(latest).isEqualTo(listOf(MODEL_CM)) - - job.cancel() } @Test fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = - runBlocking(IMMEDIATE) { - var latest: List<SubscriptionModel>? = null + testScope.runTest { + val latest by collectLastValue(underTest.subscriptions) - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) - - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM)) - - job.cancel() } @Test fun testActiveDataSubscriptionId_initialValueIsNull() = - runBlocking(IMMEDIATE) { + testScope.runTest { assertThat(underTest.activeMobileDataSubscriptionId.value).isEqualTo(null) } @Test fun testActiveDataSubscriptionId_updates() = - runBlocking(IMMEDIATE) { - var active: Int? = null - - val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) + testScope.runTest { + val active by collectLastValue(underTest.activeMobileDataSubscriptionId) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) assertThat(active).isEqualTo(SUB_2_ID) - - job.cancel() } @Test fun activeSubId_nullIfInvalidSubIdIsReceived() = - runBlocking(IMMEDIATE) { - var latest: Int? = null - - val job = underTest.activeMobileDataSubscriptionId.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.activeMobileDataSubscriptionId) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) @@ -316,8 +312,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) assertThat(latest).isNull() - - job.cancel() } @Test @@ -327,23 +321,19 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Test fun activeRepo_updatesWithActiveDataId() = - runBlocking(IMMEDIATE) { - var latest: MobileConnectionRepository? = null - val job = underTest.activeMobileDataRepository.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.activeMobileDataRepository) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) assertThat(latest?.subId).isEqualTo(SUB_2_ID) - - job.cancel() } @Test fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() = - runBlocking(IMMEDIATE) { - var latest: MobileConnectionRepository? = null - val job = underTest.activeMobileDataRepository.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.activeMobileDataRepository) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) @@ -354,64 +344,49 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) assertThat(latest).isNull() - - job.cancel() } @Test /** Regression test for b/268146648. */ fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() = - runBlocking(IMMEDIATE) { - var activeRepo: MobileConnectionRepository? = null - var subscriptions: List<SubscriptionModel>? = null - - val activeRepoJob = - underTest.activeMobileDataRepository.onEach { activeRepo = it }.launchIn(this) - val subscriptionsJob = - underTest.subscriptions.onEach { subscriptions = it }.launchIn(this) + testScope.runTest { + val activeRepo by collectLastValue(underTest.activeMobileDataRepository) + val subscriptions by collectLastValue(underTest.subscriptions) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) assertThat(subscriptions).isEmpty() assertThat(activeRepo).isNotNull() - - activeRepoJob.cancel() - subscriptionsJob.cancel() } @Test fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() = - runBlocking(IMMEDIATE) { - var latest: MobileConnectionRepository? = null - var subscriptions: List<SubscriptionModel>? = null - val activeSubIdJob = - underTest.activeMobileDataSubscriptionId - .filterNotNull() - .onEach { latest = underTest.getRepoForSubId(it) } - .launchIn(this) - val subscriptionsJob = - underTest.subscriptions.onEach { subscriptions = it }.launchIn(this) + testScope.runTest { + var latestActiveRepo: MobileConnectionRepository? = null + collectLastValue( + underTest.activeMobileDataSubscriptionId.filterNotNull().onEach { + latestActiveRepo = underTest.getRepoForSubId(it) + } + ) + + val latestSubscriptions by collectLastValue(underTest.subscriptions) // Active data subscription id is sent, but no subscription change has been posted yet getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) // Subscriptions list is empty - assertThat(subscriptions).isEmpty() + assertThat(latestSubscriptions).isEmpty() // getRepoForSubId does not throw - assertThat(latest).isNotNull() - - activeSubIdJob.cancel() - subscriptionsJob.cancel() + assertThat(latestActiveRepo).isNotNull() } @Test fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() = - runBlocking(IMMEDIATE) { - var activeRepo: MobileConnectionRepository? = null - val job = underTest.activeMobileDataRepository.onEach { activeRepo = it }.launchIn(this) - val subscriptionsJob = underTest.subscriptions.launchIn(this) + testScope.runTest { + val activeRepo by collectLastValue(underTest.activeMobileDataRepository) + collectLastValue(underTest.subscriptions) // GIVEN active repo is updated before the subscription list updates getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() @@ -429,15 +404,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // THEN the newly request repo has been cached and reused assertThat(activeRepo).isSameInstanceAs(newRepo) - - job.cancel() - subscriptionsJob.cancel() } @Test fun testConnectionRepository_validSubId_isCached() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1)) @@ -447,16 +419,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { val repo2 = underTest.getRepoForSubId(SUB_1_ID) assertThat(repo1).isSameInstanceAs(repo2) - - job.cancel() } @Test fun testConnectionRepository_carrierMergedSubId_isCached() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() @@ -465,16 +436,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { val repo2 = underTest.getRepoForSubId(SUB_CM_ID) assertThat(repo1).isSameInstanceAs(repo2) - - job.cancel() } @Test fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() @@ -483,16 +453,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - - job.cancel() } @Test fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() @@ -503,26 +472,28 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(mobileRepo.getIsCarrierMerged()).isFalse() // WHEN the wifi network updates to be not carrier merged - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1)) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) + runCurrent() // THEN the repos update val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) mobileRepo = underTest.getRepoForSubId(SUB_1_ID) assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - - job.cancel() } @Test fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) - wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() + runCurrent() val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) @@ -530,21 +501,21 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(mobileRepo.getIsCarrierMerged()).isFalse() // WHEN the wifi network updates to be carrier merged - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + runCurrent() // THEN the repos update val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) mobileRepo = underTest.getRepoForSubId(SUB_1_ID) assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - - job.cancel() } @Test fun testConnectionCache_clearsInvalidSubscriptions() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2)) @@ -563,16 +534,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { getSubscriptionCallback().onSubscriptionsChanged() assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) - - job.cancel() } @Test fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) getSubscriptionCallback().onSubscriptionsChanged() @@ -591,15 +561,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { getSubscriptionCallback().onSubscriptionsChanged() assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) - - job.cancel() } /** Regression test for b/261706421 */ @Test fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2)) @@ -617,26 +585,20 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { getSubscriptionCallback().onSubscriptionsChanged() assertThat(underTest.getSubIdRepoCache()).isEmpty() - - job.cancel() } @Test fun testConnectionRepository_invalidSubId_throws() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) - + testScope.runTest { assertThrows(IllegalArgumentException::class.java) { underTest.getRepoForSubId(SUB_1_ID) } - - job.cancel() } @Test fun connectionRepository_logBufferContainsSubIdInItsName() = - runBlocking(IMMEDIATE) { - val job = underTest.subscriptions.launchIn(this) + testScope.runTest { + collectLastValue(underTest.subscriptions) whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2)) @@ -655,15 +617,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { eq(tableBufferLogName(SUB_2_ID)), anyInt(), ) - - job.cancel() } @Test fun testDefaultDataSubId_updatesOnBroadcast() = - runBlocking(IMMEDIATE) { - var latest: Int? = null - val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.defaultDataSubId) assertThat(latest).isEqualTo(INVALID_SUBSCRIPTION_ID) @@ -686,28 +645,24 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } assertThat(latest).isEqualTo(SUB_1_ID) - - job.cancel() } @Test fun defaultDataSubId_fetchesInitialValueOnStart() = - runBlocking(IMMEDIATE) { + testScope.runTest { subscriptionManagerProxy.defaultDataSubId = 2 - var latest: Int? = null - val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.defaultDataSubId) assertThat(latest).isEqualTo(2) - - job.cancel() } @Test fun defaultDataSubId_fetchesCurrentOnRestart() = - runBlocking(IMMEDIATE) { + testScope.runTest { subscriptionManagerProxy.defaultDataSubId = 2 var latest: Int? = null var job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + runCurrent() assertThat(latest).isEqualTo(2) @@ -720,6 +675,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { subscriptionManagerProxy.defaultDataSubId = 1 job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + runCurrent() assertThat(latest).isEqualTo(1) @@ -733,43 +689,37 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Test fun mobileIsDefault_capsHaveCellular_isDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) } - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.mobileIsDefault) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun mobileIsDefault_capsDoNotHaveCellular_isNotDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) } - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.mobileIsDefault) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isFalse() - - job.cancel() } @Test fun mobileIsDefault_carrierMergedViaMobile_isDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } val caps = @@ -778,151 +728,144 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(it.transportInfo).thenReturn(carrierMergedInfo) } - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.mobileIsDefault) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun mobileIsDefault_wifiDefault_mobileNotDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) } - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.mobileIsDefault) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isFalse() - - job.cancel() } @Test fun mobileIsDefault_ethernetDefault_mobileNotDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true) } - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.mobileIsDefault) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isFalse() - - job.cancel() } /** Regression test for b/272586234. */ @Test fun hasCarrierMergedConnection_carrierMergedViaWifi_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) whenever(it.transportInfo).thenReturn(carrierMergedInfo) } - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.hasCarrierMergedConnection) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun hasCarrierMergedConnection_carrierMergedViaMobile_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) whenever(it.transportInfo).thenReturn(carrierMergedInfo) } - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.hasCarrierMergedConnection) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } /** Regression test for b/272586234. */ @Test fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) } - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.hasCarrierMergedConnection) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun hasCarrierMergedConnection_carrierMergedViaMobileWithVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) } - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.hasCarrierMergedConnection) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingWifi_isTrue() = - runBlocking(IMMEDIATE) { - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.hasCarrierMergedConnection) val underlyingNetwork = mock<Network>() val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val underlyingWifiCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) @@ -941,23 +884,23 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) // THEN there's a carrier merged connection assertThat(latest).isTrue() - - job.cancel() } @Test fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingCellular_isTrue() = - runBlocking(IMMEDIATE) { - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.hasCarrierMergedConnection) val underlyingCarrierMergedNetwork = mock<Network>() val carrierMergedInfo = - mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + mock<WifiInfo>().apply { + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.isPrimary).thenReturn(true) + } val underlyingCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) @@ -977,22 +920,19 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) // THEN there's a carrier merged connection assertThat(latest).isTrue() - - job.cancel() } /** Regression test for b/272586234. */ @Test - fun hasCarrierMergedConnection_defaultNotCarrierMerged_butWifiRepoHasCarrierMerged_isTrue() = - runBlocking(IMMEDIATE) { - var latest: Boolean? = null - val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + fun hasCarrierMergedConnection_defaultIsWifiNotCarrierMerged_wifiRepoIsCarrierMerged_isTrue() = + testScope.runTest { + val latest by collectLastValue(underTest.hasCarrierMergedConnection) - // WHEN the default callback isn't carrier merged + // WHEN the default callback is TRANSPORT_WIFI but not carrier merged val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(false) } val caps = @@ -1001,16 +941,57 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(it.transportInfo).thenReturn(carrierMergedInfo) } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - yield() // BUT the wifi repo has gotten updates that it *is* carrier merged - wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) - yield() + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) // THEN hasCarrierMergedConnection is true assertThat(latest).isTrue() + } - job.cancel() + /** Regression test for b/278618530. */ + @Test + fun hasCarrierMergedConnection_defaultIsCellular_wifiRepoIsCarrierMerged_isFalse() = + testScope.runTest { + val latest by collectLastValue(underTest.hasCarrierMergedConnection) + + // WHEN the default callback is TRANSPORT_CELLULAR and not carrier merged + val caps = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(null) + } + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + // BUT the wifi repo has gotten updates that it *is* carrier merged + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + + // THEN hasCarrierMergedConnection is **false** (The default network being CELLULAR + // takes precedence over the wifi network being carrier merged.) + assertThat(latest).isFalse() + } + + /** Regression test for b/278618530. */ + @Test + fun hasCarrierMergedConnection_defaultCellular_wifiIsCarrierMerged_airplaneMode_isTrue() = + testScope.runTest { + val latest by collectLastValue(underTest.hasCarrierMergedConnection) + + // WHEN the default callback is TRANSPORT_CELLULAR and not carrier merged + val caps = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(null) + } + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + // BUT the wifi repo has gotten updates that it *is* carrier merged + getNormalNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + // AND we're in airplane mode + airplaneModeRepository.setIsAirplaneMode(true) + + // THEN hasCarrierMergedConnection is true. + assertThat(latest).isTrue() } @Test @@ -1020,43 +1001,37 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Test fun defaultConnectionIsValidated_capsHaveValidated_isValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true) } - var latest: Boolean? = null - val job = underTest.defaultConnectionIsValidated.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.defaultConnectionIsValidated) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isTrue() - - job.cancel() } @Test fun defaultConnectionIsValidated_capsHaveNotValidated_isNotValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val caps = mock<NetworkCapabilities>().also { whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(false) } - var latest: Boolean? = null - val job = underTest.defaultConnectionIsValidated.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.defaultConnectionIsValidated) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) assertThat(latest).isFalse() - - job.cancel() } @Test fun config_initiallyFromContext() = - runBlocking(IMMEDIATE) { + testScope.runTest { overrideResource(R.bool.config_showMin3G, true) val configFromContext = MobileMappings.Config.readConfig(context) assertThat(configFromContext.showAtLeast3G).isTrue() @@ -1074,26 +1049,24 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { mobileMappings, fakeBroadcastDispatcher, context, - IMMEDIATE, - scope, + dispatcher, + testScope.backgroundScope, + airplaneModeRepository, wifiRepository, fullConnectionFactory, ) - var latest: MobileMappings.Config? = null - val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.defaultDataSubRatConfig) assertTrue(latest!!.areEqual(configFromContext)) assertTrue(latest!!.showAtLeast3G) - - job.cancel() } @Test fun config_subIdChangeEvent_updated() = - runBlocking(IMMEDIATE) { - var latest: MobileMappings.Config? = null - val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.defaultDataSubRatConfig) + assertThat(latest!!.showAtLeast3G).isFalse() overrideResource(R.bool.config_showMin3G, true) @@ -1112,15 +1085,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // THEN the config is updated assertTrue(latest!!.areEqual(configFromContext)) assertTrue(latest!!.showAtLeast3G) - - job.cancel() } @Test fun config_carrierConfigChangeEvent_updated() = - runBlocking(IMMEDIATE) { - var latest: MobileMappings.Config? = null - val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.defaultDataSubRatConfig) + assertThat(latest!!.showAtLeast3G).isFalse() overrideResource(R.bool.config_showMin3G, true) @@ -1138,15 +1109,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // THEN the config is updated assertThat(latest!!.areEqual(configFromContext)).isTrue() assertThat(latest!!.showAtLeast3G).isTrue() - - job.cancel() } @Test fun activeDataChange_inSameGroup_emitsUnit() = - runBlocking(IMMEDIATE) { - var latest: Unit? = null - val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.activeSubChangedInGroupEvent) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED) @@ -1154,15 +1122,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED) assertThat(latest).isEqualTo(Unit) - - job.cancel() } @Test fun activeDataChange_notInSameGroup_doesNotEmit() = - runBlocking(IMMEDIATE) { - var latest: Unit? = null - val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this) + testScope.runTest { + val latest by collectLastValue(underTest.activeSubChangedInGroupEvent) getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED) @@ -1170,38 +1135,46 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { .onActiveDataSubscriptionIdChanged(SUB_1_ID) assertThat(latest).isEqualTo(null) - - job.cancel() } - private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + private fun TestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + runCurrent() val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) return callbackCaptor.value!! } - private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + // Note: This is used to update the [WifiRepository]. + private fun TestScope.getNormalNetworkCallback(): ConnectivityManager.NetworkCallback { + runCurrent() + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun TestScope.getSubscriptionCallback(): + SubscriptionManager.OnSubscriptionsChangedListener { + runCurrent() val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() verify(subscriptionManager) .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) return callbackCaptor.value!! } - private fun getTelephonyCallbacks(): List<TelephonyCallback> { + private fun TestScope.getTelephonyCallbacks(): List<TelephonyCallback> { + runCurrent() val callbackCaptor = argumentCaptor<TelephonyCallback>() verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) return callbackCaptor.allValues } - private inline fun <reified T> getTelephonyCallbackForType(): T { - val cbs = getTelephonyCallbacks().filterIsInstance<T>() + private inline fun <reified T> TestScope.getTelephonyCallbackForType(): T { + val cbs = this.getTelephonyCallbacks().filterIsInstance<T>() assertThat(cbs.size).isEqualTo(1) return cbs[0] } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - // Subscription 1 private const val SUB_1_ID = 1 private val GROUP_1 = ParcelUuid(UUID.randomUUID()) @@ -1259,11 +1232,30 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private val SUB_CM = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) - private val WIFI_NETWORK_CM = - WifiNetworkModel.CarrierMerged( - networkId = 3, - subscriptionId = SUB_CM_ID, - level = 1, - ) + + private val WIFI_INFO_CM = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(SUB_CM_ID) + } + private val WIFI_NETWORK_CAPS_CM = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(WIFI_INFO_CM) + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true) + } + + private val WIFI_INFO_ACTIVE = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(false) + } + private val WIFI_NETWORK_CAPS_ACTIVE = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(WIFI_INFO_ACTIVE) + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 6e1ab58db56d..1fb76b048d47 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -272,6 +272,52 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test + fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = + testScope.runTest { + val (sub1, sub3) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + connectionsRepository.setSubscriptions(listOf(sub1, sub3)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + connectivityRepository.vcnSubId.value = SUB_3_ID + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionModel>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf(sub3)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = + testScope.runTest { + val (sub1, sub3) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + connectionsRepository.setSubscriptions(listOf(sub1, sub3)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + connectivityRepository.vcnSubId.value = SUB_1_ID + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionModel>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf(sub1)) + + job.cancel() + } + + @Test fun activeDataConnection_turnedOn() = testScope.runTest { CONNECTION_1.setDataEnabled(true) @@ -361,6 +407,21 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun failedConnection_carrierMergedDefault_notValidated_failed() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.hasCarrierMergedConnection.value = true + connectionsRepository.defaultConnectionIsValidated.value = false + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + /** Regression test for b/275076959. */ @Test fun failedConnection_dataSwitchInSameGroup_notFailed() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt index 661002d275b0..fa4e91b68a5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt @@ -24,6 +24,7 @@ import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager @@ -37,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.tuner.TunerService import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -655,6 +657,139 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { } @Test + fun vcnSubId_initiallyNull() { + assertThat(underTest.vcnSubId.value).isNull() + } + + @Test + fun vcnSubId_tracksVcnTransportInfo() = + testScope.runTest { + val vcnInfo = VcnTransportInfo(SUB_1_ID) + + var latest: Int? = null + val job = underTest.vcnSubId.onEach { latest = it }.launchIn(this) + + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(vcnInfo) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isEqualTo(SUB_1_ID) + job.cancel() + } + + @Test + fun vcnSubId_filersOutInvalid() = + testScope.runTest { + val vcnInfo = VcnTransportInfo(INVALID_SUBSCRIPTION_ID) + + var latest: Int? = null + val job = underTest.vcnSubId.onEach { latest = it }.launchIn(this) + + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(vcnInfo) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isNull() + job.cancel() + } + + @Test + fun vcnSubId_nullIfNoTransportInfo() = + testScope.runTest { + var latest: Int? = null + val job = underTest.vcnSubId.onEach { latest = it }.launchIn(this) + + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(null) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isNull() + job.cancel() + } + + @Test + fun vcnSubId_nullIfVcnInfoIsNotCellular() = + testScope.runTest { + // If the underlying network of the VCN is a WiFi network, then there is no subId that + // could disagree with telephony's active data subscription id. + + var latest: Int? = null + val job = underTest.vcnSubId.onEach { latest = it }.launchIn(this) + + val wifiInfo = mock<WifiInfo>() + val vcnInfo = VcnTransportInfo(wifiInfo) + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(vcnInfo) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isNull() + job.cancel() + } + + @Test + fun vcnSubId_changingVcnInfoIsTracked() = + testScope.runTest { + var latest: Int? = null + val job = underTest.vcnSubId.onEach { latest = it }.launchIn(this) + + val wifiInfo = mock<WifiInfo>() + val wifiVcnInfo = VcnTransportInfo(wifiInfo) + val sub1VcnInfo = VcnTransportInfo(SUB_1_ID) + val sub2VcnInfo = VcnTransportInfo(SUB_2_ID) + + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(wifiVcnInfo) + } + + // WIFI VCN info + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isNull() + + // Cellular VCN info with subId 1 + whenever(capabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true) + whenever(capabilities.transportInfo).thenReturn(sub1VcnInfo) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isEqualTo(SUB_1_ID) + + // Cellular VCN info with subId 2 + whenever(capabilities.transportInfo).thenReturn(sub2VcnInfo) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isEqualTo(SUB_2_ID) + + // No VCN anymore + whenever(capabilities.transportInfo).thenReturn(null) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test fun getMainOrUnderlyingWifiInfo_wifi_hasInfo() { val wifiInfo = mock<WifiInfo>() val capabilities = @@ -964,6 +1099,9 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { private const val SLOT_WIFI = "wifi" private const val SLOT_MOBILE = "mobile" + private const val SUB_1_ID = 1 + private const val SUB_2_ID = 2 + const val NETWORK_ID = 45 val NETWORK = mock<Network>().apply { whenever(this.getNetId()).thenReturn(NETWORK_ID) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt index 9e825b704851..8f28cc003d67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt @@ -30,6 +30,8 @@ class FakeConnectivityRepository : ConnectivityRepository { override val defaultConnections: StateFlow<DefaultConnectionModel> = MutableStateFlow(DefaultConnectionModel()) + override val vcnSubId: MutableStateFlow<Int?> = MutableStateFlow(null) + fun setForceHiddenIcons(hiddenIcons: Set<ConnectivitySlot>) { _forceHiddenIcons.value = hiddenIcons } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java index 2b1370544bfd..7402b4d64b16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java @@ -14,6 +14,8 @@ package com.android.systemui.statusbar.policy; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -44,7 +46,11 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository; +import com.android.systemui.statusbar.policy.bluetooth.FakeBluetoothRepository; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -69,6 +75,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { private DumpManager mMockDumpManager; private BluetoothControllerImpl mBluetoothControllerImpl; private BluetoothAdapter mMockAdapter; + private final FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags(); private List<CachedBluetoothDevice> mDevices; @@ -89,17 +96,26 @@ public class BluetoothControllerImplTest extends SysuiTestCase { .thenReturn(mock(LocalBluetoothProfileManager.class)); mMockDumpManager = mock(DumpManager.class); - mBluetoothControllerImpl = new BluetoothControllerImpl(mContext, + BluetoothRepository bluetoothRepository = + new FakeBluetoothRepository(mMockBluetoothManager); + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true); + + mBluetoothControllerImpl = new BluetoothControllerImpl( + mContext, + mFakeFeatureFlags, mUserTracker, mMockDumpManager, mock(BluetoothLogger.class), + bluetoothRepository, mTestableLooper.getLooper(), mMockBluetoothManager, mMockAdapter); } @Test - public void testNoConnectionWithDevices() { + public void testNoConnectionWithDevices_repoFlagOff() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false); + CachedBluetoothDevice device = mock(CachedBluetoothDevice.class); when(device.isConnected()).thenReturn(true); when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED); @@ -113,7 +129,39 @@ public class BluetoothControllerImplTest extends SysuiTestCase { } @Test - public void testOnServiceConnected_updatesConnectionState() { + public void testNoConnectionWithDevices_repoFlagOn() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true); + + CachedBluetoothDevice device = mock(CachedBluetoothDevice.class); + when(device.isConnected()).thenReturn(true); + when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED); + + mDevices.add(device); + when(mMockLocalAdapter.getConnectionState()) + .thenReturn(BluetoothAdapter.STATE_DISCONNECTED); + + mBluetoothControllerImpl.onConnectionStateChanged(null, + BluetoothAdapter.STATE_DISCONNECTED); + + assertTrue(mBluetoothControllerImpl.isBluetoothConnected()); + } + + @Test + public void testOnServiceConnected_updatesConnectionState_repoFlagOff() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false); + + when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); + + mBluetoothControllerImpl.onServiceConnected(); + + assertTrue(mBluetoothControllerImpl.isBluetoothConnecting()); + assertFalse(mBluetoothControllerImpl.isBluetoothConnected()); + } + + @Test + public void testOnServiceConnected_updatesConnectionState_repoFlagOn() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true); + when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING); mBluetoothControllerImpl.onServiceConnected(); @@ -123,6 +171,46 @@ public class BluetoothControllerImplTest extends SysuiTestCase { } @Test + public void getConnectedDevices_onlyReturnsConnected_repoFlagOff() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false); + + CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class); + when(device1Disconnected.isConnected()).thenReturn(false); + mDevices.add(device1Disconnected); + + CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class); + when(device2Connected.isConnected()).thenReturn(true); + mDevices.add(device2Connected); + + mBluetoothControllerImpl.onDeviceAdded(device1Disconnected); + mBluetoothControllerImpl.onDeviceAdded(device2Connected); + + assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1); + assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0)) + .isEqualTo(device2Connected); + } + + @Test + public void getConnectedDevices_onlyReturnsConnected_repoFlagOn() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true); + + CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class); + when(device1Disconnected.isConnected()).thenReturn(false); + mDevices.add(device1Disconnected); + + CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class); + when(device2Connected.isConnected()).thenReturn(true); + mDevices.add(device2Connected); + + mBluetoothControllerImpl.onDeviceAdded(device1Disconnected); + mBluetoothControllerImpl.onDeviceAdded(device2Connected); + + assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1); + assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0)) + .isEqualTo(device2Connected); + } + + @Test public void testOnBluetoothStateChange_updatesBluetoothState() { mBluetoothControllerImpl.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF); @@ -147,8 +235,9 @@ public class BluetoothControllerImplTest extends SysuiTestCase { } @Test - public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection() - throws Exception { + public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOff() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false); + BluetoothController.Callback callback = mock(BluetoothController.Callback.class); mBluetoothControllerImpl.addCallback(callback); @@ -168,6 +257,29 @@ public class BluetoothControllerImplTest extends SysuiTestCase { } @Test + public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOn() { + mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true); + + BluetoothController.Callback callback = mock(BluetoothController.Callback.class); + mBluetoothControllerImpl.addCallback(callback); + + assertFalse(mBluetoothControllerImpl.isBluetoothConnected()); + CachedBluetoothDevice device = mock(CachedBluetoothDevice.class); + mDevices.add(device); + when(device.isConnected()).thenReturn(true); + when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED); + reset(callback); + mBluetoothControllerImpl.onAclConnectionStateChanged(device, + BluetoothProfile.STATE_CONNECTED); + + mTestableLooper.processAllMessages(); + + assertTrue(mBluetoothControllerImpl.isBluetoothConnected()); + verify(callback, atLeastOnce()).onBluetoothStateChange(anyBoolean()); + } + + + @Test public void testOnActiveDeviceChanged_updatesAudioActive() { assertFalse(mBluetoothControllerImpl.isBluetoothAudioActive()); assertFalse(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt new file mode 100644 index 000000000000..6f40f15ca00a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.statusbar.policy.bluetooth + +import android.bluetooth.BluetoothProfile +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothAdapter +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class BluetoothRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: BluetoothRepositoryImpl + + private lateinit var scheduler: TestCoroutineScheduler + private lateinit var dispatcher: TestDispatcher + private lateinit var testScope: TestScope + + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter) + + scheduler = TestCoroutineScheduler() + dispatcher = StandardTestDispatcher(scheduler) + testScope = TestScope(dispatcher) + + underTest = + BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager) + } + + @Test + fun fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() { + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + + val status = fetchConnectionStatus(currentDevices = emptyList()) + + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING) + } + + @Test + fun fetchConnectionStatusInBackground_currentDevicesEmpty_nullManager_maxStateIsDisconnected() { + // This CONNECTING state should be unused because localBluetoothManager is null + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + underTest = + BluetoothRepositoryImpl( + testScope.backgroundScope, + dispatcher, + localBluetoothManager = null, + ) + + val status = fetchConnectionStatus(currentDevices = emptyList()) + + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED) + } + + @Test + fun fetchConnectionStatusInBackground_managerStateLargerThanDeviceStates_maxStateIsManager() { + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + val device1 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED) + } + val device2 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED) + } + + val status = fetchConnectionStatus(currentDevices = listOf(device1, device2)) + + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING) + } + + @Test + fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() { + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED) + val device = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + } + + val status = fetchConnectionStatus(currentDevices = listOf(device)) + + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING) + } + + @Test + fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() { + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED) + + val device1 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + whenever(it.isConnected).thenReturn(false) + } + val device2 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED) + whenever(it.isConnected).thenReturn(true) + } + + val status = fetchConnectionStatus(currentDevices = listOf(device1, device2)) + + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTED) + } + + @Test + fun fetchConnectionStatusInBackground_devicesNotConnected_maxStateIsDisconnected() { + whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + + // WHEN the devices say their state is CONNECTED but [isConnected] is false + val device1 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED) + whenever(it.isConnected).thenReturn(false) + } + val device2 = + mock<CachedBluetoothDevice>().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED) + whenever(it.isConnected).thenReturn(false) + } + + val status = fetchConnectionStatus(currentDevices = listOf(device1, device2)) + + // THEN the max state is DISCONNECTED + assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED) + } + + @Test + fun fetchConnectionStatusInBackground_currentDevicesEmpty_connectedDevicesEmpty() { + val status = fetchConnectionStatus(currentDevices = emptyList()) + + assertThat(status.connectedDevices).isEmpty() + } + + @Test + fun fetchConnectionStatusInBackground_oneCurrentDeviceDisconnected_connectedDevicesEmpty() { + val device = + mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) } + + val status = fetchConnectionStatus(currentDevices = listOf(device)) + + assertThat(status.connectedDevices).isEmpty() + } + + @Test + fun fetchConnectionStatusInBackground_oneCurrentDeviceConnected_connectedDevicesHasDevice() { + val device = + mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) } + + val status = fetchConnectionStatus(currentDevices = listOf(device)) + + assertThat(status.connectedDevices).isEqualTo(listOf(device)) + } + + @Test + fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() { + val device1Connected = + mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) } + val device2Disconnected = + mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) } + val device3Connected = + mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) } + + val status = + fetchConnectionStatus( + currentDevices = listOf(device1Connected, device2Disconnected, device3Connected) + ) + + assertThat(status.connectedDevices).isEqualTo(listOf(device1Connected, device3Connected)) + } + + private fun fetchConnectionStatus( + currentDevices: Collection<CachedBluetoothDevice> + ): ConnectionStatusModel { + var receivedStatus: ConnectionStatusModel? = null + underTest.fetchConnectionStatusInBackground(currentDevices) { status -> + receivedStatus = status + } + scheduler.runCurrent() + return receivedStatus!! + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt new file mode 100644 index 000000000000..d8c0f777d4cc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.statusbar.policy.bluetooth + +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope + +/** + * Fake [BluetoothRepository] that delegates to the real [BluetoothRepositoryImpl]. + * + * We only need this because [BluetoothRepository] is called from Java, which can't use [TestScope], + * [StandardTestDispatcher], etc. to create a test version of the repo. This class uses those test + * items under-the-hood so Java classes can indirectly access them. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class FakeBluetoothRepository(localBluetoothManager: LocalBluetoothManager) : BluetoothRepository { + + private val scheduler = TestCoroutineScheduler() + private val dispatcher = StandardTestDispatcher(scheduler) + private val testScope = TestScope(dispatcher) + + private val impl = + BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager) + + override fun fetchConnectionStatusInBackground( + currentDevices: Collection<CachedBluetoothDevice>, + callback: ConnectionStatusFetchedCallback + ) { + impl.fetchConnectionStatusInBackground(currentDevices, callback) + scheduler.runCurrent() + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 7463061256b7..a324b2ff88e8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -2551,6 +2551,9 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ @Override public void attachAccessibilityOverlayToWindow(int accessibilityWindowId, SurfaceControl sc) throws RemoteException { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setTrustedOverlay(sc, true).apply(); + t.close(); synchronized (mLock) { RemoteAccessibilityConnection connection = mA11yWindowManager.getConnectionLocked( diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index e894f1c2879e..2d1290caf32d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -5329,9 +5329,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mA11yOverlayLayers.remove(displayId); return; } - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - transaction.reparent(sc, parent); - transaction.apply(); - transaction.close(); + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(sc, parent).setTrustedOverlay(sc, true).apply(); + t.close(); } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index d290c3611f78..fb94af65513f 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1581,6 +1581,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // TODO(b/266379948): Ideally wait for PCC request to finish for a while more // (say 100ms) before proceeding further on. + processResponseLockedForPcc(response, response.getClientState(), requestFlags); + } + + + @GuardedBy("mLock") + private void processResponseLockedForPcc(@NonNull FillResponse response, + @Nullable Bundle newClientState, int flags) { if (DBG) { Slog.d(TAG, "DBG: Initial response: " + response); } @@ -1588,12 +1595,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState response = getEffectiveFillResponse(response); if (isEmptyResponse(response)) { // Treat it as a null response. - processNullResponseLocked(requestId, requestFlags); + processNullResponseLocked( + response != null ? response.getRequestId() : 0, + flags); + return; } if (DBG) { Slog.d(TAG, "DBG: Processed response: " + response); } - processResponseLocked(response, null, requestFlags); + processResponseLocked(response, newClientState, flags); } } @@ -2490,7 +2500,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (sDebug) Slog.d(TAG, "Updating client state from auth dataset"); mClientState = newClientState; } - final Dataset dataset = (Dataset) result; + Dataset dataset = (Dataset) result; + FillResponse temp = new FillResponse.Builder().addDataset(dataset).build(); + temp = getEffectiveFillResponse(temp); + dataset = temp.getDatasets().get(0); final Dataset oldDataset = authenticatedResponse.getDatasets().get(datasetIdx); if (!isAuthResultDatasetEphemeral(oldDataset, data)) { authenticatedResponse.getDatasets().set(datasetIdx, dataset); @@ -4665,10 +4678,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState setViewStatesLocked(oldResponse, ViewState.STATE_INITIAL, true); // Move over the id newResponse.setRequestId(oldResponse.getRequestId()); - // Replace the old response - mResponses.put(newResponse.getRequestId(), newResponse); // Now process the new response - processResponseLocked(newResponse, newClientState, 0); + processResponseLockedForPcc(newResponse, newClientState, 0); } @GuardedBy("mLock") diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a888a0b5d7d9..5d3bb31bc31f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2497,13 +2497,10 @@ public class ActivityManagerService extends IActivityManager.Stub final File systemDir = SystemServiceManager.ensureSystemDir(); // TODO: Move creation of battery stats service outside of activity manager service. - mBatteryStatsService = new BatteryStatsService(systemContext, systemDir, - BackgroundThread.get().getHandler()); - mBatteryStatsService.getActiveStatistics().readLocked(); - mBatteryStatsService.scheduleWriteToDisk(); + mBatteryStatsService = BatteryStatsService.create(systemContext, systemDir, + BackgroundThread.getHandler(), this); mOnBattery = DEBUG_POWER ? true : mBatteryStatsService.getActiveStatistics().getIsOnBattery(); - mBatteryStatsService.getActiveStatistics().setCallback(this); mOomAdjProfiler.batteryPowerChanged(mOnBattery); mProcessStats = new ProcessStatsService(this, new File(systemDir, "procstats")); @@ -7514,7 +7511,31 @@ public class ActivityManagerService extends IActivityManager.Stub "registerUidObserver"); } mUidObserverController.register(observer, which, cutpoint, callingPackage, - Binder.getCallingUid()); + Binder.getCallingUid(), /*uids*/null); + } + + /** + * Registers a UidObserver with a uid filter. + * + * @param observer The UidObserver implementation to register. + * @param which A bitmask of events to observe. See ActivityManager.UID_OBSERVER_*. + * @param cutpoint The cutpoint for onUidStateChanged events. When the state crosses this + * threshold in either direction, onUidStateChanged will be called. + * @param callingPackage The name of the calling package. + * @param uids A list of uids to watch. If all uids are to be watched, use + * registerUidObserver instead. + * @throws RemoteException + * @return Returns A binder token identifying the UidObserver registration. + */ + @Override + public IBinder registerUidObserverForUids(IUidObserver observer, int which, int cutpoint, + String callingPackage, int[] uids) { + if (!hasUsageStatsPermission(callingPackage)) { + enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, + "registerUidObserver"); + } + return mUidObserverController.register(observer, which, cutpoint, callingPackage, + Binder.getCallingUid(), uids); } @Override @@ -7522,6 +7543,40 @@ public class ActivityManagerService extends IActivityManager.Stub mUidObserverController.unregister(observer); } + /** + * Adds a uid to the list of uids that a UidObserver will receive updates about. + * + * @param observerToken The binder token identifying the UidObserver registration. + * @param callingPackage The name of the calling package. + * @param uid The uid to watch. + * @throws RemoteException + */ + @Override + public void addUidToObserver(IBinder observerToken, String callingPackage, int uid) { + if (!hasUsageStatsPermission(callingPackage)) { + enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, + "registerUidObserver"); + } + mUidObserverController.addUidToObserver(observerToken, uid); + } + + /** + * Removes a uid from the list of uids that a UidObserver will receive updates about. + * + * @param observerToken The binder token identifying the UidObserver registration. + * @param callingPackage The name of the calling package. + * @param uid The uid to stop watching. + * @throws RemoteException + */ + @Override + public void removeUidFromObserver(IBinder observerToken, String callingPackage, int uid) { + if (!hasUsageStatsPermission(callingPackage)) { + enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, + "registerUidObserver"); + } + mUidObserverController.removeUidFromObserver(observerToken, uid); + } + @Override public boolean isUidActive(int uid, String callingPackage) { if (!hasUsageStatsPermission(callingPackage)) { @@ -18616,7 +18671,7 @@ public class ActivityManagerService extends IActivityManager.Stub int which, int cutpoint, @NonNull String callingPackage) { mNetworkPolicyUidObserver = observer; mUidObserverController.register(observer, which, cutpoint, callingPackage, - Binder.getCallingUid()); + Binder.getCallingUid(), /*uids*/null); } @Override diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 6360e2a0d089..36da888dbc2a 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -399,6 +399,20 @@ public final class BatteryStatsService extends IBatteryStats.Stub mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler); } + /** + * Creates an instance of BatteryStatsService and restores data from stored state. + */ + public static BatteryStatsService create(Context context, File systemDir, Handler handler, + BatteryStatsImpl.BatteryCallback callback) { + BatteryStatsService service = new BatteryStatsService(context, systemDir, handler); + service.mStats.setCallback(callback); + synchronized (service.mStats) { + service.mStats.readLocked(); + } + service.scheduleWriteToDisk(); + return service; + } + public void publish() { LocalServices.addService(BatteryStatsInternal.class, new LocalService()); ServiceManager.addService(BatteryStats.SERVICE_NAME, asBinder()); diff --git a/services/core/java/com/android/server/am/UidObserverController.java b/services/core/java/com/android/server/am/UidObserverController.java index 790cc7b87f80..5e41dcd0009e 100644 --- a/services/core/java/com/android/server/am/UidObserverController.java +++ b/services/core/java/com/android/server/am/UidObserverController.java @@ -27,7 +27,9 @@ import android.app.ActivityManager; import android.app.ActivityManagerProto; import android.app.IUidObserver; import android.content.pm.PackageManager; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; @@ -43,6 +45,8 @@ import com.android.server.am.ActivityManagerServiceDumpProcessesProto.UidObserve import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.UUID; public class UidObserverController { /** If a UID observer takes more than this long, send a WTF. */ @@ -79,14 +83,19 @@ public class UidObserverController { mValidateUids = new ActiveUids(null /* service */, false /* postChangesToAtm */); } - void register(@NonNull IUidObserver observer, int which, int cutpoint, - @NonNull String callingPackage, int callingUid) { + IBinder register(@NonNull IUidObserver observer, int which, int cutpoint, + @NonNull String callingPackage, int callingUid, @Nullable int[] uids) { + IBinder token = new Binder("UidObserver-" + callingPackage + "-" + + UUID.randomUUID().toString()); + synchronized (mLock) { mUidObservers.register(observer, new UidObserverRegistration(callingUid, callingPackage, which, cutpoint, ActivityManager.checkUidPermission(INTERACT_ACROSS_USERS_FULL, callingUid) - == PackageManager.PERMISSION_GRANTED)); + == PackageManager.PERMISSION_GRANTED, uids, token)); } + + return token; } void unregister(@NonNull IUidObserver observer) { @@ -95,6 +104,42 @@ public class UidObserverController { } } + void addUidToObserver(@NonNull IBinder observerToken, int uid) { + synchronized (mLock) { + int i = mUidObservers.beginBroadcast(); + while (i-- > 0) { + var reg = (UidObserverRegistration) mUidObservers.getBroadcastCookie(i); + if (reg.getToken().equals(observerToken)) { + reg.addUid(uid); + break; + } + + if (i == 0) { + Slog.e(TAG_UID_OBSERVERS, "Unable to find UidObserver by token"); + } + } + mUidObservers.finishBroadcast(); + } + } + + void removeUidFromObserver(@NonNull IBinder observerToken, int uid) { + synchronized (mLock) { + int i = mUidObservers.beginBroadcast(); + while (i-- > 0) { + var reg = (UidObserverRegistration) mUidObservers.getBroadcastCookie(i); + if (reg.getToken().equals(observerToken)) { + reg.removeUid(uid); + break; + } + + if (i == 0) { + Slog.e(TAG_UID_OBSERVERS, "Unable to find UidObserver by token"); + } + } + mUidObservers.finishBroadcast(); + } + } + int enqueueUidChange(@Nullable ChangeRecord currentRecord, int uid, int change, int procState, int procAdj, long procStateSeq, int capability, boolean ephemeral) { synchronized (mLock) { @@ -257,6 +302,10 @@ public class UidObserverController { final ChangeRecord item = mActiveUidChanges[j]; final long start = SystemClock.uptimeMillis(); final int change = item.change; + // Is the observer watching this uid? + if (!reg.isWatchingUid(item.uid)) { + continue; + } // Does the user have permission? Don't send a non user UID change otherwise if (UserHandle.getUserId(item.uid) != UserHandle.getUserId(reg.mUid) && !reg.mCanInteractAcrossUsers) { @@ -450,6 +499,8 @@ public class UidObserverController { private final int mWhich; private final int mCutpoint; private final boolean mCanInteractAcrossUsers; + private final IBinder mToken; + private int[] mUids; /** * Total # of callback calls that took more than {@link #SLOW_UID_OBSERVER_THRESHOLD_MS}. @@ -481,16 +532,94 @@ public class UidObserverController { }; UidObserverRegistration(int uid, @NonNull String pkg, int which, int cutpoint, - boolean canInteractAcrossUsers) { + boolean canInteractAcrossUsers, @Nullable int[] uids, @NonNull IBinder token) { this.mUid = uid; this.mPkg = pkg; this.mWhich = which; this.mCutpoint = cutpoint; this.mCanInteractAcrossUsers = canInteractAcrossUsers; + + if (uids != null) { + this.mUids = uids.clone(); + Arrays.sort(this.mUids); + } else { + this.mUids = null; + } + + this.mToken = token; + mLastProcStates = cutpoint >= ActivityManager.MIN_PROCESS_STATE ? new SparseIntArray() : null; } + boolean isWatchingUid(int uid) { + if (mUids == null) { + return true; + } + + return Arrays.binarySearch(mUids, uid) != -1; + } + + void addUid(int uid) { + if (mUids == null) { + return; + } + + int[] temp = mUids; + mUids = new int[temp.length + 1]; + boolean inserted = false; + for (int i = 0; i < temp.length; i++) { + if (!inserted) { + if (temp[i] < uid) { + mUids[i] = temp[i]; + } else if (temp[i] == uid) { + // Duplicate uid, no-op and fallback to the previous array + mUids = temp; + return; + } else { + mUids[i] = uid; + mUids[i + 1] = temp[i]; + inserted = true; + } + } else { + mUids[i + 1] = temp[i]; + } + } + + if (!inserted) { + mUids[temp.length] = uid; + } + } + + void removeUid(int uid) { + if (mUids == null || mUids.length == 0) { + return; + } + + int[] temp = mUids; + mUids = new int[temp.length - 1]; + boolean removed = false; + for (int i = 0; i < temp.length; i++) { + if (!removed) { + if (temp[i] == uid) { + removed = true; + } else if (i == temp.length - 1) { + // Uid not found, no-op and fallback to the previous array + mUids = temp; + return; + } else { + mUids[i] = temp[i]; + } + } else { + mUids[i - 1] = temp[i]; + } + } + } + + IBinder getToken() { + return mToken; + } + void dump(@NonNull PrintWriter pw, @NonNull IUidObserver observer) { pw.print(" "); UserHandle.formatUid(pw, mUid); diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index a110169ac8c2..1f3795a12b76 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -44,6 +44,7 @@ import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; import static android.app.AppOpsManager.OP_RECORD_AUDIO_SANDBOXED; +import static android.app.AppOpsManager.OP_RUN_ANY_IN_BACKGROUND; import static android.app.AppOpsManager.OP_VIBRATE; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; @@ -1769,6 +1770,11 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void setUidMode(int code, int uid, int mode) { setUidMode(code, uid, mode, null); + if (code == OP_RUN_ANY_IN_BACKGROUND) { + // TODO (b/280869337): Remove this once we have the required data. + Slog.wtfStack(TAG, "setUidMode called for RUN_ANY_IN_BACKGROUND by uid: " + + UserHandle.formatUid(Binder.getCallingUid())); + } } private void setUidMode(int code, int uid, int mode, @@ -1944,6 +1950,17 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void setMode(int code, int uid, @NonNull String packageName, int mode) { setMode(code, uid, packageName, mode, null); + final int callingUid = Binder.getCallingUid(); + if (code == OP_RUN_ANY_IN_BACKGROUND && mode != MODE_ALLOWED) { + // TODO (b/280869337): Remove this once we have the required data. + final String callingPackage = ArrayUtils.firstOrNull(getPackagesForUid(callingUid)); + Slog.wtfStack(TAG, + "RUN_ANY_IN_BACKGROUND for package " + packageName + " changed to mode: " + + modeToName(mode) + " via setMode. Calling package: " + callingPackage + + ", calling uid: " + UserHandle.formatUid(callingUid) + + ", calling pid: " + Binder.getCallingPid() + + ", system pid: " + Process.myPid()); + } } void setMode(int code, int uid, @NonNull String packageName, int mode, diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 773df3720ed3..1d8bef1c6732 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1031,7 +1031,8 @@ public class AudioDeviceInventory { synchronized (rolesMap) { Pair<Integer, Integer> key = new Pair<>(useCase, role); if (!rolesMap.containsKey(key)) { - return AudioSystem.SUCCESS; + // trying to remove a role for a device that wasn't set + return AudioSystem.BAD_VALUE; } List<AudioDeviceAttributes> roleDevices = rolesMap.get(key); List<AudioDeviceAttributes> appliedDevices = new ArrayList<>(); diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index aece17e7a6e6..4aa256d3ce74 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -78,6 +78,23 @@ public class SoundDoseHelper { /*package*/ static final String ACTION_CHECK_MUSIC_ACTIVE = "com.android.server.audio.action.CHECK_MUSIC_ACTIVE"; + /** + * Property to force the index based safe volume warnings. Note that usually when the + * CSD warnings are active the safe volume warnings are deactivated. In combination with + * {@link SoundDoseHelper#SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE} both approaches can be active + * at the same time. + */ + private static final String SYSTEM_PROPERTY_SAFEMEDIA_FORCE = "audio.safemedia.force"; + /** Property for bypassing the index based safe volume approach. */ + private static final String SYSTEM_PROPERTY_SAFEMEDIA_BYPASS = "audio.safemedia.bypass"; + /** + * Property to force the CSD warnings. Note that usually when the CSD warnings are active the + * safe volume warnings are deactivated. In combination with + * {@link SoundDoseHelper#SYSTEM_PROPERTY_SAFEMEDIA_FORCE} both approaches can be active + * at the same time. + */ + private static final String SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE = "audio.safemedia.csd.force"; + // mSafeMediaVolumeState indicates whether the media volume is limited over headphones. // It is SAFE_MEDIA_VOLUME_NOT_CONFIGURED at boot time until a network service is connected // or the configure time is elapsed. It is then set to SAFE_MEDIA_VOLUME_ACTIVE or @@ -830,56 +847,64 @@ public class SoundDoseHelper { } private void onConfigureSafeMedia(boolean force, String caller) { + updateCsdEnabled(caller); + synchronized (mSafeMediaVolumeStateLock) { int mcc = mContext.getResources().getConfiguration().mcc; if ((mMcc != mcc) || ((mMcc == 0) && force)) { mSafeMediaVolumeIndex = mContext.getResources().getInteger( com.android.internal.R.integer.config_safe_media_volume_index) * 10; - initSafeMediaVolumeIndex(); - boolean safeMediaVolumeEnabled = - SystemProperties.getBoolean("audio.safemedia.force", false) - || mContext.getResources().getBoolean( - com.android.internal.R.bool.config_safe_media_volume_enabled); - boolean safeMediaVolumeBypass = - SystemProperties.getBoolean("audio.safemedia.bypass", false); - - // The persisted state is either "disabled" or "active": this is the state applied - // next time we boot and cannot be "inactive" - int persistedState; - if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) { - persistedState = SAFE_MEDIA_VOLUME_ACTIVE; - // The state can already be "inactive" here if the user has forced it before - // the 30 seconds timeout for forced configuration. In this case we don't reset - // it to "active". - if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) { - if (mMusicActiveMs == 0) { - mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE; - enforceSafeMediaVolume(caller); - } else { - // We have existing playback time recorded, already confirmed. - mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; - mLastMusicActiveTimeMs = 0; - } - } - } else { - persistedState = SAFE_MEDIA_VOLUME_DISABLED; - mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED; - } + updateSafeMediaVolume_l(caller); + mMcc = mcc; - mAudioHandler.sendMessageAtTime( - mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE, - persistedState, /*arg2=*/0, - /*obj=*/null), /*delay=*/0); } + } + } - updateCsdEnabled(caller); + @GuardedBy("mSafeMediaVolumeStateLock") + private void updateSafeMediaVolume_l(String caller) { + boolean safeMediaVolumeEnabled = + SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_FORCE, false) + || (mContext.getResources().getBoolean( + com.android.internal.R.bool.config_safe_media_volume_enabled) + && !mEnableCsd.get()); + boolean safeMediaVolumeBypass = + SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_BYPASS, false); + + // The persisted state is either "disabled" or "active": this is the state applied + // next time we boot and cannot be "inactive" + int persistedState; + if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) { + persistedState = SAFE_MEDIA_VOLUME_ACTIVE; + // The state can already be "inactive" here if the user has forced it before + // the 30 seconds timeout for forced configuration. In this case we don't reset + // it to "active". + if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) { + if (mMusicActiveMs == 0) { + mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE; + enforceSafeMediaVolume(caller); + } else { + // We have existing playback time recorded, already confirmed. + mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; + mLastMusicActiveTimeMs = 0; + } + } + } else { + persistedState = SAFE_MEDIA_VOLUME_DISABLED; + mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED; } + + mAudioHandler.sendMessageAtTime( + mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE, + persistedState, /*arg2=*/0, + /*obj=*/null), /*delay=*/0); } private void updateCsdEnabled(String caller) { - boolean newEnableCsd = SystemProperties.getBoolean("audio.safemedia.force", false); + boolean newEnableCsd = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE, + false); if (!newEnableCsd) { final String featureFlagEnableCsdValue = DeviceConfig.getProperty( DeviceConfig.NAMESPACE_MEDIA, @@ -895,6 +920,10 @@ public class SoundDoseHelper { if (mEnableCsd.compareAndSet(!newEnableCsd, newEnableCsd)) { Log.i(TAG, caller + ": enable CSD " + newEnableCsd); initCsd(); + + synchronized (mSafeMediaVolumeStateLock) { + updateSafeMediaVolume_l(caller); + } } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 9b1a80bed17b..0bd6dffed024 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -1330,7 +1330,11 @@ public class LauncherAppsService extends SystemService { @Override public PendingIntent getActivityLaunchIntent(String callingPackage, ComponentName component, UserHandle user) { - ensureShortcutPermission(callingPackage); + if (mContext.checkPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS, + injectBinderCallingPid(), injectBinderCallingUid()) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Permission START_TASKS_FROM_RECENTS required"); + } if (!canAccessProfile(user.getIdentifier(), "Cannot start activity")) { throw new ActivityNotFoundException("Activity could not be found"); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index dc56def09ee8..d7eff52af9b4 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3096,18 +3096,21 @@ public class PhoneWindowManager implements WindowManagerPolicy { } int screenDisplayId = displayId < 0 ? DEFAULT_DISPLAY : displayId; + float minLinearBrightness = mPowerManager.getBrightnessConstraint( + PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM); + float maxLinearBrightness = mPowerManager.getBrightnessConstraint( + PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM); float linearBrightness = mDisplayManager.getBrightness(screenDisplayId); float gammaBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness); float adjustedGammaBrightness = gammaBrightness + 1f / BRIGHTNESS_STEPS * direction; - + adjustedGammaBrightness = MathUtils.constrain(adjustedGammaBrightness, 0f, + 1f); float adjustedLinearBrightness = BrightnessUtils.convertGammaToLinear( adjustedGammaBrightness); - - adjustedLinearBrightness = MathUtils.constrain(adjustedLinearBrightness, 0f, - 1f); - + adjustedLinearBrightness = MathUtils.constrain(adjustedLinearBrightness, + minLinearBrightness, maxLinearBrightness); mDisplayManager.setBrightness(screenDisplayId, adjustedLinearBrightness); startActivityAsUser(new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG), diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 1c89ec445426..3c5ad2acc8a0 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -1573,7 +1573,7 @@ public class TrustManagerService extends SystemService { @Override public void reportUserMayRequestUnlock(int userId) throws RemoteException { enforceReportPermission(); - mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId).sendToTarget(); + mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /*arg2=*/ 0).sendToTarget(); } @Override diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 6f640700949e..84d3a216ac91 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2789,6 +2789,20 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } /** + * Returns true if there is a static wallpaper on the specified screen. With which=FLAG_LOCK, + * always return false if the lockscreen doesn't run its own wallpaper engine. + */ + @Override + public boolean isStaticWallpaper(int which) { + synchronized (mLock) { + WallpaperData wallpaperData = (which == FLAG_LOCK ? mLockWallpaperMap : mWallpaperMap) + .get(mCurrentUserId); + if (wallpaperData == null) return false; + return mImageWallpaper.equals(wallpaperData.wallpaperComponent); + } + } + + /** * Sets wallpaper dim amount for the calling UID. This applies to all destinations (home, lock) * with an active wallpaper engine. * diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index c40d72c3746e..6b90181cff0b 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -1159,9 +1159,10 @@ class ActivityClientController extends IActivityClientController.Stub { } return; } + transition.collect(topFocusedRootTask); + executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask); r.mTransitionController.requestStartTransition(transition, topFocusedRootTask, null /* remoteTransition */, null /* displayChange */); - executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask); transition.setReady(topFocusedRootTask, true); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 76fd69328ceb..911591c6a508 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -22,6 +22,7 @@ import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE; import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS; import static android.view.InsetsFrameProvider.SOURCE_DISPLAY; import static android.view.InsetsFrameProvider.SOURCE_FRAME; +import static android.view.ViewRootImpl.CLIENT_TRANSIENT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; @@ -213,7 +214,8 @@ public class DisplayPolicy { } } - private final SystemGesturesPointerEventListener mSystemGestures; + // Will be null in client transient mode. + private SystemGesturesPointerEventListener mSystemGestures; final DecorInsets mDecorInsets; @@ -408,158 +410,162 @@ public class DisplayPolicy { final Looper looper = UiThread.getHandler().getLooper(); mHandler = new PolicyHandler(looper); // TODO(b/181821798) Migrate SystemGesturesPointerEventListener to use window context. - mSystemGestures = new SystemGesturesPointerEventListener(mUiContext, mHandler, - new SystemGesturesPointerEventListener.Callbacks() { + if (!CLIENT_TRANSIENT) { + SystemGesturesPointerEventListener.Callbacks gesturesPointerEventCallbacks = + new SystemGesturesPointerEventListener.Callbacks() { - private static final long MOUSE_GESTURE_DELAY_MS = 500; + private static final long MOUSE_GESTURE_DELAY_MS = 500; - private Runnable mOnSwipeFromLeft = this::onSwipeFromLeft; - private Runnable mOnSwipeFromTop = this::onSwipeFromTop; - private Runnable mOnSwipeFromRight = this::onSwipeFromRight; - private Runnable mOnSwipeFromBottom = this::onSwipeFromBottom; + private Runnable mOnSwipeFromLeft = this::onSwipeFromLeft; + private Runnable mOnSwipeFromTop = this::onSwipeFromTop; + private Runnable mOnSwipeFromRight = this::onSwipeFromRight; + private Runnable mOnSwipeFromBottom = this::onSwipeFromBottom; - private Insets getControllableInsets(WindowState win) { - if (win == null) { - return Insets.NONE; - } - final InsetsSourceProvider provider = win.getControllableInsetProvider(); - if (provider == null) { - return Insets.NONE; - } - return provider.getSource().calculateInsets(win.getBounds(), - true /* ignoreVisibility */); + private Insets getControllableInsets(WindowState win) { + if (win == null) { + return Insets.NONE; } - - @Override - public void onSwipeFromTop() { - synchronized (mLock) { - requestTransientBars(mTopGestureHost, - getControllableInsets(mTopGestureHost).top > 0); - } + final InsetsSourceProvider provider = win.getControllableInsetProvider(); + if (provider == null) { + return Insets.NONE; } + return provider.getSource().calculateInsets(win.getBounds(), + true /* ignoreVisibility */); + } - @Override - public void onSwipeFromBottom() { - synchronized (mLock) { - requestTransientBars(mBottomGestureHost, - getControllableInsets(mBottomGestureHost).bottom > 0); - } + @Override + public void onSwipeFromTop() { + synchronized (mLock) { + requestTransientBars(mTopGestureHost, + getControllableInsets(mTopGestureHost).top > 0); } + } - private boolean allowsSideSwipe(Region excludedRegion) { - return mNavigationBarAlwaysShowOnSideGesture - && !mSystemGestures.currentGestureStartedInRegion(excludedRegion); + @Override + public void onSwipeFromBottom() { + synchronized (mLock) { + requestTransientBars(mBottomGestureHost, + getControllableInsets(mBottomGestureHost).bottom > 0); } + } - @Override - public void onSwipeFromRight() { - final Region excludedRegion = Region.obtain(); - synchronized (mLock) { - mDisplayContent.calculateSystemGestureExclusion( - excludedRegion, null /* outUnrestricted */); - final boolean hasWindow = - getControllableInsets(mRightGestureHost).right > 0; - if (hasWindow || allowsSideSwipe(excludedRegion)) { - requestTransientBars(mRightGestureHost, hasWindow); - } - } - excludedRegion.recycle(); - } + private boolean allowsSideSwipe(Region excludedRegion) { + return mNavigationBarAlwaysShowOnSideGesture + && !mSystemGestures.currentGestureStartedInRegion(excludedRegion); + } - @Override - public void onSwipeFromLeft() { - final Region excludedRegion = Region.obtain(); - synchronized (mLock) { - mDisplayContent.calculateSystemGestureExclusion( - excludedRegion, null /* outUnrestricted */); - final boolean hasWindow = - getControllableInsets(mLeftGestureHost).left > 0; - if (hasWindow || allowsSideSwipe(excludedRegion)) { - requestTransientBars(mLeftGestureHost, hasWindow); - } + @Override + public void onSwipeFromRight() { + final Region excludedRegion = Region.obtain(); + synchronized (mLock) { + mDisplayContent.calculateSystemGestureExclusion( + excludedRegion, null /* outUnrestricted */); + final boolean hasWindow = + getControllableInsets(mRightGestureHost).right > 0; + if (hasWindow || allowsSideSwipe(excludedRegion)) { + requestTransientBars(mRightGestureHost, hasWindow); } - excludedRegion.recycle(); } + excludedRegion.recycle(); + } - @Override - public void onFling(int duration) { - if (mService.mPowerManagerInternal != null) { - mService.mPowerManagerInternal.setPowerBoost( - Boost.INTERACTION, duration); + @Override + public void onSwipeFromLeft() { + final Region excludedRegion = Region.obtain(); + synchronized (mLock) { + mDisplayContent.calculateSystemGestureExclusion( + excludedRegion, null /* outUnrestricted */); + final boolean hasWindow = + getControllableInsets(mLeftGestureHost).left > 0; + if (hasWindow || allowsSideSwipe(excludedRegion)) { + requestTransientBars(mLeftGestureHost, hasWindow); } } + excludedRegion.recycle(); + } - @Override - public void onDebug() { - // no-op + @Override + public void onFling(int duration) { + if (mService.mPowerManagerInternal != null) { + mService.mPowerManagerInternal.setPowerBoost( + Boost.INTERACTION, duration); } + } - private WindowOrientationListener getOrientationListener() { - final DisplayRotation rotation = mDisplayContent.getDisplayRotation(); - return rotation != null ? rotation.getOrientationListener() : null; - } + @Override + public void onDebug() { + // no-op + } - @Override - public void onDown() { - final WindowOrientationListener listener = getOrientationListener(); - if (listener != null) { - listener.onTouchStart(); - } - } + private WindowOrientationListener getOrientationListener() { + final DisplayRotation rotation = mDisplayContent.getDisplayRotation(); + return rotation != null ? rotation.getOrientationListener() : null; + } - @Override - public void onUpOrCancel() { - final WindowOrientationListener listener = getOrientationListener(); - if (listener != null) { - listener.onTouchEnd(); - } + @Override + public void onDown() { + final WindowOrientationListener listener = getOrientationListener(); + if (listener != null) { + listener.onTouchStart(); } + } - @Override - public void onMouseHoverAtLeft() { - mHandler.removeCallbacks(mOnSwipeFromLeft); - mHandler.postDelayed(mOnSwipeFromLeft, MOUSE_GESTURE_DELAY_MS); + @Override + public void onUpOrCancel() { + final WindowOrientationListener listener = getOrientationListener(); + if (listener != null) { + listener.onTouchEnd(); } + } - @Override - public void onMouseHoverAtTop() { - mHandler.removeCallbacks(mOnSwipeFromTop); - mHandler.postDelayed(mOnSwipeFromTop, MOUSE_GESTURE_DELAY_MS); - } + @Override + public void onMouseHoverAtLeft() { + mHandler.removeCallbacks(mOnSwipeFromLeft); + mHandler.postDelayed(mOnSwipeFromLeft, MOUSE_GESTURE_DELAY_MS); + } - @Override - public void onMouseHoverAtRight() { - mHandler.removeCallbacks(mOnSwipeFromRight); - mHandler.postDelayed(mOnSwipeFromRight, MOUSE_GESTURE_DELAY_MS); - } + @Override + public void onMouseHoverAtTop() { + mHandler.removeCallbacks(mOnSwipeFromTop); + mHandler.postDelayed(mOnSwipeFromTop, MOUSE_GESTURE_DELAY_MS); + } - @Override - public void onMouseHoverAtBottom() { - mHandler.removeCallbacks(mOnSwipeFromBottom); - mHandler.postDelayed(mOnSwipeFromBottom, MOUSE_GESTURE_DELAY_MS); - } + @Override + public void onMouseHoverAtRight() { + mHandler.removeCallbacks(mOnSwipeFromRight); + mHandler.postDelayed(mOnSwipeFromRight, MOUSE_GESTURE_DELAY_MS); + } - @Override - public void onMouseLeaveFromLeft() { - mHandler.removeCallbacks(mOnSwipeFromLeft); - } + @Override + public void onMouseHoverAtBottom() { + mHandler.removeCallbacks(mOnSwipeFromBottom); + mHandler.postDelayed(mOnSwipeFromBottom, MOUSE_GESTURE_DELAY_MS); + } - @Override - public void onMouseLeaveFromTop() { - mHandler.removeCallbacks(mOnSwipeFromTop); - } + @Override + public void onMouseLeaveFromLeft() { + mHandler.removeCallbacks(mOnSwipeFromLeft); + } - @Override - public void onMouseLeaveFromRight() { - mHandler.removeCallbacks(mOnSwipeFromRight); - } + @Override + public void onMouseLeaveFromTop() { + mHandler.removeCallbacks(mOnSwipeFromTop); + } - @Override - public void onMouseLeaveFromBottom() { - mHandler.removeCallbacks(mOnSwipeFromBottom); - } - }); - displayContent.registerPointerEventListener(mSystemGestures); + @Override + public void onMouseLeaveFromRight() { + mHandler.removeCallbacks(mOnSwipeFromRight); + } + + @Override + public void onMouseLeaveFromBottom() { + mHandler.removeCallbacks(mOnSwipeFromBottom); + } + }; + mSystemGestures = new SystemGesturesPointerEventListener(mUiContext, mHandler, + gesturesPointerEventCallbacks); + displayContent.registerPointerEventListener(mSystemGestures); + } mAppTransitionListener = new WindowManagerInternal.AppTransitionListener() { private Runnable mAppTransitionPending = () -> { @@ -645,7 +651,9 @@ public class DisplayPolicy { mContext, () -> { synchronized (mLock) { onConfigurationChanged(); - mSystemGestures.onConfigurationChanged(); + if (!CLIENT_TRANSIENT) { + mSystemGestures.onConfigurationChanged(); + } mDisplayContent.updateSystemGestureExclusion(); } }); @@ -667,7 +675,9 @@ public class DisplayPolicy { } void systemReady() { - mSystemGestures.systemReady(); + if (!CLIENT_TRANSIENT) { + mSystemGestures.systemReady(); + } if (mService.mPointerLocationEnabled) { setPointerLocationEnabled(true); } @@ -1308,7 +1318,9 @@ public class DisplayPolicy { } void onDisplayInfoChanged(DisplayInfo info) { - mSystemGestures.onDisplayInfoChanged(info); + if (!CLIENT_TRANSIENT) { + mSystemGestures.onDisplayInfoChanged(info); + } } /** @@ -1681,7 +1693,9 @@ public class DisplayPolicy { // Update the latest display size, cutout. mDisplayContent.updateDisplayInfo(); onConfigurationChanged(); - mSystemGestures.onConfigurationChanged(); + if (!CLIENT_TRANSIENT) { + mSystemGestures.onConfigurationChanged(); + } } /** @@ -1960,6 +1974,9 @@ public class DisplayPolicy { @VisibleForTesting void requestTransientBars(WindowState swipeTarget, boolean isGestureOnSystemBar) { + if (CLIENT_TRANSIENT) { + return; + } if (swipeTarget == null || !mService.mPolicy.isUserSetupComplete()) { // Swipe-up for navigation bar is disabled during setup return; @@ -2608,7 +2625,9 @@ public class DisplayPolicy { final DecorInsets.Info info = mDecorInsets.mInfoForRotation[rotation]; pw.println(prefixInner + Surface.rotationToString(rotation) + "=" + info); } - mSystemGestures.dump(pw, prefix); + if (!CLIENT_TRANSIENT) { + mSystemGestures.dump(pw, prefix); + } pw.print(prefix); pw.println("Looper state:"); mHandler.getLooper().dump(new PrintWriterPrinter(pw), prefix + " "); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index fc99f4c9eef7..0288e4bbb26e 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -832,13 +832,14 @@ final class LetterboxUiController { // Get the bounds of the "space-to-fill". The transformed bounds have the highest // priority because the activity is launched in a rotated environment. In multi-window - // mode, the task-level represents this. In fullscreen-mode, the task container does + // mode, the taskFragment-level represents this for both split-screen + // and activity-embedding. In fullscreen-mode, the task container does // (since the orientation letterbox is also applied to the task). final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); final Rect spaceToFill = transformedBounds != null ? transformedBounds : mActivityRecord.inMultiWindowMode() - ? mActivityRecord.getTask().getBounds() + ? mActivityRecord.getTaskFragment().getBounds() : mActivityRecord.getRootTask().getParent().getBounds(); // In case of translucent activities an option is to use the WindowState#getFrame() of // the first opaque activity beneath. In some cases (e.g. an opaque activity is using diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 8728c3c64715..86dbe11d5ddc 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -482,7 +482,7 @@ public final class CredentialManagerService callback, request, constructCallingAppInfo(callingPackage, userId, request.getOrigin()), - getEnabledProviders(), + getEnabledProvidersForUser(userId), CancellationSignal.fromTransport(cancelTransport), timestampBegan); addSessionLocked(userId, session); @@ -537,7 +537,7 @@ public final class CredentialManagerService getCredentialCallback, request, constructCallingAppInfo(callingPackage, userId, request.getOrigin()), - getEnabledProviders(), + getEnabledProvidersForUser(userId), CancellationSignal.fromTransport(cancelTransport), timestampBegan, prepareGetCredentialCallback); @@ -655,7 +655,7 @@ public final class CredentialManagerService request, callback, constructCallingAppInfo(callingPackage, userId, request.getOrigin()), - getEnabledProviders(), + getEnabledProvidersForUser(userId), getPrimaryProvidersForUserId(getContext(), userId), CancellationSignal.fromTransport(cancelTransport), timestampBegan); @@ -726,10 +726,13 @@ public final class CredentialManagerService "setEnabledProviders", null); + Set<String> enableProvider = new HashSet<>(providers); + enableProvider.addAll(primaryProviders); + boolean writeEnabledStatus = Settings.Secure.putStringForUser(getContext().getContentResolver(), Settings.Secure.CREDENTIAL_SERVICE, - String.join(":", providers), + String.join(":", enableProvider), userId); boolean writePrimaryStatus = @@ -804,7 +807,7 @@ public final class CredentialManagerService verifyGetProvidersPermission(); return CredentialProviderInfoFactory.getCredentialProviderServices( - mContext, userId, providerFilter, getEnabledProviders(), + mContext, userId, providerFilter, getEnabledProvidersForUser(userId), getPrimaryProvidersForUserId(mContext, userId)); } @@ -815,7 +818,7 @@ public final class CredentialManagerService final int userId = UserHandle.getCallingUserId(); return CredentialProviderInfoFactory.getCredentialProviderServicesForTesting( - mContext, userId, providerFilter, getEnabledProviders(), + mContext, userId, providerFilter, getEnabledProvidersForUser(userId), getPrimaryProvidersForUserId(mContext, userId)); } @@ -832,24 +835,26 @@ public final class CredentialManagerService } } - @SuppressWarnings("GuardedBy") // ErrorProne requires service.mLock which is the same - // this.mLock - private Set<ComponentName> getEnabledProviders() { + private Set<ComponentName> getEnabledProvidersForUser(int userId) { + final int resolvedUserId = ActivityManager.handleIncomingUser( + Binder.getCallingPid(), Binder.getCallingUid(), + userId, false, false, + "getEnabledProvidersForUser", null); + Set<ComponentName> enabledProviders = new HashSet<>(); - synchronized (mLock) { - runForUser( - (service) -> { - try { - enabledProviders.add( - service.getCredentialProviderInfo() - .getServiceInfo().getComponentName()); - } catch (NullPointerException e) { - // Safe check - Slog.e(TAG, "Skipping provider as either the providerInfo" - + " or serviceInfo is null - weird"); - } - }); + String directValue = Settings.Secure.getStringForUser( + mContext.getContentResolver(), Settings.Secure.CREDENTIAL_SERVICE, resolvedUserId); + + if (!TextUtils.isEmpty(directValue)) { + String[] components = directValue.split(":"); + for (String componentString : components) { + ComponentName component = ComponentName.unflattenFromString(componentString); + if (component != null) { + enabledProviders.add(component); + } + } } + return enabledProviders; } @@ -879,7 +884,7 @@ public final class CredentialManagerService callback, request, constructCallingAppInfo(callingPackage, userId, null), - getEnabledProviders(), + getEnabledProvidersForUser(userId), CancellationSignal.fromTransport(cancelTransport), timestampBegan); addSessionLocked(userId, session); diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 15034104b5e0..e9fa88328691 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -19,6 +19,7 @@ package com.android.server.credentials; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.credentials.CredentialOption; import android.credentials.CredentialProviderInfo; import android.credentials.GetCredentialException; import android.credentials.GetCredentialRequest; @@ -52,11 +53,22 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, CancellationSignal cancellationSignal, long startedTimestamp) { super(context, sessionCallback, lock, userId, callingUid, request, callback, - RequestInfo.TYPE_GET, callingAppInfo, enabledProviders, cancellationSignal, - startedTimestamp); + getRequestInfoFromRequest(request), callingAppInfo, enabledProviders, + cancellationSignal, startedTimestamp); mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); } + private static String getRequestInfoFromRequest(GetCredentialRequest request) { + for (CredentialOption option : request.getCredentialOptions()) { + if (option.getCredentialRetrievalData().getStringArrayList( + CredentialOption + .SUPPORTED_ELEMENT_KEYS) != null) { + return RequestInfo.TYPE_GET_VIA_REGISTRY; + } + } + return RequestInfo.TYPE_GET; + } + /** * Creates a new provider session, and adds it list of providers that are contributing to * this session. diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index 1f0346a2b9d6..d4b88001111e 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -132,7 +132,8 @@ public final class ProviderClearSession extends ProviderSession<ClearCredentialS protected void invokeSession() { if (mRemoteCredentialService != null) { startCandidateMetrics(); - mRemoteCredentialService.onClearCredentialState(mProviderRequest, this); + mRemoteCredentialService.setCallback(this); + mRemoteCredentialService.onClearCredentialState(mProviderRequest); } } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 16beaa4eb13e..409806a21679 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -248,7 +248,8 @@ public final class ProviderCreateSession extends ProviderSession< protected void invokeSession() { if (mRemoteCredentialService != null) { startCandidateMetrics(); - mRemoteCredentialService.onBeginCreateCredential(mProviderRequest, this); + mRemoteCredentialService.setCallback(this); + mRemoteCredentialService.onBeginCreateCredential(mProviderRequest); } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 1e80703378e0..64438e338e5b 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -309,7 +309,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential protected void invokeSession() { if (mRemoteCredentialService != null) { startCandidateMetrics(); - mRemoteCredentialService.onBeginGetCredential(mProviderRequest, this); + mRemoteCredentialService.setCallback(this); + mRemoteCredentialService.onBeginGetCredential(mProviderRequest); } } diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index f5e3b86213a1..4bcf8be0d21e 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -48,6 +48,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** @@ -59,13 +60,17 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr private static final String TAG = "RemoteCredentialService"; /** Timeout for a single request. */ - private static final long TIMEOUT_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS; + private static final long TIMEOUT_REQUEST_MILLIS = 3 * DateUtils.SECOND_IN_MILLIS; /** Timeout to unbind after the task queue is empty. */ private static final long TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS; private final ComponentName mComponentName; + private AtomicBoolean mOngoingRequest = new AtomicBoolean(false); + + @Nullable private ProviderCallbacks mCallback; + /** * Callbacks to be invoked when the provider remote service responds with a * success or failure. @@ -94,12 +99,35 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr mComponentName = componentName; } + public void setCallback(ProviderCallbacks callback) { + mCallback = callback; + } + /** Unbinds automatically after this amount of time. */ @Override protected long getAutoDisconnectTimeoutMs() { return TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS; } + @Override + public void onBindingDied(ComponentName name) { + super.onBindingDied(name); + + Slog.w(TAG, "binding died for: " + name); + } + + @Override + public void binderDied() { + super.binderDied(); + Slog.w(TAG, "binderDied"); + + if (mCallback != null) { + mOngoingRequest.set(false); + mCallback.onProviderServiceDied(this); + } + + } + /** Return the componentName of the service to be connected. */ @NonNull public ComponentName getComponentName() { @@ -116,11 +144,14 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr * provider service. * * @param request the request to be sent to the provider - * @param callback the callback to be used to send back the provider response to the - * {@link ProviderGetSession} class that maintains provider state */ - public void onBeginGetCredential(@NonNull BeginGetCredentialRequest request, - ProviderCallbacks<BeginGetCredentialResponse> callback) { + public void onBeginGetCredential(@NonNull BeginGetCredentialRequest request) { + if (mCallback == null) { + Slog.w(TAG, "Callback is not set"); + return; + } + mOngoingRequest.set(true); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); AtomicReference<CompletableFuture<BeginGetCredentialResponse>> futureRef = new AtomicReference<>(); @@ -154,7 +185,9 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr dispatchCancellationSignal(cancellation); } else { cancellationSink.set(cancellation); - callback.onProviderCancellable(cancellation); + if (mCallback != null) { + mCallback.onProviderCancellable(cancellation); + } } } }); @@ -166,7 +199,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> - handleExecutionResponse(result, error, cancellationSink, callback))); + handleExecutionResponse(result, error, cancellationSink))); } /** @@ -174,11 +207,14 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr * provider service. * * @param request the request to be sent to the provider - * @param callback the callback to be used to send back the provider response to the - * {@link ProviderCreateSession} class that maintains provider state */ - public void onBeginCreateCredential(@NonNull BeginCreateCredentialRequest request, - ProviderCallbacks<BeginCreateCredentialResponse> callback) { + public void onBeginCreateCredential(@NonNull BeginCreateCredentialRequest request) { + if (mCallback == null) { + Slog.w(TAG, "Callback is not set"); + return; + } + mOngoingRequest.set(true); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); AtomicReference<CompletableFuture<BeginCreateCredentialResponse>> futureRef = new AtomicReference<>(); @@ -212,7 +248,9 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr dispatchCancellationSignal(cancellation); } else { cancellationSink.set(cancellation); - callback.onProviderCancellable(cancellation); + if (mCallback != null) { + mCallback.onProviderCancellable(cancellation); + } } } }); @@ -224,7 +262,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> - handleExecutionResponse(result, error, cancellationSink, callback))); + handleExecutionResponse(result, error, cancellationSink))); } /** @@ -232,11 +270,14 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr * provider service. * * @param request the request to be sent to the provider - * @param callback the callback to be used to send back the provider response to the - * {@link ProviderClearSession} class that maintains provider state */ - public void onClearCredentialState(@NonNull ClearCredentialStateRequest request, - ProviderCallbacks<Void> callback) { + public void onClearCredentialState(@NonNull ClearCredentialStateRequest request) { + if (mCallback == null) { + Slog.w(TAG, "Callback is not set"); + return; + } + mOngoingRequest.set(true); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); AtomicReference<CompletableFuture<Void>> futureRef = new AtomicReference<>(); @@ -269,7 +310,9 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr dispatchCancellationSignal(cancellation); } else { cancellationSink.set(cancellation); - callback.onProviderCancellable(cancellation); + if (mCallback != null) { + mCallback.onProviderCancellable(cancellation); + } } } }); @@ -281,40 +324,58 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr futureRef.set(connectThenExecute); connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> - handleExecutionResponse(result, error, cancellationSink, callback))); + handleExecutionResponse(result, error, cancellationSink))); } private <T> void handleExecutionResponse(T result, Throwable error, - AtomicReference<ICancellationSignal> cancellationSink, - ProviderCallbacks<T> callback) { + AtomicReference<ICancellationSignal> cancellationSink) { if (error == null) { - callback.onProviderResponseSuccess(result); + if (mCallback != null) { + mCallback.onProviderResponseSuccess(result); + } } else { if (error instanceof TimeoutException) { Slog.i(TAG, "Remote provider response timed tuo for: " + mComponentName); + if (!mOngoingRequest.get()) { + return; + } dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderErrors.ERROR_TIMEOUT, - null); + if (mCallback != null) { + mOngoingRequest.set(false); + mCallback.onProviderResponseFailure( + CredentialProviderErrors.ERROR_TIMEOUT, null); + } } else if (error instanceof CancellationException) { Slog.i(TAG, "Cancellation exception for remote provider: " + mComponentName); + if (!mOngoingRequest.get()) { + return; + } dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderErrors.ERROR_TASK_CANCELED, - null); + if (mCallback != null) { + mOngoingRequest.set(false); + mCallback.onProviderResponseFailure( + CredentialProviderErrors.ERROR_TASK_CANCELED, + null); + } } else if (error instanceof GetCredentialException) { - callback.onProviderResponseFailure( - CredentialProviderErrors.ERROR_PROVIDER_FAILURE, - (GetCredentialException) error); + if (mCallback != null) { + mCallback.onProviderResponseFailure( + CredentialProviderErrors.ERROR_PROVIDER_FAILURE, + (GetCredentialException) error); + } } else if (error instanceof CreateCredentialException) { - callback.onProviderResponseFailure( - CredentialProviderErrors.ERROR_PROVIDER_FAILURE, - (CreateCredentialException) error); + if (mCallback != null) { + mCallback.onProviderResponseFailure( + CredentialProviderErrors.ERROR_PROVIDER_FAILURE, + (CreateCredentialException) error); + } } else { - callback.onProviderResponseFailure( - CredentialProviderErrors.ERROR_UNKNOWN, - (Exception) error); + if (mCallback != null) { + mCallback.onProviderResponseFailure( + CredentialProviderErrors.ERROR_UNKNOWN, + (Exception) error); + } } } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java index 1930a4859e87..fd497965b5b1 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java @@ -18,11 +18,13 @@ package com.android.server.credentials.metrics; import static android.credentials.ui.RequestInfo.TYPE_CREATE; import static android.credentials.ui.RequestInfo.TYPE_GET; +import static android.credentials.ui.RequestInfo.TYPE_GET_VIA_REGISTRY; import static android.credentials.ui.RequestInfo.TYPE_UNDEFINED; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_CLEAR_CREDENTIAL; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_CREATE_CREDENTIAL; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_GET_CREDENTIAL; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_GET_CREDENTIAL_VIA_REGISTRY; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_UNKNOWN; @@ -35,6 +37,8 @@ import java.util.Map; public enum ApiName { UNKNOWN(CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_UNKNOWN), GET_CREDENTIAL(CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_GET_CREDENTIAL), + GET_CREDENTIAL_VIA_REGISTRY( +CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_GET_CREDENTIAL_VIA_REGISTRY), CREATE_CREDENTIAL( CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_CREATE_CREDENTIAL), CLEAR_CREDENTIAL( @@ -52,6 +56,8 @@ CREDENTIAL_MANAGER_INITIAL_PHASE_REPORTED__API_NAME__API_NAME_IS_ENABLED_CREDENT CREATE_CREDENTIAL.mInnerMetricCode), new AbstractMap.SimpleEntry<>(TYPE_GET, GET_CREDENTIAL.mInnerMetricCode), + new AbstractMap.SimpleEntry<>(TYPE_GET_VIA_REGISTRY, + GET_CREDENTIAL_VIA_REGISTRY.mInnerMetricCode), new AbstractMap.SimpleEntry<>(TYPE_UNDEFINED, CLEAR_CREDENTIAL.mInnerMetricCode) ); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index debfedcd1806..bb3b4386a4de 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -23200,6 +23200,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_ACROSS_USERS); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_DEFAULT_SMS, MANAGE_DEVICE_POLICY_ACROSS_USERS); + CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_INPUT_METHODS, + MANAGE_DEVICE_POLICY_ACROSS_USERS); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MICROPHONE, MANAGE_DEVICE_POLICY_ACROSS_USERS); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MOBILE_NETWORK, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 8030bb7b4fd9..bac39e021d2f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -265,7 +265,7 @@ final class PolicyDefinition<V> { // never used, but might need some refactoring to not always assume a non-null // mechanism. TRUE_MORE_RESTRICTIVE, - POLICY_FLAG_LOCAL_ONLY_POLICY, + POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE, PolicyEnforcerCallbacks::setApplicationHidden, new BooleanPolicySerializer()); @@ -290,7 +290,7 @@ final class PolicyDefinition<V> { new AccountTypePolicyKey( DevicePolicyIdentifiers.ACCOUNT_MANAGEMENT_DISABLED_POLICY), TRUE_MORE_RESTRICTIVE, - POLICY_FLAG_LOCAL_ONLY_POLICY, + POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE, // Nothing is enforced, we just need to store it (Boolean value, Context context, Integer userId, PolicyKey policyKey) -> true, new BooleanPolicySerializer()); @@ -311,7 +311,7 @@ final class PolicyDefinition<V> { static PolicyDefinition<Set<String>> PERMITTED_INPUT_METHODS = new PolicyDefinition<>( new NoArgsPolicyKey(DevicePolicyIdentifiers.PERMITTED_INPUT_METHODS_POLICY), new MostRecent<>(), - POLICY_FLAG_LOCAL_ONLY_POLICY, + POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE, (Set<String> value, Context context, Integer userId, PolicyKey policyKey) -> true, new StringSetPolicySerializer()); @@ -319,14 +319,14 @@ final class PolicyDefinition<V> { static PolicyDefinition<Boolean> SCREEN_CAPTURE_DISABLED = new PolicyDefinition<>( new NoArgsPolicyKey(DevicePolicyIdentifiers.SCREEN_CAPTURE_DISABLED_POLICY), TRUE_MORE_RESTRICTIVE, - /* flags= */ 0, + POLICY_FLAG_INHERITABLE, PolicyEnforcerCallbacks::setScreenCaptureDisabled, new BooleanPolicySerializer()); static PolicyDefinition<Boolean> PERSONAL_APPS_SUSPENDED = new PolicyDefinition<>( new NoArgsPolicyKey(DevicePolicyIdentifiers.PERSONAL_APPS_SUSPENDED_POLICY), new MostRecent<>(), - POLICY_FLAG_LOCAL_ONLY_POLICY, + POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE, PolicyEnforcerCallbacks::setPersonalAppsSuspended, new BooleanPolicySerializer()); @@ -547,7 +547,7 @@ final class PolicyDefinition<V> { String restriction, int flags) { String identifier = DevicePolicyIdentifiers.getIdentifierForUserRestriction(restriction); UserRestrictionPolicyKey key = new UserRestrictionPolicyKey(identifier, restriction); - flags |= POLICY_FLAG_USER_RESTRICTION_POLICY; + flags |= (POLICY_FLAG_USER_RESTRICTION_POLICY | POLICY_FLAG_INHERITABLE); PolicyDefinition<Boolean> definition = new PolicyDefinition<>( key, TRUE_MORE_RESTRICTIVE, diff --git a/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java index 46974cf72381..37afc7f52f7e 100644 --- a/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java @@ -217,7 +217,8 @@ public class UidObserverControllerTest { private void registerObserver(IUidObserver observer, int which, int cutpoint, String callingPackage, int callingUid) { when(observer.asBinder()).thenReturn((IBinder) observer); - mUidObserverController.register(observer, which, cutpoint, callingPackage, callingUid); + mUidObserverController.register(observer, which, cutpoint, callingPackage, callingUid, + /*uids*/null); Mockito.reset(observer); } diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java index 385c28ae7152..6a1674b7df8e 100644 --- a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java @@ -38,7 +38,6 @@ import android.os.BatteryStatsInternal; import android.os.Process; import android.os.RemoteException; -import androidx.test.filters.FlakyTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.FakeLatencyTracker; @@ -92,12 +91,10 @@ public class SoundTriggerMiddlewareLoggingLatencyTest { } @Test - @FlakyTest(bugId = 275113847) public void testSetUpAndTearDown() { } @Test - @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionStartsLatencyTrackerWithSuccessfulPhraseIdTrigger() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( @@ -114,7 +111,6 @@ public class SoundTriggerMiddlewareLoggingLatencyTest { } @Test - @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionRestartsActiveSession() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( ISoundTriggerCallback.class); @@ -135,7 +131,6 @@ public class SoundTriggerMiddlewareLoggingLatencyTest { } @Test - @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNonSuccessEvent() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( @@ -153,7 +148,6 @@ public class SoundTriggerMiddlewareLoggingLatencyTest { } @Test - @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNoKeyphraseId() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java index bf6901e963e0..9029bc48138c 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java @@ -18,6 +18,7 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_ALT_LEFT; import static android.view.KeyEvent.KEYCODE_B; +import static android.view.KeyEvent.KEYCODE_BRIGHTNESS_DOWN; import static android.view.KeyEvent.KEYCODE_C; import static android.view.KeyEvent.KEYCODE_CTRL_LEFT; import static android.view.KeyEvent.KEYCODE_E; @@ -186,4 +187,19 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0); mPhoneWindowManager.assertGoToHomescreen(); } + + /** + * Sends a KEYCODE_BRIGHTNESS_DOWN event and validates the brightness is decreased as expected; + */ + @Test + public void testKeyCodeBrightnessDown() { + float[] currentBrightness = new float[]{0.1f, 0.05f, 0.0f}; + float[] newBrightness = new float[]{0.065738f, 0.0275134f, 0.0f}; + + for (int i = 0; i < currentBrightness.length; i++) { + mPhoneWindowManager.prepareBrightnessDecrease(currentBrightness[i]); + sendKey(KEYCODE_BRIGHTNESS_DOWN); + mPhoneWindowManager.verifyNewBrightness(newBrightness[i]); + } + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 1053fd5c4518..d38302429c02 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -89,6 +89,7 @@ import com.android.server.wm.WindowManagerInternal.AppTransitionListener; import junit.framework.Assert; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockSettings; @@ -339,6 +340,20 @@ class TestPhoneWindowManager { setPhoneCallIsInProgress(); } + void prepareBrightnessDecrease(float currentBrightness) { + doReturn(0.0f).when(mPowerManager) + .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM); + doReturn(1.0f).when(mPowerManager) + .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM); + doReturn(currentBrightness).when(mDisplayManager) + .getBrightness(0); + } + + void verifyNewBrightness(float newBrightness) { + verify(mDisplayManager).setBrightness(Mockito.eq(0), + AdditionalMatchers.eq(newBrightness, 0.001f)); + } + void setPhoneCallIsInProgress() { // Let device has an ongoing phone call. doReturn(false).when(mTelecomManager).isRinging(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 353a8ecaf13d..8ac6b0f65b72 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -22,6 +22,7 @@ import static android.view.RoundedCorners.NO_ROUNDED_CORNERS; import static android.view.Surface.ROTATION_0; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.ViewRootImpl.CLIENT_TRANSIENT; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; @@ -63,6 +64,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; +import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @@ -388,6 +390,7 @@ public class DisplayPolicyTests extends WindowTestsBase { @SetupWindows(addWindows = { W_ACTIVITY, W_NAVIGATION_BAR }) @Test public void testCanSystemBarsBeShownByUser() { + Assume.assumeFalse(CLIENT_TRANSIENT); ((TestWindowManagerPolicy) mWm.mPolicy).mIsUserSetupComplete = true; mAppWindow.mAttrs.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; mAppWindow.setRequestedVisibleTypes(0, navigationBars()); @@ -409,6 +412,7 @@ public class DisplayPolicyTests extends WindowTestsBase { @UseTestDisplay(addWindows = { W_NAVIGATION_BAR }) @Test public void testTransientBarsSuppressedOnDreams() { + Assume.assumeFalse(CLIENT_TRANSIENT); final WindowState win = createDreamWindow(); ((TestWindowManagerPolicy) mWm.mPolicy).mIsUserSetupComplete = true; diff --git a/telecomm/java/android/telecom/DisconnectCause.java b/telecomm/java/android/telecom/DisconnectCause.java index b003f59d5e81..331caa1bad7a 100644 --- a/telecomm/java/android/telecom/DisconnectCause.java +++ b/telecomm/java/android/telecom/DisconnectCause.java @@ -43,8 +43,8 @@ public final class DisconnectCause implements Parcelable { /** Disconnected because of a local user-initiated action, such as hanging up. */ public static final int LOCAL = TelecomProtoEnums.LOCAL; // = 2 /** - * Disconnected because of a remote user-initiated action, such as the other party hanging up - * up. + * Disconnected because the remote party hung up an ongoing call, or because an outgoing call + * was not answered by the remote party. */ public static final int REMOTE = TelecomProtoEnums.REMOTE; // = 3 /** Disconnected because it has been canceled. */ diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index fb46ff991d23..2021ac7c4cd5 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -278,6 +278,11 @@ public class SatelliteManager { */ public static final int SATELLITE_REQUEST_IN_PROGRESS = 21; + /** + * Satellite modem is currently busy due to which current request cannot be processed. + */ + public static final int SATELLITE_MODEM_BUSY = 22; + /** @hide */ @IntDef(prefix = {"SATELLITE_"}, value = { SATELLITE_ERROR_NONE, @@ -301,7 +306,8 @@ public class SatelliteManager { SATELLITE_NOT_REACHABLE, SATELLITE_NOT_AUTHORIZED, SATELLITE_NOT_SUPPORTED, - SATELLITE_REQUEST_IN_PROGRESS + SATELLITE_REQUEST_IN_PROGRESS, + SATELLITE_MODEM_BUSY }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteError {} diff --git a/tests/SilkFX/res/layout/gainmap_metadata.xml b/tests/SilkFX/res/layout/gainmap_metadata.xml index 0dabaca457f0..4cc3e0cbdb83 100644 --- a/tests/SilkFX/res/layout/gainmap_metadata.xml +++ b/tests/SilkFX/res/layout/gainmap_metadata.xml @@ -21,8 +21,8 @@ android:layout_height="wrap_content"> <LinearLayout - android:layout_width="350dp" - android:layout_height="300dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:padding="8dp" android:orientation="vertical" diff --git a/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt b/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt index 6a8752abfde7..501b9d33871a 100644 --- a/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt +++ b/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt @@ -79,6 +79,16 @@ class UserUnlockRequestTest { .isEqualTo(oldCount + 1) } + @Test + fun reportUserMayRequestUnlock_differentUserId_doesNotPropagateToAgent() { + val oldCount = trustAgentRule.agent.onUserMayRequestUnlockCallCount + trustManager.reportUserMayRequestUnlock(userId + 1) + await() + + assertThat(trustAgentRule.agent.onUserMayRequestUnlockCallCount) + .isEqualTo(oldCount) + } + companion object { private const val TAG = "UserUnlockRequestTest" private fun await() = Thread.sleep(250) |