diff options
243 files changed, 5874 insertions, 1979 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING index e649485ed5e5..e82df1203137 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING +++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING @@ -41,10 +41,10 @@ ] }, { - "name": "CtsHostsideNetworkTests", + "name": "CtsHostsideNetworkPolicyTests", "options": [ - {"include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests#testMeteredNetworkAccess_expeditedJob"}, - {"include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests#testNonMeteredNetworkAccess_expeditedJob"} + {"include-filter": "com.android.cts.netpolicy.HostsideRestrictBackgroundNetworkTests#testMeteredNetworkAccess_expeditedJob"}, + {"include-filter": "com.android.cts.netpolicy.HostsideRestrictBackgroundNetworkTests#testNonMeteredNetworkAccess_expeditedJob"} ] }, { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f10c0fc21455..eabe1f1c271a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -872,6 +872,7 @@ package android.app { public static final class AppOpsManager.OpEventProxyInfo implements android.os.Parcelable { method public int describeContents(); method @Nullable public String getAttributionTag(); + method @FlaggedApi("android.permission.flags.device_id_in_op_proxy_info_enabled") @Nullable public String getDeviceId(); method @Nullable public String getPackageName(); method @IntRange(from=0) public int getUid(); method public void writeToParcel(@NonNull android.os.Parcel, int); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 68cc17b6d92b..caaaf519eaca 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7438,6 +7438,7 @@ public final class ActivityThread extends ClientTransactionHandler } mDdmSyncStageUpdater.next(Stage.Running); + long timestampApplicationOnCreateNs = 0; try { // If the app is being launched for full backup or restore, bring it up in // a restricted environment with the base application class. @@ -7480,8 +7481,10 @@ public final class ActivityThread extends ClientTransactionHandler + data.instrumentationName + ": " + e.toString(), e); } try { + timestampApplicationOnCreateNs = SystemClock.elapsedRealtimeNanos(); mInstrumentation.callApplicationOnCreate(app); } catch (Exception e) { + timestampApplicationOnCreateNs = 0; if (!mInstrumentation.onException(app, e)) { throw new RuntimeException( "Unable to create application " + app.getClass().getName() @@ -7519,7 +7522,7 @@ public final class ActivityThread extends ClientTransactionHandler } try { - mgr.finishAttachApplication(mStartSeq); + mgr.finishAttachApplication(mStartSeq, timestampApplicationOnCreateNs); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 20b2357e967d..2d0f6fccb8f2 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -3457,6 +3457,8 @@ public class AppOpsManager { private @Nullable String mPackageName; /** Attribution tag of the proxy that noted the op */ private @Nullable String mAttributionTag; + /** Persistent device Id of the proxy that noted the op */ + private @Nullable String mDeviceId; /** * Reinit existing object with new state. @@ -3464,14 +3466,16 @@ public class AppOpsManager { * @param uid UID of the proxy app that noted the op * @param packageName Package of the proxy that noted the op * @param attributionTag attribution tag of the proxy that noted the op + * @param deviceId Persistent device Id of the proxy that noted the op * * @hide */ public void reinit(@IntRange(from = 0) int uid, @Nullable String packageName, - @Nullable String attributionTag) { + @Nullable String attributionTag, @Nullable String deviceId) { mUid = Preconditions.checkArgumentNonnegative(uid); mPackageName = packageName; mAttributionTag = attributionTag; + mDeviceId = deviceId; } @@ -3505,16 +3509,33 @@ public class AppOpsManager { @IntRange(from = 0) int uid, @Nullable String packageName, @Nullable String attributionTag) { + this(uid, packageName, attributionTag, + VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT); + } + + /** + * Creates a new OpEventProxyInfo. + * + * @param uid UID of the proxy app that noted the op + * @param packageName Package of the proxy that noted the op + * @param attributionTag Attribution tag of the proxy that noted the op + * @param deviceId Persistent device Id of the proxy that noted the op + * + * @hide + */ + public OpEventProxyInfo( + @IntRange(from = 0) int uid, + @Nullable String packageName, + @Nullable String attributionTag, + @Nullable String deviceId) { this.mUid = uid; com.android.internal.util.AnnotationValidations.validate( IntRange.class, null, mUid, "from", 0); this.mPackageName = packageName; this.mAttributionTag = attributionTag; - - // onConstructed(); // You can define this method to get a callback + this.mDeviceId = deviceId; } - /** * Copy constructor * @@ -3525,6 +3546,7 @@ public class AppOpsManager { mUid = orig.mUid; mPackageName = orig.mPackageName; mAttributionTag = orig.mAttributionTag; + mDeviceId = orig.mDeviceId; } /** @@ -3551,6 +3573,9 @@ public class AppOpsManager { return mAttributionTag; } + @FlaggedApi(Flags.FLAG_DEVICE_ID_IN_OP_PROXY_INFO_ENABLED) + public @Nullable String getDeviceId() { return mDeviceId; } + @Override @DataClass.Generated.Member public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -3560,10 +3585,12 @@ public class AppOpsManager { byte flg = 0; if (mPackageName != null) flg |= 0x2; if (mAttributionTag != null) flg |= 0x4; + if (mDeviceId != null) flg |= 0x8; dest.writeByte(flg); dest.writeInt(mUid); if (mPackageName != null) dest.writeString(mPackageName); if (mAttributionTag != null) dest.writeString(mAttributionTag); + if (mDeviceId != null) dest.writeString(mDeviceId); } @Override @@ -3581,14 +3608,14 @@ public class AppOpsManager { int uid = in.readInt(); String packageName = (flg & 0x2) == 0 ? null : in.readString(); String attributionTag = (flg & 0x4) == 0 ? null : in.readString(); - + String deviceId = (flg & 0x8) == 0 ? null : in.readString(); this.mUid = uid; com.android.internal.util.AnnotationValidations.validate( IntRange.class, null, mUid, "from", 0); this.mPackageName = packageName; this.mAttributionTag = attributionTag; - + this.mDeviceId = deviceId; // onConstructed(); // You can define this method to get a callback } diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 3765c817b117..e8b57f224a0b 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -196,7 +196,7 @@ interface IActivityManager { oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map, boolean abortBroadcast, int flags); void attachApplication(in IApplicationThread app, long startSeq); - void finishAttachApplication(long startSeq); + void finishAttachApplication(long startSeq, long timestampApplicationOnCreateNs); List<ActivityManager.RunningTaskInfo> getTasks(int maxNum); @UnsupportedAppUsage void moveTaskToFront(in IApplicationThread caller, in String callingPackage, int task, diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index f1e44cc267d6..1df8f63aa402 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1104,6 +1104,10 @@ public final class LoadedApk { return true; } + if (mDataDir == null) { + return false; + } + // Temporarily disable logging of disk reads on the Looper thread as this is necessary - // and the loader will access the directory anyway if we don't check it. StrictMode.ThreadPolicy oldThreadPolicy = allowThreadDiskReads(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4d7e29b19771..25dbedce1394 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -18,7 +18,6 @@ package android.app; import static android.annotation.Dimension.DP; import static android.app.Flags.evenlyDividedCallStyleActionLayout; -import static android.app.Flags.updateRankingTime; import static android.app.admin.DevicePolicyResources.Drawables.Source.NOTIFICATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -897,15 +896,16 @@ public class Notification implements Parcelable /** * Sphere of visibility of this notification, which affects how and when the SystemUI reveals * the notification's presence and contents in untrusted situations (namely, on the secure - * lockscreen). + * lockscreen and during screen sharing). * * The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always * done on Android: The notification's {@link #icon} and {@link #tickerText} (if available) are * shown in all situations, but the contents are only available if the device is unlocked for - * the appropriate user. + * the appropriate user and there is no active screen sharing session. * * A more permissive policy can be expressed by {@link #VISIBILITY_PUBLIC}; such a notification - * can be read even in an "insecure" context (that is, above a secure lockscreen). + * can be read even in an "insecure" context (that is, above a secure lockscreen or while + * screen sharing with a remote viewer). * To modify the public version of this notification—for example, to redact some portions—see * {@link Builder#setPublicVersion(Notification)}. * @@ -924,7 +924,8 @@ public class Notification implements Parcelable public @interface Visibility {} /** - * Notification visibility: Show this notification in its entirety on all lockscreens. + * Notification visibility: Show this notification in its entirety on all lockscreens and while + * screen sharing. * * {@see #visibility} */ @@ -932,14 +933,16 @@ public class Notification implements Parcelable /** * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or - * private information on secure lockscreens. + * private information on secure lockscreens. Conceal sensitive or private information while + * screen sharing. * * {@see #visibility} */ public static final int VISIBILITY_PRIVATE = 0; /** - * Notification visibility: Do not reveal any part of this notification on a secure lockscreen. + * Notification visibility: Do not reveal any part of this notification on a secure lockscreen + * or while screen sharing. * * {@see #visibility} */ @@ -2596,7 +2599,7 @@ public class Notification implements Parcelable public Notification() { this.when = System.currentTimeMillis(); - if (updateRankingTime()) { + if (Flags.sortSectionByTime()) { creationTime = when; extras.putBoolean(EXTRA_SHOW_WHEN, true); } else { @@ -2612,7 +2615,7 @@ public class Notification implements Parcelable public Notification(Context context, int icon, CharSequence tickerText, long when, CharSequence contentTitle, CharSequence contentText, Intent contentIntent) { - if (updateRankingTime()) { + if (Flags.sortSectionByTime()) { creationTime = when; extras.putBoolean(EXTRA_SHOW_WHEN, true); } @@ -2645,7 +2648,7 @@ public class Notification implements Parcelable this.icon = icon; this.tickerText = tickerText; this.when = when; - if (updateRankingTime()) { + if (Flags.sortSectionByTime()) { creationTime = when; extras.putBoolean(EXTRA_SHOW_WHEN, true); } else { @@ -5981,21 +5984,22 @@ public class Notification implements Parcelable } if (mN.extras.getBoolean(EXTRA_SHOW_CHRONOMETER)) { contentView.setViewVisibility(R.id.chronometer, View.VISIBLE); - contentView.setLong(R.id.chronometer, "setBase", - mN.when + (SystemClock.elapsedRealtime() - System.currentTimeMillis())); + contentView.setLong(R.id.chronometer, "setBase", mN.getWhen() + + (SystemClock.elapsedRealtime() - System.currentTimeMillis())); contentView.setBoolean(R.id.chronometer, "setStarted", true); boolean countsDown = mN.extras.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN); contentView.setChronometerCountDown(R.id.chronometer, countsDown); setTextViewColorSecondary(contentView, R.id.chronometer, p); } else { contentView.setViewVisibility(R.id.time, View.VISIBLE); - contentView.setLong(R.id.time, "setTime", mN.when); + contentView.setLong(R.id.time, "setTime", mN.getWhen()); setTextViewColorSecondary(contentView, R.id.time, p); } } else { // We still want a time to be set but gone, such that we can show and hide it // on demand in case it's a child notification without anything in the header - contentView.setLong(R.id.time, "setTime", mN.when != 0 ? mN.when : mN.creationTime); + contentView.setLong(R.id.time, "setTime", mN.getWhen() != 0 ? mN.getWhen() : + mN.creationTime); setTextViewColorSecondary(contentView, R.id.time, p); } } @@ -7162,7 +7166,7 @@ public class Notification implements Parcelable } } - if (!updateRankingTime()) { + if (!Flags.sortSectionByTime()) { mN.creationTime = System.currentTimeMillis(); } @@ -7615,10 +7619,29 @@ public class Notification implements Parcelable } /** + * Returns #when, unless it's set to 0, which should be shown as/treated as a 'current' + * notification. 0 is treated as a special value because it was special in an old version of + * android, and some apps are still (incorrectly) using it. + * + * @hide + */ + public long getWhen() { + if (Flags.sortSectionByTime()) { + if (when == 0) { + return creationTime; + } + } + return when; + } + + /** * @return true if the notification will show the time; false otherwise * @hide */ public boolean showsTime() { + if (Flags.sortSectionByTime()) { + return extras.getBoolean(EXTRA_SHOW_WHEN); + } return when != 0 && extras.getBoolean(EXTRA_SHOW_WHEN); } @@ -7627,6 +7650,9 @@ public class Notification implements Parcelable * @hide */ public boolean showsChronometer() { + if (Flags.sortSectionByTime()) { + return extras.getBoolean(EXTRA_SHOW_CHRONOMETER); + } return when != 0 && extras.getBoolean(EXTRA_SHOW_CHRONOMETER); } @@ -9770,6 +9796,12 @@ public class Notification implements Parcelable * You can opt-out of this behavior by using {@link Notification.Builder#setColorized(boolean)}. * <p> * + * <p> + * Starting at {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM Android V} the + * {@link Notification#FLAG_NO_CLEAR NO_CLEAR flag} will be set for valid MediaStyle + * notifications. + * <p> + * * To use this style with your Notification, feed it to * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: * <pre class="prettyprint"> diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index f092945a5d28..726064e39778 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -1197,6 +1197,25 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac /** * Callback called when a particular foreground service type has timed out. * + * <p>This callback is meant to give the app a small grace period of a few seconds to finish + * the foreground service of the associated type - if it fails to do so, the app will be + * declared an ANR. + * + * <p>The foreground service of the associated type can be stopped within the time limit by + * {@link android.app.Service#stopSelf()}, + * {@link android.content.Context#stopService(android.content.Intent)} or their overloads. + * {@link android.app.Service#stopForeground(int)} can be used as well, which demotes the + * service to a "background" service, which will soon be stopped by the system. + * + * <p>The specific time limit for each type (if one exists) is mentioned in the documentation + * for that foreground service type. See + * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC dataSync} for example. + * + * <p>Note: time limits are restricted to a rolling 24-hour window - for example, if a + * foreground service type has a time limit of 6 hours, that time counter begins as soon as the + * foreground service starts. This time limit will only be reset once every 24 hours or if the + * app comes into the foreground state. + * * @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when * the service started. * @param fgsType the {@link ServiceInfo.ForegroundServiceType foreground service type} which diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index e4425ca1e8a5..8c4667f15299 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -40,3 +40,13 @@ flag { description: "Add a new callback in Service to indicate a FGS has reached its timeout." bug: "317799821" } + +flag { + namespace: "system_performance" + name: "app_start_info_timestamps" + description: "Additional timestamps." + bug: "287153617" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 7ac95475d24b..4f062090ca40 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1348,6 +1348,17 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long OVERRIDE_MIN_ASPECT_RATIO = 174042980L; // buganizer id /** + * This change id restricts treatments that force a given min aspect ratio to + * only when an app is connected to the camera + * + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA = 325586858L; // buganizer id + + /** * This change id restricts treatments that force a given min aspect ratio to activities * whose orientation is fixed to portrait. * diff --git a/core/java/android/hardware/biometrics/BiometricTestSession.java b/core/java/android/hardware/biometrics/BiometricTestSession.java index c62680f7354e..027d1015a4b5 100644 --- a/core/java/android/hardware/biometrics/BiometricTestSession.java +++ b/core/java/android/hardware/biometrics/BiometricTestSession.java @@ -92,6 +92,7 @@ public class BiometricTestSession implements AutoCloseable { mTestedUsers = new ArraySet<>(); mUsersCleaningUp = new ArraySet<>(); setTestHalEnabled(true); + Log.d(getTag(), "Opening BiometricTestSession"); } /** diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index c6a8762e4d79..342479bc159e 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -5062,21 +5062,29 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>The version of the session configuration query * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported } - * API</p> + * and {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#getSessionCharacteristics } + * APIs.</p> * <p>The possible values in this key correspond to the values defined in * android.os.Build.VERSION_CODES. Each version defines a set of feature combinations the * camera device must reliably report whether they are supported via - * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported } - * API. And the version is always less or equal to android.os.Build.VERSION.SDK_INT.</p> - * <p>If set to UPSIDE_DOWN_CAKE, this camera device doesn't support * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported }. - * Calling the method for this camera ID throws an UnsupportedOperationException.</p> - * <p>If set to VANILLA_ICE_CREAM, the application can call + * It also defines the set of session specific keys in CameraCharacteristics when returned from + * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#getSessionCharacteristics }. + * The version is always less or equal to android.os.Build.VERSION.SDK_INT.</p> + * <p>If set to UPSIDE_DOWN_CAKE, this camera device doesn't support the + * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup } API. + * Trying to create a CameraDeviceSetup instance throws an UnsupportedOperationException.</p> + * <p>From VANILLA_ICE_CREAM onwards, the camera compliance tests verify a set of + * commonly used SessionConfigurations to ensure that the outputs of * {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported } - * to check if the combinations of below features are supported.</p> + * and {@link android.hardware.camera2.CameraDevice.CameraDeviceSetup#getSessionCharacteristics } + * are accurate. The application is encouraged to use these SessionConfigurations when turning on + * multiple features at the same time.</p> + * <p>When set to VANILLA_ICE_CREAM, the combinations of the following configurations are verified + * by the compliance tests:</p> * <ul> - * <li>A subset of LIMITED-level device stream combinations.</li> - * </ul> + * <li> + * <p>A set of commonly used stream combinations:</p> * <table> * <thead> * <tr> @@ -5084,257 +5092,108 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <th style="text-align: center;">Size</th> * <th style="text-align: center;">Target 2</th> * <th style="text-align: center;">Size</th> - * <th style="text-align: center;">Sample use case(s)</th> * </tr> * </thead> * <tbody> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;">Simple preview, GPU video processing, or no-preview video recording.</td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> * <td style="text-align: center;"></td> * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S720P</td> * <td style="text-align: center;"></td> * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;">In-application video/image processing.</td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;">Standard still imaging.</td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">MAXIMUM_16_9</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">UHD</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">JPEG</td> * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;">In-app processing plus still capture.</td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">MAXIMUM</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> * </tr> * <tr> - * <td style="text-align: center;">YUV</td> + * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;">JPEG</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">JPEG</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">Standard recording.</td> - * </tr> - * <tr> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">UHD</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">MAXIMUM_16_9</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">PREVIEW</td> - * <td style="text-align: center;">Preview plus in-app processing.</td> + * <td style="text-align: center;">S720P</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">UHD</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1440P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">S720P</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">S1080P</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S1080P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">XVGA</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">MAXIMUM_4_3</td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;">YUV</td> - * <td style="text-align: center;">S720P</td> - * <td style="text-align: center;"></td> + * <td style="text-align: center;">S1080P_4_3</td> + * <td style="text-align: center;">JPEG/JPEG_R</td> + * <td style="text-align: center;">MAXIMUM_4_3</td> * </tr> * </tbody> * </table> - * <pre><code>- {@code MAXIMUM} size refers to the camera device's maximum output resolution for - * that format from {@code StreamConfigurationMap#getOutputSizes}. {@code PREVIEW} size - * refers to the best size match to the device's screen resolution, or to 1080p - * (@code 1920x1080}, whichever is smaller. Both sizes are guaranteed to be supported. - * - * - {@code S1440P} refers to {@code 1920x1440 (4:3)} and {@code 2560x1440 (16:9)}. - * {@code S1080P} refers to {@code 1440x1080 (4:3)} and {@code 1920x1080 (16:9)}. - * And {@code S720P} refers to {@code 960x720 (4:3)} and {@code 1280x720 (16:9)}. - * - * - If a combination contains a S1440P, S1080P, or S720P stream, - * both 4:3 and 16:9 aspect ratio sizes can be queried. For example, for the - * stream combination of {PRIV, S1440P, JPEG, MAXIMUM}, and if MAXIMUM == - * 4032 x 3024, the application will be able to query both - * {PRIV, 1920 x 1440, JPEG, 4032 x 3024} and {PRIV, 2560 x 1440, JPEG, 4032 x 2268} - * without an exception being thrown. - * </code></pre> * <ul> - * <li>VIDEO_STABILIZATION_MODES: {OFF, PREVIEW}</li> - * <li>AE_TARGET_FPS_RANGE: { {<em>, 30}, {</em>, 60} }</li> - * <li>DYNAMIC_RANGE_PROFILE: {STANDARD, HLG10}</li> + * <li>{@code MAXIMUM_4_3} refers to the camera device's maximum output resolution with + * 4:3 aspect ratio for that format from {@code StreamConfigurationMap#getOutputSizes}.</li> + * <li>{@code MAXIMUM_16_9} is the maximum output resolution with 16:9 aspect ratio.</li> + * <li>{@code S1440P} refers to {@code 2560x1440 (16:9)}.</li> + * <li>{@code S1080P} refers to {@code 1920x1080 (16:9)}.</li> + * <li>{@code S720P} refers to {@code 1280x720 (16:9)}.</li> + * <li>{@code UHD} refers to {@code 3840x2160 (16:9)}.</li> + * <li>{@code XVGA} refers to {@code 1024x768 (4:3)}.</li> + * <li>{@code S1080P_43} refers to {@code 1440x1080 (4:3)}.</li> + * </ul> + * </li> + * <li> + * <p>VIDEO_STABILIZATION_MODE: {OFF, PREVIEW}</p> + * </li> + * <li> + * <p>AE_TARGET_FPS_RANGE: { {*, 30}, {*, 60} }</p> + * </li> + * <li> + * <p>DYNAMIC_RANGE_PROFILE: {STANDARD, HLG10}</p> + * </li> * </ul> + * <p>All of the above configurations can be set up with a SessionConfiguration. The list of + * OutputConfiguration contains the stream configurations and DYNAMIC_RANGE_PROFILE, and + * the AE_TARGET_FPS_RANGE and VIDEO_STABILIZATION_MODE are set as session parameters.</p> * <p>This key is available on all devices.</p> */ @PublicKey diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index eb644e8cfa01..dfbf06b20e2c 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -917,8 +917,11 @@ public final class CameraExtensionCharacteristics { * image. For example, it can be used as a temporary placeholder for the requested capture * while the final image is being processed. The supported sizes for a still capture's postview * can be retrieved using - * {@link CameraExtensionCharacteristics#getPostviewSupportedSizes(int, Size, int)}. - * The formats of the still capture and postview should be equivalent upon capture request.</p> + * {@link CameraExtensionCharacteristics#getPostviewSupportedSizes(int, Size, int)}.</p> + * + * <p>Starting with Android {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * the formats of the still capture and postview are not required to be equivalent upon capture + * request.</p> * * @param extension the extension type * @return {@code true} in case postview is supported, {@code false} otherwise @@ -976,8 +979,7 @@ public final class CameraExtensionCharacteristics { * * @param extension the extension type * @param captureSize size of the still capture for which the postview is requested - * @param format device-specific extension output format of the still capture and - * postview + * @param format device-specific extension output format of the postview * @return non-modifiable list of available sizes or an empty list if the format and * size is not supported. * @throws IllegalArgumentException in case of unsupported extension or if postview @@ -1018,8 +1020,8 @@ public final class CameraExtensionCharacteristics { } IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension); extender.init(mCameraId, mCharacteristicsMapNative); - return generateSupportedSizes(extender.getSupportedPostviewResolutions( - sz), format, streamMap); + return getSupportedSizes(extender.getSupportedPostviewResolutions(sz), + format); } else { Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders = initializeExtension(extension); @@ -1034,15 +1036,13 @@ public final class CameraExtensionCharacteristics { } if (format == ImageFormat.YUV_420_888) { - return generateSupportedSizes( - extenders.second.getSupportedPostviewResolutions(sz), - format, streamMap); + return getSupportedSizes( + extenders.second.getSupportedPostviewResolutions(sz), format); } else if (format == ImageFormat.JPEG) { // The framework will perform the additional encoding pass on the // processed YUV_420 buffers. - return generateJpegSupportedSizes( - extenders.second.getSupportedPostviewResolutions(sz), - streamMap); + return getSupportedSizes( + extenders.second.getSupportedPostviewResolutions(sz), format); } else if (format == ImageFormat.JPEG_R || format == ImageFormat.YCBCR_P010) { // Jpeg_R/UltraHDR + YCBCR_P010 is currently not supported in the basic // extension case diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java b/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java index 875550aea5f5..a10e2505758e 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java @@ -36,6 +36,8 @@ import android.os.RemoteException; import android.util.Log; import android.view.Surface; +import com.android.internal.camera.flags.Flags; + import java.nio.ByteBuffer; import java.util.HashSet; import java.util.Iterator; @@ -57,6 +59,8 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl { private android.hardware.camera2.extension.Size mResolution = null; private android.hardware.camera2.extension.Size mPostviewResolution = null; private int mFormat = -1; + private int mPostviewFormat = -1; + private int mCaptureFormat = -1; private Surface mOutputSurface = null; private ImageWriter mOutputWriter = null; private Surface mPostviewOutputSurface = null; @@ -204,10 +208,12 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl { } public void onOutputSurface(Surface surface, int format) throws RemoteException { - if (format != ImageFormat.JPEG) { + if (!Flags.extension10Bit() && format != ImageFormat.JPEG) { Log.e(TAG, "Unsupported output format: " + format); return; } + CameraExtensionUtils.SurfaceInfo surfaceInfo = CameraExtensionUtils.querySurface(surface); + mCaptureFormat = surfaceInfo.mFormat; mOutputSurface = surface; initializePipeline(); } @@ -215,10 +221,11 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl { public void onPostviewOutputSurface(Surface surface) throws RemoteException { CameraExtensionUtils.SurfaceInfo postviewSurfaceInfo = CameraExtensionUtils.querySurface(surface); - if (postviewSurfaceInfo.mFormat != ImageFormat.JPEG) { + if (!Flags.extension10Bit() && postviewSurfaceInfo.mFormat != ImageFormat.JPEG) { Log.e(TAG, "Unsupported output format: " + postviewSurfaceInfo.mFormat); return; } + mPostviewFormat = postviewSurfaceInfo.mFormat; mPostviewOutputSurface = surface; initializePostviewPipeline(); } @@ -233,7 +240,7 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl { } public void onImageFormatUpdate(int format) throws RemoteException { - if (format != ImageFormat.YUV_420_888) { + if (!Flags.extension10Bit() && format != ImageFormat.YUV_420_888) { Log.e(TAG, "Unsupported input format: " + format); return; } @@ -244,33 +251,45 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl { private void initializePipeline() throws RemoteException { if ((mFormat != -1) && (mOutputSurface != null) && (mResolution != null) && (mYuvReader == null)) { - // Jpeg/blobs are expected to be configured with (w*h)x1.5 + 64k Jpeg APP1 segment - mOutputWriter = ImageWriter.newInstance(mOutputSurface, 1 /*maxImages*/, - ImageFormat.JPEG, - (mResolution.width * mResolution.height * 3)/2 + JPEG_APP_SEGMENT_SIZE, 1); - mYuvReader = ImageReader.newInstance(mResolution.width, mResolution.height, mFormat, - JPEG_QUEUE_SIZE); - mYuvReader.setOnImageAvailableListener( - new YuvCallback(mYuvReader, mOutputWriter), mHandler); - mProcessor.onOutputSurface(mYuvReader.getSurface(), mFormat); + if (Flags.extension10Bit() && mCaptureFormat == ImageFormat.YUV_420_888) { + // For the case when postview is JPEG and capture is YUV + mProcessor.onOutputSurface(mOutputSurface, mCaptureFormat); + } else { + // Jpeg/blobs are expected to be configured with (w*h)x1.5 + 64k Jpeg APP1 segment + mOutputWriter = ImageWriter.newInstance(mOutputSurface, 1 /*maxImages*/, + ImageFormat.JPEG, + (mResolution.width * mResolution.height * 3) / 2 + + JPEG_APP_SEGMENT_SIZE, 1); + mYuvReader = ImageReader.newInstance(mResolution.width, mResolution.height, + mFormat, JPEG_QUEUE_SIZE); + mYuvReader.setOnImageAvailableListener( + new YuvCallback(mYuvReader, mOutputWriter), mHandler); + mProcessor.onOutputSurface(mYuvReader.getSurface(), mFormat); + } mProcessor.onResolutionUpdate(mResolution, mPostviewResolution); - mProcessor.onImageFormatUpdate(mFormat); + mProcessor.onImageFormatUpdate(ImageFormat.YUV_420_888); } } private void initializePostviewPipeline() throws RemoteException { if ((mFormat != -1) && (mPostviewOutputSurface != null) && (mPostviewResolution != null) && (mPostviewYuvReader == null)) { - // Jpeg/blobs are expected to be configured with (w*h)x1 - mPostviewOutputWriter = ImageWriter.newInstance(mPostviewOutputSurface, 1/*maxImages*/, - ImageFormat.JPEG, mPostviewResolution.width * mPostviewResolution.height, 1); - mPostviewYuvReader = ImageReader.newInstance(mPostviewResolution.width, - mPostviewResolution.height, mFormat, JPEG_QUEUE_SIZE); - mPostviewYuvReader.setOnImageAvailableListener( - new YuvCallback(mPostviewYuvReader, mPostviewOutputWriter), mHandler); - mProcessor.onPostviewOutputSurface(mPostviewYuvReader.getSurface()); + if (Flags.extension10Bit() && mPostviewFormat == ImageFormat.YUV_420_888) { + // For the case when postview is YUV and capture is JPEG + mProcessor.onPostviewOutputSurface(mPostviewOutputSurface); + } else { + // Jpeg/blobs are expected to be configured with (w*h)x1 + mPostviewOutputWriter = ImageWriter.newInstance(mPostviewOutputSurface, + 1/*maxImages*/, ImageFormat.JPEG, + mPostviewResolution.width * mPostviewResolution.height, 1); + mPostviewYuvReader = ImageReader.newInstance(mPostviewResolution.width, + mPostviewResolution.height, mFormat, JPEG_QUEUE_SIZE); + mPostviewYuvReader.setOnImageAvailableListener( + new YuvCallback(mPostviewYuvReader, mPostviewOutputWriter), mHandler); + mProcessor.onPostviewOutputSurface(mPostviewYuvReader.getSurface()); + } mProcessor.onResolutionUpdate(mResolution, mPostviewResolution); - mProcessor.onImageFormatUpdate(mFormat); + mProcessor.onImageFormatUpdate(ImageFormat.YUV_420_888); } } diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index c00e6101b363..3ae319999e35 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -390,7 +390,16 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { if (surfaceInfo.mFormat == ImageFormat.JPEG) { mImageJpegProcessor = new CameraExtensionJpegProcessor(mImageProcessor); mImageProcessor = mImageJpegProcessor; + } else if (Flags.extension10Bit() && mClientPostviewSurface != null) { + // Handles case when postview is JPEG and capture is YUV + CameraExtensionUtils.SurfaceInfo postviewSurfaceInfo = + CameraExtensionUtils.querySurface(mClientPostviewSurface); + if (postviewSurfaceInfo.mFormat == ImageFormat.JPEG) { + mImageJpegProcessor = new CameraExtensionJpegProcessor(mImageProcessor); + mImageProcessor = mImageJpegProcessor; + } } + mBurstCaptureImageReader = ImageReader.newInstance(surfaceInfo.mWidth, surfaceInfo.mHeight, CameraExtensionCharacteristics.PROCESSING_INPUT_FORMAT, mImageExtender.getMaxCaptureStage()); diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java index f0c6e2e4e123..40f047732c06 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java @@ -112,19 +112,30 @@ public final class CameraExtensionUtils { if (outputConfig == null) return null; SurfaceInfo surfaceInfo = querySurface(outputConfig.getSurface()); - if (surfaceInfo.mFormat == captureFormat) { - if (supportedPostviewSizes.containsKey(captureFormat)) { - Size postviewSize = new Size(surfaceInfo.mWidth, surfaceInfo.mHeight); - if (supportedPostviewSizes.get(surfaceInfo.mFormat) - .contains(postviewSize)) { - return outputConfig.getSurface(); - } else { - throw new IllegalArgumentException("Postview size not supported!"); - } + + if (Flags.extension10Bit()) { + Size postviewSize = new Size(surfaceInfo.mWidth, surfaceInfo.mHeight); + if (supportedPostviewSizes.get(surfaceInfo.mFormat) + .contains(postviewSize)) { + return outputConfig.getSurface(); + } else { + throw new IllegalArgumentException("Postview size not supported!"); } } else { - throw new IllegalArgumentException("Postview format should be equivalent to " + - " the capture format!"); + if (surfaceInfo.mFormat == captureFormat) { + if (supportedPostviewSizes.containsKey(captureFormat)) { + Size postviewSize = new Size(surfaceInfo.mWidth, surfaceInfo.mHeight); + if (supportedPostviewSizes.get(surfaceInfo.mFormat) + .contains(postviewSize)) { + return outputConfig.getSurface(); + } else { + throw new IllegalArgumentException("Postview size not supported!"); + } + } + } else { + throw new IllegalArgumentException("Postview format should be equivalent to " + + " the capture format!"); + } } return null; diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 6b5e17d8563d..b58830861c5e 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -180,3 +180,11 @@ flag { description: "Use runtime permission state to determine appop state" bug: "266164193" } + +flag { + name: "device_id_in_op_proxy_info_enabled" + is_fixed_read_only: true + namespace: "permissions" + description: "Enable getDeviceId API in OpEventProxyInfo" + bug: "337340961" +}
\ No newline at end of file diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 72ab970b4b80..e6ddf3556490 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1736,6 +1736,24 @@ public final class Settings { "android.settings.NETWORK_OPERATOR_SETTINGS"; /** + * Activity Action: Show settings for selecting the network provider. + * <p> + * In some cases, a matching Activity may not be provided, so ensure you + * safeguard against this. + * <p> + * Access to this preference can be customized via Settings' app. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NETWORK_PROVIDER_SETTINGS = + "android.settings.NETWORK_PROVIDER_SETTINGS"; + + /** * Activity Action: Show settings for selection of 2G/3G. * <p> * In some cases, a matching Activity may not exist, so ensure you diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 997c958187fe..5f6bdbf193b9 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1383,16 +1383,22 @@ public class DreamService extends Service implements Window.Callback { DreamService.DREAM_META_DATA, DREAM_META_DATA_ROOT_TAG, com.android.internal.R.styleable.Dream)) { if (rawMetadata == null) return null; - return new DreamMetadata( - convertToComponentName( - rawMetadata.getString( - com.android.internal.R.styleable.Dream_settingsActivity), serviceInfo), - rawMetadata.getDrawable( - com.android.internal.R.styleable.Dream_previewImage), - rawMetadata.getBoolean(R.styleable.Dream_showClockAndComplications, - DEFAULT_SHOW_COMPLICATIONS), - rawMetadata.getInt(R.styleable.Dream_dreamCategory, DREAM_CATEGORY_DEFAULT) - ); + try { + return new DreamMetadata( + convertToComponentName( + rawMetadata.getString( + com.android.internal.R.styleable.Dream_settingsActivity), + serviceInfo), + rawMetadata.getDrawable( + com.android.internal.R.styleable.Dream_previewImage), + rawMetadata.getBoolean(R.styleable.Dream_showClockAndComplications, + DEFAULT_SHOW_COMPLICATIONS), + rawMetadata.getInt(R.styleable.Dream_dreamCategory, DREAM_CATEGORY_DEFAULT) + ); + } catch (Exception exception) { + Log.e(TAG, "Failed to create read metadata", exception); + return null; + } } } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 8dee4b19c6d3..c674968bba8a 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -70,6 +70,11 @@ import java.util.Locale; * For text that will not change, use a {@link StaticLayout}. */ public abstract class Layout { + + // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h + private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 4f; + private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0.2f; + /** @hide */ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { LineBreaker.BREAK_STRATEGY_SIMPLE, @@ -494,9 +499,9 @@ public abstract class Layout { drawText(canvas, firstLine, lastLine); - // Since high contrast text draws a solid rectangle background behind the text, it covers up - // the highlights and selections. In this case we draw over the top of the text with a - // blend mode that ensures the text stays high-contrast. + // Since high contrast text draws a thick border on the text, the highlight actually makes + // it harder to read. In this case we draw over the top of the text with a blend mode that + // ensures the text stays high-contrast. if (shouldDrawHighlightsOnTop(canvas)) { drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, cursorOffsetVertical, firstLine, lastLine); @@ -922,6 +927,9 @@ public abstract class Layout { public void drawBackground( @NonNull Canvas canvas, int firstLine, int lastLine) { + + drawHighContrastBackground(canvas, firstLine, lastLine); + // First, draw LineBackgroundSpans. // LineBackgroundSpans know nothing about the alignment, margins, or // direction of the layout or line. XXX: Should they? @@ -988,6 +996,66 @@ public abstract class Layout { } /** + * Draws a solid rectangle behind the text, the same color as the high contrast stroke border, + * to make it even easier to read. + * + * <p>We draw it here instead of in DrawTextFunctor so that multiple spans don't draw + * backgrounds over each other's text. + */ + private void drawHighContrastBackground(@NonNull Canvas canvas, int firstLine, int lastLine) { + if (!shouldDrawHighlightsOnTop(canvas)) { + return; + } + + var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, + mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); + + var bgPaint = mWorkPlainPaint; + bgPaint.reset(); + bgPaint.setColor(isHighContrastTextDark() ? Color.WHITE : Color.BLACK); + bgPaint.setStyle(Paint.Style.FILL); + + int start = getLineStart(firstLine); + int end = getLineEnd(lastLine); + // Draw a separate background rectangle for each line of text, that only surrounds the + // characters on that line. + forEachCharacterBounds( + start, + end, + firstLine, + lastLine, + new CharacterBoundsListener() { + int mLastLineNum = -1; + final RectF mLineBackground = new RectF(); + + @Override + public void onCharacterBounds(int index, int lineNum, float left, float top, + float right, float bottom) { + if (lineNum != mLastLineNum) { + drawRect(); + mLineBackground.set(left, top, right, bottom); + mLastLineNum = lineNum; + } else { + mLineBackground.union(left, top, right, bottom); + } + } + + @Override + public void onEnd() { + drawRect(); + } + + private void drawRect() { + if (!mLineBackground.isEmpty()) { + mLineBackground.inset(-padding, -padding); + canvas.drawRect(mLineBackground, bgPaint); + } + } + } + ); + } + + /** * @param canvas * @return The range of lines that need to be drawn, possibly empty. * @hide @@ -1682,7 +1750,7 @@ public abstract class Layout { } if (bounds == null) { - throw new IllegalArgumentException("bounds can't be null."); + throw new IllegalArgumentException("bounds can't be null."); } final int neededLength = 4 * (end - start); @@ -1698,6 +1766,34 @@ public abstract class Layout { final int startLine = getLineForOffset(start); final int endLine = getLineForOffset(end - 1); + + forEachCharacterBounds(start, end, startLine, endLine, + (index, lineNum, left, lineTop, right, lineBottom) -> { + final int boundsIndex = boundsStart + 4 * (index - start); + bounds[boundsIndex] = left; + bounds[boundsIndex + 1] = lineTop; + bounds[boundsIndex + 2] = right; + bounds[boundsIndex + 3] = lineBottom; + }); + } + + /** + * Return the characters' bounds in the given range. The coordinates are in local text layout. + * + * @param start the start index to compute the character bounds, inclusive. + * @param end the end index to compute the character bounds, exclusive. + * @param startLine index of the line that contains {@code start} + * @param endLine index of the line that contains {@code end} + * @param listener called for each character with its bounds + * + */ + private void forEachCharacterBounds( + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @IntRange(from = 0) int startLine, + @IntRange(from = 0) int endLine, + CharacterBoundsListener listener + ) { float[] horizontalBounds = null; for (int line = startLine; line <= endLine; ++line) { final int lineStart = getLineStart(line); @@ -1722,13 +1818,10 @@ public abstract class Layout { final float left = horizontalBounds[offset * 2] + lineStartPos; final float right = horizontalBounds[offset * 2 + 1] + lineStartPos; - final int boundsIndex = boundsStart + 4 * (index - start); - bounds[boundsIndex] = left; - bounds[boundsIndex + 1] = lineTop; - bounds[boundsIndex + 2] = right; - bounds[boundsIndex + 3] = lineBottom; + listener.onCharacterBounds(index, line, left, lineTop, right, lineBottom); } } + listener.onEnd(); } /** @@ -4443,4 +4536,15 @@ public abstract class Layout { public Paint.FontMetrics getMinimumFontMetrics() { return mMinimumFontMetrics; } + + /** + * Callback for {@link #forEachCharacterBounds(int, int, int, int, CharacterBoundsListener)} + */ + private interface CharacterBoundsListener { + void onCharacterBounds(int index, int lineNum, float left, float top, float right, + float bottom); + + /** Called after the last character has been sent to {@link #onCharacterBounds}. */ + default void onEnd() {} + } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index ddead88b128f..f4d240882e93 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -912,12 +912,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final String AUTOFILL_LOG_TAG = "View.Autofill"; /** - * The logging tag used by this class when logging verbose and chatty (high volume) - * autofill-related messages. - */ - private static final String AUTOFILL_CHATTY_LOG_TAG = "View.Autofill.Chatty"; - - /** * The logging tag used by this class when logging content capture-related messages. */ private static final String CONTENT_CAPTURE_LOG_TAG = "View.ContentCapture"; @@ -8708,8 +8702,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @CallSuper protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, @Nullable Rect previouslyFocusedRect) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "onFocusChanged() entered. gainFocus: " + if (DBG) { + Log.d(VIEW_LOG_TAG, "onFocusChanged() entered. gainFocus: " + gainFocus); } if (gainFocus) { @@ -8777,8 +8771,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (canNotifyAutofillEnterExitEvent()) { AutofillManager afm = getAutofillManager(); if (afm != null) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, this + " afm is not null"); + if (DBG) { + Log.d(VIEW_LOG_TAG, this + " afm is not null"); } if (enter) { // We have not been laid out yet, hence cannot evaluate @@ -8791,8 +8785,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // animation beginning. On the time, the view is not visible // to the user. And then as the animation progresses, the view // becomes visible to the user. - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, + if (DBG) { + Log.d(VIEW_LOG_TAG, "notifyEnterOrExitForAutoFillIfNeeded:" + " isLaidOut(): " + isLaidOut() + " isVisibleToUser(): " + isVisibleToUser() @@ -11024,28 +11018,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private boolean isAutofillable() { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "isAutofillable() entered."); + if (DBG) { + Log.d(VIEW_LOG_TAG, "isAutofillable() entered."); } if (getAutofillType() == AUTOFILL_TYPE_NONE) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "getAutofillType() returns AUTOFILL_TYPE_NONE"); + if (DBG) { + Log.d(VIEW_LOG_TAG, "getAutofillType() returns AUTOFILL_TYPE_NONE"); } return false; } final AutofillManager afm = getAutofillManager(); if (afm == null) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "AutofillManager is null"); + if (DBG) { + Log.d(VIEW_LOG_TAG, "AutofillManager is null"); } return false; } // Check whether view is not part of an activity. If it's not, return false. if (getAutofillViewId() <= LAST_APP_AUTOFILL_ID) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "getAutofillViewId()<=LAST_APP_AUTOFILL_ID"); + if (DBG) { + Log.d(VIEW_LOG_TAG, "getAutofillViewId()<=LAST_APP_AUTOFILL_ID"); } return false; } @@ -11056,9 +11050,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((isImportantForAutofill() && afm.isTriggerFillRequestOnFilteredImportantViewsEnabled()) || (!isImportantForAutofill() && afm.isTriggerFillRequestOnUnimportantViewEnabled())) { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, - "isImportantForAutofill(): " + isImportantForAutofill() + if (DBG) { + Log.d(VIEW_LOG_TAG, "isImportantForAutofill(): " + isImportantForAutofill() + "afm.isAutofillable(): " + afm.isAutofillable(this)); } return afm.isAutofillable(this) ? true : notifyAugmentedAutofillIfNeeded(afm); @@ -11066,8 +11059,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // If the previous condition is not met, fall back to the previous way to trigger fill // request based on autofill importance instead. - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "isImportantForAutofill(): " + isImportantForAutofill()); + if (DBG) { + Log.d(VIEW_LOG_TAG, "isImportantForAutofill(): " + isImportantForAutofill()); } return isImportantForAutofill() ? true : notifyAugmentedAutofillIfNeeded(afm); } @@ -11083,8 +11076,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** @hide */ public boolean canNotifyAutofillEnterExitEvent() { - if (Log.isLoggable(AUTOFILL_CHATTY_LOG_TAG, Log.VERBOSE)) { - Log.v(AUTOFILL_CHATTY_LOG_TAG, "canNotifyAutofillEnterExitEvent() entered. " + if (DBG) { + Log.d(VIEW_LOG_TAG, "canNotifyAutofillEnterExitEvent() entered. " + " isAutofillable(): " + isAutofillable() + " isAttachedToWindow(): " + isAttachedToWindow()); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c7ca16b6d1bd..f51d909e9e8b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4257,8 +4257,8 @@ public final class ViewRootImpl implements ViewParent, // when the values are applicable. if (mDrawnThisFrame) { mDrawnThisFrame = false; - updateInfrequentCount(); setCategoryFromCategoryCounts(); + updateInfrequentCount(); setPreferredFrameRate(mPreferredFrameRate); setPreferredFrameRateCategory(mPreferredFrameRateCategory); if (!mIsFrameRateConflicted) { @@ -4276,6 +4276,10 @@ public final class ViewRootImpl implements ViewParent, mPreferredFrameRate = -1; mIsFrameRateConflicted = false; mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN; + } else if (mPreferredFrameRate == 0) { + // From MSG_FRAME_RATE_SETTING, where mPreferredFrameRate is set to 0 + setPreferredFrameRate(0); + mPreferredFrameRate = -1; } } diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 98ff3c6bc347..cd13c4abac0b 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -2,6 +2,13 @@ package: "com.android.window.flags" container: "system" flag { + name: "disable_thin_letterboxing_reachability" + namespace: "large_screen_experiences_app_compat" + description: "Whether reachability is disabled in case of thin letterboxing" + bug: "334077350" +} + +flag { name: "allows_screen_size_decoupled_from_status_bar_and_cutout" namespace: "large_screen_experiences_app_compat" description: "When necessary, configuration decoupled from status bar and display cutout" diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java index 7c7c7b8fa51d..9f9aae53e0af 100644 --- a/core/java/com/android/internal/os/PowerStats.java +++ b/core/java/com/android/internal/os/PowerStats.java @@ -473,7 +473,14 @@ public final class PowerStats { } finally { // Unconditionally skip to the end of the written data, even if the actual parcel // format is incompatible - parcel.setDataPosition(endPos); + if (endPos > parcel.dataPosition()) { + if (endPos >= parcel.dataSize()) { + throw new IndexOutOfBoundsException( + "PowerStats end position: " + endPos + " is outside the parcel bounds: " + + parcel.dataSize()); + } + parcel.setDataPosition(endPos); + } } } diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java index 69d3d6a6d521..c21a43e807a9 100644 --- a/core/java/com/android/internal/util/ScreenshotHelper.java +++ b/core/java/com/android/internal/util/ScreenshotHelper.java @@ -53,8 +53,6 @@ public class ScreenshotHelper { public ScreenshotHelper(Context context) { mContext = context; - IntentFilter filter = new IntentFilter(ACTION_USER_SWITCHED); - mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED); } /** @@ -108,6 +106,8 @@ public class ScreenshotHelper { public void takeScreenshotInternal(ScreenshotRequest request, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer, long timeoutMs) { synchronized (mScreenshotLock) { + mContext.registerReceiver(mBroadcastReceiver, + new IntentFilter(ACTION_USER_SWITCHED), Context.RECEIVER_EXPORTED); final Runnable mScreenshotTimeout = () -> { synchronized (mScreenshotLock) { @@ -223,6 +223,11 @@ public class ScreenshotHelper { mScreenshotConnection = null; mScreenshotService = null; } + try { + mContext.unregisterReceiver(mBroadcastReceiver); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Attempted to remove broadcast receiver twice"); + } } /** diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index e2106c53f565..77a99120072e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8233,7 +8233,7 @@ </activity> <activity android:name="com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity" android:exported="false" - android:theme="@style/Theme.DeviceDefault.Resolver" + android:theme="@style/AccessibilityButtonChooserDialog" android:finishOnCloseSystemDialogs="true" android:excludeFromRecents="true" android:documentLaunchMode="never" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 0d1be3814cb5..50ed4228fa92 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -764,6 +764,9 @@ <!-- Indicates whether to enable hinge angle sensor when using unfold animation --> <bool name="config_unfoldTransitionHingeAngle">false</bool> + <!-- Indicates whether to enable haptics during unfold animation --> + <bool name="config_unfoldTransitionHapticsEnabled">false</bool> + <!-- Indicates the time needed to time out the fold animation if the device stops in half folded mode. --> <integer name="config_unfoldTransitionHalfFoldedTimeout">1000</integer> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 5945f81704c5..a46dc045269d 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1685,4 +1685,9 @@ please see styles_device_defaults.xml. <item name="android:paddingStart">8dp</item> <item name="android:background">?android:attr/selectableItemBackground</item> </style> + + <style name="AccessibilityButtonChooserDialog" + parent="@style/Theme.DeviceDefault.Resolver"> + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + </style> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index daf12301ea0f..d432c05d199f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4219,6 +4219,7 @@ <java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" /> <java-symbol type="bool" name="config_unfoldTransitionEnabled" /> <java-symbol type="bool" name="config_unfoldTransitionHingeAngle" /> + <java-symbol type="bool" name="config_unfoldTransitionHapticsEnabled" /> <java-symbol type="integer" name="config_unfoldTransitionHalfFoldedTimeout" /> <java-symbol type="array" name="config_perDeviceStateRotationLockDefaults" /> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 5cc1ee46f61c..7fb894a9dbe9 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -86,6 +86,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemProperties; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -126,6 +127,7 @@ public class NotificationTest { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { @@ -1835,6 +1837,36 @@ public class NotificationTest { Assert.assertEquals(bitmap, resultBitmap); } + @Test + public void testGetWhen_zero() { + Notification n = new Notification.Builder(mContext, "test") + .setWhen(0) + .build(); + + mSetFlagsRule.disableFlags(Flags.FLAG_SORT_SECTION_BY_TIME); + + assertThat(n.getWhen()).isEqualTo(0); + + mSetFlagsRule.enableFlags(Flags.FLAG_SORT_SECTION_BY_TIME); + + assertThat(n.getWhen()).isEqualTo(n.creationTime); + } + + @Test + public void testGetWhen_devProvidedNonZero() { + Notification n = new Notification.Builder(mContext, "test") + .setWhen(9) + .build(); + + mSetFlagsRule.disableFlags(Flags.FLAG_SORT_SECTION_BY_TIME); + + assertThat(n.getWhen()).isEqualTo(9); + + mSetFlagsRule.enableFlags(Flags.FLAG_SORT_SECTION_BY_TIME); + + assertThat(n.getWhen()).isEqualTo(9); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index f60eff69690a..1c1236279b61 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -18,9 +18,6 @@ package android.text; import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -47,6 +44,8 @@ import android.text.style.StrikethroughSpan; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.google.common.truth.Expect; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -63,7 +62,13 @@ public class LayoutTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final Expect expect = Expect.create(); + + // Line count when using MockLayout private static final int LINE_COUNT = 5; + // Actual line count when using StaticLayout + private static final int STATIC_LINE_COUNT = 9; private static final int LINE_HEIGHT = 12; private static final int LINE_DESCENT = 4; private static final CharSequence LAYOUT_TEXT = "alwei\t;sdfs\ndf @"; @@ -655,8 +660,8 @@ public class LayoutTest { @Test @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) public void highContrastTextEnabled_testDrawSelectionAndHighlight_drawsHighContrastSelectionAndHighlight() { - Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, - mAlign, mSpacingMult, mSpacingAdd); + Layout layout = new StaticLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); List<Path> highlightPaths = new ArrayList<>(); List<Paint> highlightPaints = new ArrayList<>(); @@ -677,9 +682,12 @@ public class LayoutTest { layout.draw(c, highlightPaths, highlightPaints, selectionPath, selectionPaint, /* cursorOffsetVertical= */ 0); List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); - var textsDrawn = LINE_COUNT; + var textsDrawn = STATIC_LINE_COUNT; var highlightsDrawn = 2; - assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn); + var backgroundRectsDrawn = STATIC_LINE_COUNT; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); var highlightsFound = 0; var curLineIndex = 0; @@ -687,29 +695,26 @@ public class LayoutTest { MockCanvas.DrawCommand drawCommand = drawCommands.get(i); if (drawCommand.path != null) { - assertThat(drawCommand.path).isEqualTo(selectionPath); - assertThat(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW); - assertThat(drawCommand.paint.getBlendMode()).isNotNull(); + expect.that(drawCommand.path).isEqualTo(selectionPath); + expect.that(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW); + expect.that(drawCommand.paint.getBlendMode()).isNotNull(); highlightsFound++; } else if (drawCommand.text != null) { - int start = layout.getLineStart(curLineIndex); - int end = layout.getLineEnd(curLineIndex); - assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text); curLineIndex++; - assertWithMessage("highlight is drawn on top of text") + expect.withMessage("highlight is drawn on top of text") .that(highlightsFound).isEqualTo(0); } } - assertThat(highlightsFound).isEqualTo(2); + expect.that(highlightsFound).isEqualTo(2); } @Test @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) public void highContrastTextEnabled_testDrawHighlight_drawsHighContrastHighlight() { - Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, - mAlign, mSpacingMult, mSpacingAdd); + Layout layout = new StaticLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); List<Path> highlightPaths = new ArrayList<>(); List<Paint> highlightPaints = new ArrayList<>(); @@ -730,9 +735,12 @@ public class LayoutTest { layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null, /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0); List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); - var textsDrawn = LINE_COUNT; + var textsDrawn = STATIC_LINE_COUNT; var highlightsDrawn = 1; - assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn); + var backgroundRectsDrawn = STATIC_LINE_COUNT; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); var highlightsFound = 0; var curLineIndex = 0; @@ -740,29 +748,26 @@ public class LayoutTest { MockCanvas.DrawCommand drawCommand = drawCommands.get(i); if (drawCommand.path != null) { - assertThat(drawCommand.path).isEqualTo(selectionPath); - assertThat(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW); - assertThat(drawCommand.paint.getBlendMode()).isNotNull(); + expect.that(drawCommand.path).isEqualTo(selectionPath); + expect.that(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW); + expect.that(drawCommand.paint.getBlendMode()).isNotNull(); highlightsFound++; } else if (drawCommand.text != null) { - int start = layout.getLineStart(curLineIndex); - int end = layout.getLineEnd(curLineIndex); - assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text); curLineIndex++; - assertWithMessage("highlight is drawn on top of text") + expect.withMessage("highlight is drawn on top of text") .that(highlightsFound).isEqualTo(0); } } - assertThat(highlightsFound).isEqualTo(1); + expect.that(highlightsFound).isEqualTo(1); } @Test @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) public void highContrastTextDisabledByDefault_testDrawHighlight_drawsNormalHighlightBehind() { - Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, - mAlign, mSpacingMult, mSpacingAdd); + Layout layout = new StaticLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); List<Path> highlightPaths = new ArrayList<>(); List<Paint> highlightPaints = new ArrayList<>(); @@ -782,9 +787,12 @@ public class LayoutTest { layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null, /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0); List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); - var textsDrawn = LINE_COUNT; + var textsDrawn = STATIC_LINE_COUNT; var highlightsDrawn = 1; - assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn); + var backgroundRectsDrawn = 0; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); var highlightsFound = 0; var curLineIndex = 0; @@ -792,29 +800,26 @@ public class LayoutTest { MockCanvas.DrawCommand drawCommand = drawCommands.get(i); if (drawCommand.path != null) { - assertThat(drawCommand.path).isEqualTo(selectionPath); - assertThat(drawCommand.paint.getColor()).isEqualTo(Color.CYAN); - assertThat(drawCommand.paint.getBlendMode()).isNull(); + expect.that(drawCommand.path).isEqualTo(selectionPath); + expect.that(drawCommand.paint.getColor()).isEqualTo(Color.CYAN); + expect.that(drawCommand.paint.getBlendMode()).isNull(); highlightsFound++; } else if (drawCommand.text != null) { - int start = layout.getLineStart(curLineIndex); - int end = layout.getLineEnd(curLineIndex); - assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text); curLineIndex++; - assertWithMessage("highlight is drawn behind text") + expect.withMessage("highlight is drawn behind text") .that(highlightsFound).isGreaterThan(0); } } - assertThat(highlightsFound).isEqualTo(1); + expect.that(highlightsFound).isEqualTo(1); } @Test @RequiresFlagsDisabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) public void highContrastTextEnabledButFlagOff_testDrawHighlight_drawsNormalHighlightBehind() { - Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, - mAlign, mSpacingMult, mSpacingAdd); + Layout layout = new StaticLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); List<Path> highlightPaths = new ArrayList<>(); List<Paint> highlightPaints = new ArrayList<>(); @@ -835,9 +840,12 @@ public class LayoutTest { layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null, /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0); List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); - var textsDrawn = LINE_COUNT; + var textsDrawn = STATIC_LINE_COUNT; var highlightsDrawn = 1; - assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn); + var backgroundRectsDrawn = 0; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); var highlightsFound = 0; var curLineIndex = 0; @@ -845,33 +853,84 @@ public class LayoutTest { MockCanvas.DrawCommand drawCommand = drawCommands.get(i); if (drawCommand.path != null) { - assertThat(drawCommand.path).isEqualTo(selectionPath); - assertThat(drawCommand.paint.getColor()).isEqualTo(Color.CYAN); - assertThat(drawCommand.paint.getBlendMode()).isNull(); + expect.that(drawCommand.path).isEqualTo(selectionPath); + expect.that(drawCommand.paint.getColor()).isEqualTo(Color.CYAN); + expect.that(drawCommand.paint.getBlendMode()).isNull(); highlightsFound++; } else if (drawCommand.text != null) { - int start = layout.getLineStart(curLineIndex); - int end = layout.getLineEnd(curLineIndex); - assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text); curLineIndex++; - assertWithMessage("highlight is drawn behind text") + expect.withMessage("highlight is drawn behind text") .that(highlightsFound).isGreaterThan(0); } } - assertThat(highlightsFound).isEqualTo(1); + expect.that(highlightsFound).isEqualTo(1); } @Test public void mockCanvasHighContrastOverridesCorrectly() { var canvas = new MockCanvas(100, 100); - assertThat(canvas.isHighContrastTextEnabled()).isFalse(); + expect.that(canvas.isHighContrastTextEnabled()).isFalse(); canvas.setHighContrastTextEnabled(true); - assertThat(canvas.isHighContrastTextEnabled()).isTrue(); + expect.that(canvas.isHighContrastTextEnabled()).isTrue(); canvas.setHighContrastTextEnabled(false); - assertThat(canvas.isHighContrastTextEnabled()).isFalse(); + expect.that(canvas.isHighContrastTextEnabled()).isFalse(); + } + + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testDrawLightText_drawsBlackBackgroundRects() { + mTextPaint.setColor(Color.parseColor("#CCAA33")); + Layout layout = new StaticLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + final int width = 256; + final int height = 256; + MockCanvas c = new MockCanvas(width, height); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + var textsDrawn = STATIC_LINE_COUNT; + var highlightsDrawn = 0; + var backgroundRectsDrawn = STATIC_LINE_COUNT; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); + + int numBackgroundsFound = 0; + var curLineIndex = 0; + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + + if (drawCommand.rect != null) { + numBackgroundsFound++; + expect.that(drawCommand.paint.getColor()).isEqualTo(Color.BLACK); + expect.that(drawCommand.rect.height()).isAtLeast(LINE_HEIGHT); + expect.that(drawCommand.rect.width()).isGreaterThan(0); + float expectedY = (numBackgroundsFound) * (LINE_HEIGHT + LINE_DESCENT); + expect.that(drawCommand.rect.bottom).isAtLeast(expectedY); + } else if (drawCommand.text != null) { + // draw text + curLineIndex++; + + expect.withMessage("background is drawn on top of text") + .that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn); + } else { + fail("unexpected path drawn"); + } + } + + // One for each line + expect.that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn); } private static final class MockCanvas extends Canvas { @@ -881,22 +940,46 @@ public class LayoutTest { public final float x; public final float y; public final Path path; + public final RectF rect; public final Paint paint; DrawCommand(String text, float x, float y, Paint paint) { this.text = text; this.x = x; this.y = y; - this.paint = paint; + this.paint = new Paint(paint); path = null; + rect = null; } DrawCommand(Path path, Paint paint) { this.path = path; - this.paint = paint; + this.paint = new Paint(paint); y = 0; x = 0; text = null; + rect = null; + } + + DrawCommand(RectF rect, Paint paint) { + this.rect = new RectF(rect); + this.paint = new Paint(paint); + path = null; + y = 0; + x = 0; + text = null; + } + + @Override + public String toString() { + return "DrawCommand{" + + "text='" + text + '\'' + + ", x=" + x + + ", y=" + y + + ", path=" + path + + ", rect=" + rect + + ", paint=" + paint + + '}'; } } @@ -956,6 +1039,11 @@ public class LayoutTest { mDrawCommands.add(new DrawCommand(path, p)); } + @Override + public void drawRect(RectF rect, Paint p) { + mDrawCommands.add(new DrawCommand(rect, p)); + } + List<DrawCommand> getDrawCommands() { return mDrawCommands; } diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index f885e31ed270..32aec1a335f2 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -36,6 +36,7 @@ import static org.junit.Assert.assertTrue; import android.annotation.NonNull; import android.app.Activity; +import android.app.Instrumentation; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -47,6 +48,7 @@ import android.widget.FrameLayout; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; @@ -537,6 +539,28 @@ public class ViewFrameRateTest { }); waitForAfterDraw(); } + + @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY + }) + public void frameRateReset() throws Throwable { + mMovingView.setRequestedFrameRate(120f); + waitForFrameRateCategoryToSettle(); + mActivityRule.runOnUiThread(() -> mMovingView.setVisibility(View.INVISIBLE)); + + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + + for (int i = 0; i < 120; i++) { + mActivityRule.runOnUiThread(() -> { + mMovingView.getParent().onDescendantInvalidated(mMovingView, mMovingView); + }); + instrumentation.waitForIdleSync(); + } + + assertEquals(0f, mViewRoot.getLastPreferredFrameRate(), 0f); + } + private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 5caf77dbdcff..7c098f2a63f3 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -1138,6 +1138,8 @@ public class ViewRootImplTest { mView = new View(sContext); WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; sInstrumentation.runOnMainSync(() -> { WindowManager wm = sContext.getSystemService(WindowManager.class); @@ -1157,8 +1159,6 @@ public class ViewRootImplTest { Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { mView.invalidate(); - int expected = toolkitFrameRateDefaultNormalReadOnly() - ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; runAfterDraw(() -> assertEquals(expected, mViewRootImpl.getLastPreferredFrameRateCategory())); }); @@ -1168,8 +1168,6 @@ public class ViewRootImplTest { Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { mView.invalidate(); - int expected = toolkitFrameRateDefaultNormalReadOnly() - ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; runAfterDraw(() -> assertEquals(expected, mViewRootImpl.getLastPreferredFrameRateCategory())); }); @@ -1177,12 +1175,26 @@ public class ViewRootImplTest { // Infrequent update Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { - mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT); mView.invalidate(); runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRootImpl.getLastPreferredFrameRateCategory())); }); waitForAfterDraw(); + + // When the View vote, it's still considered as intermittent update state + sInstrumentation.runOnMainSync(() -> { + mView.invalidate(); + runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL, + mViewRootImpl.getLastPreferredFrameRateCategory())); + }); + waitForAfterDraw(); + + // Becomes frequent update state + sInstrumentation.runOnMainSync(() -> { + mView.invalidate(); + runAfterDraw(() -> assertEquals(expected, + mViewRootImpl.getLastPreferredFrameRateCategory())); + }); } /** diff --git a/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java b/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java index 6402206410b5..baab3b218746 100644 --- a/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/PowerStatsTest.java @@ -168,6 +168,20 @@ public class PowerStatsTest { assertThat(end).isEqualTo("END"); } + @Test + public void parceling_corruptParcel() { + PowerStats stats = new PowerStats(mDescriptor); + Parcel parcel = Parcel.obtain(); + stats.writeToParcel(parcel); + + Parcel newParcel = marshallAndUnmarshall(parcel); + newParcel.writeInt(-42); // Negative section length + newParcel.setDataPosition(0); + + PowerStats newStats = PowerStats.readFromParcel(newParcel, mRegistry); + assertThat(newStats).isNull(); + } + private static Parcel marshallAndUnmarshall(Parcel parcel) { byte[] bytes = parcel.marshall(); parcel.recycle(); diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt index e1bf40ca19dc..611013365e7d 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -105,18 +105,21 @@ class BubbleExpandedViewPinControllerTest { } @Test - fun onDragUpdate_stayOnSameSide() { + fun drag_stayOnSameSide() { runOnMainSync { controller.onDragStart(initialLocationOnLeft = false) controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + controller.onDragEnd() } waitForAnimateIn() assertThat(dropTargetView).isNull() assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) } @Test - fun onDragUpdate_toLeft() { + fun drag_toLeft() { + // Drag to left, but don't finish runOnMainSync { controller.onDragStart(initialLocationOnLeft = false) controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) @@ -132,10 +135,16 @@ class BubbleExpandedViewPinControllerTest { .isEqualTo(expectedDropTargetBounds.height()) assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT) + assertThat(testListener.locationReleases).isEmpty() + + // Finish the drag + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.LEFT) } @Test - fun onDragUpdate_toLeftAndBackToRight() { + fun drag_toLeftAndBackToRight() { + // Drag to left runOnMainSync { controller.onDragStart(initialLocationOnLeft = false) controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) @@ -143,6 +152,7 @@ class BubbleExpandedViewPinControllerTest { waitForAnimateIn() assertThat(dropTargetView).isNotNull() + // Drag to right runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) } // We have to wait for existing drop target to animate out and new to animate in waitForAnimateOut() @@ -158,10 +168,15 @@ class BubbleExpandedViewPinControllerTest { assertThat(testListener.locationChanges) .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT) + assertThat(testListener.locationReleases).isEmpty() + + // Release the view + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) } @Test - fun onDragUpdate_toLeftInExclusionRect() { + fun drag_toLeftInExclusionRect() { runOnMainSync { controller.onDragStart(initialLocationOnLeft = false) // Exclusion rect is around the bottom center area of the screen @@ -170,6 +185,10 @@ class BubbleExpandedViewPinControllerTest { waitForAnimateIn() assertThat(dropTargetView).isNull() assertThat(testListener.locationChanges).isEmpty() + assertThat(testListener.locationReleases).isEmpty() + + runOnMainSync { controller.onDragEnd() } + assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT) } @Test @@ -256,8 +275,13 @@ class BubbleExpandedViewPinControllerTest { internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { val locationChanges = mutableListOf<BubbleBarLocation>() + val locationReleases = mutableListOf<BubbleBarLocation>() override fun onChange(location: BubbleBarLocation) { locationChanges.add(location) } + + override fun onRelease(location: BubbleBarLocation) { + locationReleases.add(location) + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index 0297901c8921..f9a1d940c734 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -82,8 +82,8 @@ public class BadgedImageView extends ConstraintLayout { private BubbleViewProvider mBubble; private BubblePositioner mPositioner; - private boolean mOnLeft; - + private boolean mBadgeOnLeft; + private boolean mDotOnLeft; private DotRenderer mDotRenderer; private DotRenderer.DrawParams mDrawParams; private int mDotColor; @@ -153,7 +153,8 @@ public class BadgedImageView extends ConstraintLayout { public void hideDotAndBadge(boolean onLeft) { addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK); - mOnLeft = onLeft; + mBadgeOnLeft = onLeft; + mDotOnLeft = onLeft; hideBadge(); } @@ -185,7 +186,7 @@ public class BadgedImageView extends ConstraintLayout { mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; - mDrawParams.leftAlign = mOnLeft; + mDrawParams.leftAlign = mDotOnLeft; mDrawParams.scale = mDotScale; mDotRenderer.draw(canvas, mDrawParams); @@ -255,7 +256,7 @@ public class BadgedImageView extends ConstraintLayout { * Whether decorations (badges or dots) are on the left. */ boolean getDotOnLeft() { - return mOnLeft; + return mDotOnLeft; } /** @@ -263,7 +264,7 @@ public class BadgedImageView extends ConstraintLayout { */ float[] getDotCenter() { float[] dotPosition; - if (mOnLeft) { + if (mDotOnLeft) { dotPosition = mDotRenderer.getLeftDotPosition(); } else { dotPosition = mDotRenderer.getRightDotPosition(); @@ -291,22 +292,23 @@ public class BadgedImageView extends ConstraintLayout { if (onLeft != getDotOnLeft()) { if (shouldDrawDot()) { animateDotScale(0f /* showDot */, () -> { - mOnLeft = onLeft; + mDotOnLeft = onLeft; invalidate(); animateDotScale(1.0f, null /* after */); }); } else { - mOnLeft = onLeft; + mDotOnLeft = onLeft; } } + mBadgeOnLeft = onLeft; // TODO animate badge showBadge(); - } /** Sets the position of the dot and badge. */ void setDotBadgeOnLeft(boolean onLeft) { - mOnLeft = onLeft; + mBadgeOnLeft = onLeft; + mDotOnLeft = onLeft; invalidate(); showBadge(); } @@ -361,7 +363,7 @@ public class BadgedImageView extends ConstraintLayout { } int translationX; - if (mOnLeft) { + if (mBadgeOnLeft) { translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth()); } else { translationX = 0; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d2958779c0d4..edd5935b508d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -474,11 +474,16 @@ public class BubbleController implements ConfigurationChangeListener, mDisplayController.addDisplayChangingController( (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { - // This is triggered right before the rotation is applied - if (fromRotation != toRotation) { + Rect newScreenBounds = new Rect(); + if (newDisplayAreaInfo != null) { + newScreenBounds = + newDisplayAreaInfo.configuration.windowConfiguration.getBounds(); + } + // This is triggered right before the rotation or new screen size is applied + if (fromRotation != toRotation || !newScreenBounds.equals(mScreenBounds)) { if (mStackView != null) { // Layout listener set on stackView will update the positioner - // once the rotation is applied + // once the rotation or screen change is applied mStackView.onOrientationChanged(); } } @@ -725,6 +730,17 @@ public class BubbleController implements ConfigurationChangeListener, } } + /** + * Animate bubble bar to the given location. The location change is transient. It does not + * update the state of the bubble bar. + * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. + */ + public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + if (canShowAsBubbleBar()) { + mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation); + } + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -2250,15 +2266,19 @@ public class BubbleController implements ConfigurationChangeListener, private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener; private final Bubbles.BubbleStateListener mBubbleListener = new Bubbles.BubbleStateListener() { + @Override + public void onBubbleStateChange(BubbleBarUpdate update) { + Bundle b = new Bundle(); + b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); + b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); + mListener.call(l -> l.onBubbleStateChange(b)); + } - @Override - public void onBubbleStateChange(BubbleBarUpdate update) { - Bundle b = new Bundle(); - b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); - b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); - mListener.call(l -> l.onBubbleStateChange(b)); - } - }; + @Override + public void animateBubbleBarLocation(BubbleBarLocation location) { + mListener.call(l -> l.animateBubbleBarLocation(location)); + } + }; IBubblesImpl(BubbleController controller) { mController = controller; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 127a49fc7875..322088b17e63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -37,6 +37,7 @@ import android.window.ScreenCapture.SynchronousScreenCaptureListener; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.annotations.ExternalThread; @@ -304,6 +305,12 @@ public interface Bubbles { * Called when the bubbles state changes. */ void onBubbleStateChange(BubbleBarUpdate update); + + /** + * Called when bubble bar should temporarily be animated to a new location. + * Does not result in a state change. + */ + void animateBubbleBarLocation(BubbleBarLocation location); } /** Listener to find out about stack expansion / collapse events. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index e48f8d5f1c84..14d29cd887bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -15,8 +15,9 @@ */ package com.android.wm.shell.bubbles; -import android.os.Bundle; +import android.os.Bundle; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; /** * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. */ @@ -26,4 +27,10 @@ oneway interface IBubblesListener { * Called when the bubbles state changes. */ void onBubbleStateChange(in Bundle update); + + /** + * Called when bubble bar should temporarily be animated to a new location. + * Does not result in a state change. + */ + void animateBubbleBarLocation(in BubbleBarLocation location); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index fe9c4d4c9094..a51ac633ad86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -135,9 +135,9 @@ class BubbleBarExpandedViewDragController( private fun finishDrag() { if (!isStuckToDismiss) { - animationHelper.animateToRestPosition() pinController.onDragEnd() dragListener.onReleased(inDismiss = false) + animationHelper.animateToRestPosition() dismissView.hide() } isMoving = false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 62cc4da3193e..a351cef223b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -33,6 +33,8 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -42,6 +44,8 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; +import com.android.wm.shell.common.bubbles.BaseBubblePinController; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.DismissView; import kotlin.Unit; @@ -115,7 +119,18 @@ public class BubbleBarLayerView extends FrameLayout mBubbleExpandedViewPinController = new BubbleExpandedViewPinController( context, this, mPositioner); - mBubbleExpandedViewPinController.setListener(mBubbleController::setBubbleBarLocation); + mBubbleExpandedViewPinController.setListener( + new BaseBubblePinController.LocationChangeListener() { + @Override + public void onChange(@NonNull BubbleBarLocation bubbleBarLocation) { + mBubbleController.animateBubbleBarLocation(bubbleBarLocation); + } + + @Override + public void onRelease(@NonNull BubbleBarLocation location) { + mBubbleController.setBubbleBarLocation(location); + } + }); setOnClickListener(view -> hideMenuOrCollapse()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt index a008045a4b6f..e514f9d70599 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt @@ -82,6 +82,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi fun onDragEnd() { getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } dismissZone = null + listener?.onRelease(if (onLeft) LEFT else RIGHT) } /** @@ -170,14 +171,22 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi /** Receive updates on location changes */ interface LocationChangeListener { /** - * Bubble bar [BubbleBarLocation] has changed as a result of dragging + * Bubble bar has been dragged to a new [BubbleBarLocation]. And the drag is still in + * progress. * * Triggered when drag gesture passes the middle of the screen and before touch up. Can be * triggered multiple times per gesture. * * @param location new location as a result of the ongoing drag operation */ - fun onChange(location: BubbleBarLocation) + fun onChange(location: BubbleBarLocation) {} + + /** + * Bubble bar has been released in the [BubbleBarLocation]. + * + * @param location final location of the bubble bar once drag is released + */ + fun onRelease(location: BubbleBarLocation) } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 4eff3f03670e..6e61f22ca563 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -26,6 +26,7 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; @@ -43,6 +44,7 @@ import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; +import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -69,9 +71,11 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, PipTouchHandler pipTouchHandler, - @NonNull PipScheduler pipScheduler) { + @NonNull PipScheduler pipScheduler, + @NonNull PipTransitionState pipStackListenerController) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); + pipBoundsState, null, pipBoundsAlgorithm, pipScheduler, + pipStackListenerController); } @WMSingleton @@ -85,6 +89,9 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -92,7 +99,7 @@ public abstract class Pip2Module { return Optional.ofNullable(PipController.create( context, shellInit, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, - mainExecutor)); + taskStackListener, shellTaskOrganizer, pipTransitionState, mainExecutor)); } } @@ -101,8 +108,8 @@ public abstract class Pip2Module { static PipScheduler providePipScheduler(Context context, PipBoundsState pipBoundsState, @ShellMainThread ShellExecutor mainExecutor, - ShellTaskOrganizer shellTaskOrganizer) { - return new PipScheduler(context, pipBoundsState, mainExecutor, shellTaskOrganizer); + PipTransitionState pipTransitionState) { + return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState); } @WMSingleton @@ -146,4 +153,10 @@ public abstract class Pip2Module { return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, floatingContentCoordinator, pipPerfHintControllerOptional); } + + @WMSingleton + @Provides + static PipTransitionState providePipStackListenerController() { + return new PipTransitionState(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 4f71a02528c3..7730285c86c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -54,7 +54,6 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; /** * Responsible supplying PiP Transitions. @@ -125,12 +124,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start resizing Pip transition/animation. - * - * @param onFinishResizeCallback callback guaranteed to execute when animation ends and - * client completes any potential draws upon WM state updates. */ - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { // Default implementation does nothing. } @@ -266,9 +261,9 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** Whether a particular package is same as current pip package. */ - public boolean isInPipPackage(String packageName) { + public boolean isPackageActiveInPip(String packageName) { final TaskInfo inPipTask = mPipOrganizer.getTaskInfo(); - return packageName != null && inPipTask != null + return packageName != null && inPipTask != null && mPipOrganizer.isInPip() && packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 1e18b8c002db..a12882f56eb7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -16,25 +16,31 @@ package com.android.wm.shell.pip2.phone; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; import android.view.InsetsState; import android.view.SurfaceControl; import androidx.annotation.BinderThread; +import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; @@ -42,6 +48,8 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.IPip; import com.android.wm.shell.common.pip.IPipAnimationListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -57,8 +65,11 @@ import com.android.wm.shell.sysui.ShellInit; * Manages the picture-in-picture (PIP) UI and states for Phones. */ public class PipController implements ConfigurationChangeListener, + PipTransitionState.PipTransitionStateChangedListener, DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); + private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds"; + private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; private final Context mContext; private final ShellController mShellController; @@ -68,6 +79,9 @@ public class PipController implements ConfigurationChangeListener, private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipScheduler mPipScheduler; + private final TaskStackListenerImpl mTaskStackListener; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final PipTransitionState mPipTransitionState; private final ShellExecutor mMainExecutor; // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. @@ -104,6 +118,9 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -113,6 +130,10 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipDisplayLayoutState = pipDisplayLayoutState; mPipScheduler = pipScheduler; + mTaskStackListener = taskStackListener; + mShellTaskOrganizer = shellTaskOrganizer; + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); mMainExecutor = mainExecutor; if (PipUtils.isPip2ExperimentEnabled()) { @@ -132,6 +153,9 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -140,7 +164,8 @@ public class PipController implements ConfigurationChangeListener, } return new PipController(context, shellInit, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, - pipScheduler, mainExecutor); + pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, + mainExecutor); } private void onInit() { @@ -164,6 +189,17 @@ public class PipController implements ConfigurationChangeListener, mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, this::createExternalInterface, this); mShellController.addConfigurationChangeListener(this); + + mTaskStackListener.addListener(new TaskStackListenerCallback() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { + return; + } + mPipScheduler.scheduleExitPipViaExpand(); + } + }); } private ExternalInterfaceBinder createExternalInterface() { @@ -245,11 +281,46 @@ public class PipController implements ConfigurationChangeListener, Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onSwipePipToHomeAnimationStart: %s", componentName); - mPipScheduler.onSwipePipToHomeAnimationStart(taskId, componentName, destinationBounds, - overlay, appBounds); + Bundle extra = new Bundle(); + extra.putParcelable(SWIPE_TO_PIP_OVERLAY, overlay); + extra.putParcelable(SWIPE_TO_PIP_APP_BOUNDS, appBounds); + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP, extra); + if (overlay != null) { + // Shell transitions might use a root animation leash, which will be removed when + // the Recents transition is finished. Launcher attaches the overlay leash to this + // animation target leash; thus, we need to reparent it to the actual Task surface now. + // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP + // transition. + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx); + tx.setLayer(overlay, Integer.MAX_VALUE); + tx.apply(); + } mPipRecentsAnimationListener.onPipAnimationStarted(); } + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + if (newState == PipTransitionState.SWIPING_TO_PIP) { + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + SurfaceControl overlay = extra.getParcelable( + SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); + Rect appBounds = extra.getParcelable( + SWIPE_TO_PIP_APP_BOUNDS, Rect.class); + + Preconditions.checkState(appBounds != null, + "App bounds can't be null for " + mPipTransitionState); + mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); + } else if (newState == PipTransitionState.ENTERED_PIP) { + if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipTransitionState.resetSwipePipToHomeState(); + } + } + } + // // IPipAnimationListener Binder proxy helpers // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index b4ca7df10292..72fa3badeb93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -21,21 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Rect; -import android.view.SurfaceControl; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; @@ -43,7 +38,6 @@ import com.android.wm.shell.pip.PipTransitionController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.function.Consumer; /** * Scheduler for Shell initiated PiP transitions and animations. @@ -55,31 +49,10 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; private final ShellExecutor mMainExecutor; - private final ShellTaskOrganizer mShellTaskOrganizer; + private final PipTransitionState mPipTransitionState; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; - // pinned PiP task's WC token - @Nullable - private WindowContainerToken mPipTaskToken; - - // pinned PiP task's leash - @Nullable - private SurfaceControl mPinnedTaskLeash; - - // true if Launcher has started swipe PiP to home animation - private boolean mInSwipePipToHomeTransition; - - // Overlay leash potentially used during swipe PiP to home transition; - // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. - @Nullable - SurfaceControl mSwipePipToHomeOverlay; - - // App bounds used when as a starting point to swipe PiP to home animation in Launcher; - // these are also used to calculate the app icon overlay buffer size. - @NonNull - final Rect mSwipePipToHomeAppBounds = new Rect(); - /** * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -118,11 +91,11 @@ public class PipScheduler { public PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, - ShellTaskOrganizer shellTaskOrganizer) { + PipTransitionState pipTransitionState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; - mShellTaskOrganizer = shellTaskOrganizer; + mPipTransitionState = pipTransitionState; if (PipUtils.isPip2ExperimentEnabled()) { // temporary broadcast receiver to initiate exit PiP via expand @@ -140,25 +113,17 @@ public class PipScheduler { mPipTransitionController = pipTransitionController; } - void setPinnedTaskLeash(SurfaceControl pinnedTaskLeash) { - mPinnedTaskLeash = pinnedTaskLeash; - } - - void setPipTaskToken(@Nullable WindowContainerToken pipTaskToken) { - mPipTaskToken = pipTaskToken; - } - @Nullable private WindowContainerTransaction getExitPipViaExpandTransaction() { - if (mPipTaskToken == null) { + if (mPipTransitionState.mPipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); // final expanded bounds to be inherited from the parent - wct.setBounds(mPipTaskToken, null); + wct.setBounds(mPipTransitionState.mPipTaskToken, null); // if we are hitting a multi-activity case // windowing mode change will reparent to original host task - wct.setWindowingMode(mPipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED); return wct; } @@ -183,43 +148,12 @@ public class PipScheduler { /** * Animates resizing of the pinned stack given the duration. */ - public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) { - if (mPipTaskToken == null) { + public void scheduleAnimateResizePip(Rect toBounds) { + if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTaskToken, toBounds); - mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); - } - - void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { - mInSwipePipToHomeTransition = true; - mSwipePipToHomeOverlay = overlay; - mSwipePipToHomeAppBounds.set(appBounds); - if (overlay != null) { - // Shell transitions might use a root animation leash, which will be removed when - // the Recents transition is finished. Launcher attaches the overlay leash to this - // animation target leash; thus, we need to reparent it to the actual Task surface now. - // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP - // transition. - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx); - tx.setLayer(overlay, Integer.MAX_VALUE); - tx.apply(); - } - } - - void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { - mInSwipePipToHomeTransition = inSwipePipToHome; - } - - boolean isInSwipePipToHomeTransition() { - return mInSwipePipToHomeTransition; - } - - void onExitPip() { - mPipTaskToken = null; - mPinnedTaskLeash = null; + wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + mPipTransitionController.startResizeTransition(wct); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index e829d4ef650e..12dce5bf70c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -34,6 +34,7 @@ import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -43,6 +44,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -54,23 +56,33 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; -import java.util.function.Consumer; - /** * Implementation of transitions for PiP on phone. */ -public class PipTransition extends PipTransitionController { +public class PipTransition extends PipTransitionController implements + PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = PipTransition.class.getSimpleName(); + private static final String PIP_TASK_TOKEN = "pip_task_token"; + private static final String PIP_TASK_LEASH = "pip_task_leash"; + /** * The fixed start delay in ms when fading out the content overlay from bounds animation. * The fadeout animation is guaranteed to start after the client has drawn under the new config. */ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400; + // + // Dependencies + // + private final Context mContext; private final PipScheduler mPipScheduler; - @Nullable - private WindowContainerToken mPipTaskToken; + private final PipTransitionState mPipTransitionState; + + // + // Transition tokens + // + @Nullable private IBinder mEnterTransition; @Nullable @@ -78,7 +90,14 @@ public class PipTransition extends PipTransitionController { @Nullable private IBinder mResizeTransition; - private Consumer<Rect> mFinishResizeCallback; + // + // Internal state and relevant cached info + // + + @Nullable + private WindowContainerToken mPipTaskToken; + @Nullable + private SurfaceControl mPipLeash; public PipTransition( Context context, @@ -88,13 +107,16 @@ public class PipTransition extends PipTransitionController { PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipScheduler pipScheduler) { + PipScheduler pipScheduler, + PipTransitionState pipTransitionState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); mContext = context; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); } @Override @@ -104,6 +126,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition collection stage lifecycle hooks + // + @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { @@ -117,13 +143,11 @@ public class PipTransition extends PipTransitionController { } @Override - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { if (wct == null) { return; } mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); - mFinishResizeCallback = onFinishResizeCallback; } @Nullable @@ -146,6 +170,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition playing stage lifecycle hooks + // + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -163,7 +191,19 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition == mEnterTransition || info.getType() == TRANSIT_PIP) { mEnterTransition = null; - if (mPipScheduler.isInSwipePipToHomeTransition()) { + // If we are in swipe PiP to Home transition we are ENTERING_PIP as a jumpcut transition + // is being carried out. + TransitionInfo.Change pipChange = getPipChange(info); + + // If there is no PiP change, exit this transition handler and potentially try others. + if (pipChange == null) return false; + + Bundle extra = new Bundle(); + extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer()); + extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash()); + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP, extra); + + if (mPipTransitionState.isInSwipePipToHomeTransition()) { // If this is the second transition as a part of swipe PiP to home cuj, // handle this transition as a special case with no-op animation. return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction, @@ -179,9 +219,11 @@ public class PipTransition extends PipTransitionController { finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; + mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS); return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } @@ -191,6 +233,10 @@ public class PipTransition extends PipTransitionController { return false; } + // + // Animation schedulers and entry points + // + private boolean startResizeAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -236,11 +282,7 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipScheduler.setInSwipePipToHomeTransition(false); - mPipTaskToken = pipChange.getContainer(); - - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); SurfaceControl pipLeash = pipChange.getLeash(); PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; @@ -264,9 +306,9 @@ public class PipTransition extends PipTransitionController { } else { final float scaleX = (float) destinationBounds.width() / startBounds.width(); final float scaleY = (float) destinationBounds.height() / startBounds.height(); - final int overlaySize = PipContentOverlay.PipAppIconOverlay - .getOverlaySize(mPipScheduler.mSwipePipToHomeAppBounds, destinationBounds); - SurfaceControl overlayLeash = mPipScheduler.mSwipePipToHomeOverlay; + final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); + SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) .setScale(pipLeash, scaleX, scaleY) @@ -274,7 +316,7 @@ public class PipTransition extends PipTransitionController { .reparent(overlayLeash, pipLeash) .setLayer(overlayLeash, Integer.MAX_VALUE); - if (mPipTaskToken != null) { + if (pipTaskToken != null) { SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), this::onClientDrawAtTransitionEnd) @@ -282,7 +324,7 @@ public class PipTransition extends PipTransitionController { .setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f, (destinationBounds.height() - overlaySize) / 2f); - finishWct.setBoundsChangeTransaction(mPipTaskToken, tx); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); } } startTransaction.apply(); @@ -293,14 +335,6 @@ public class PipTransition extends PipTransitionController { return true; } - private void onClientDrawAtTransitionEnd() { - startOverlayFadeoutAnimation(); - } - - // - // Subroutines setting up and starting transitions' animations. - // - private void startOverlayFadeoutAnimation() { ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f); animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS); @@ -309,15 +343,17 @@ public class PipTransition extends PipTransitionController { public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.remove(mPipScheduler.mSwipePipToHomeOverlay); + tx.remove(mPipTransitionState.getSwipePipToHomeOverlay()); tx.apply(); - mPipScheduler.mSwipePipToHomeOverlay = null; + + // We have fully completed enter-PiP animation after the overlay is gone. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); } }); animator.addUpdateListener(animation -> { float alpha = (float) animation.getAnimatedValue(); SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setAlpha(mPipScheduler.mSwipePipToHomeOverlay, alpha).apply(); + tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply(); }); animator.start(); } @@ -330,10 +366,8 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); // TODO: b/275910498 Use a new implementation of the PiP animator here. @@ -349,10 +383,8 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); finishCallback.onTransitionFinished(null); @@ -366,7 +398,7 @@ public class PipTransition extends PipTransitionController { startTransaction.apply(); // TODO: b/275910498 Use a new implementation of the PiP animator here. finishCallback.onTransitionFinished(null); - onExitPip(); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); return true; } @@ -376,12 +408,20 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback) { startTransaction.apply(); finishCallback.onTransitionFinished(null); - onExitPip(); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); return true; } + /** + * TODO: b/275910498 Use a new implementation of the PiP animator here. + */ + private void startResizeAnimation(SurfaceControl leash, Rect startBounds, + Rect endBounds, int duration) { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + } + // - // Utility methods for checking PiP-related transition info and requests. + // Various helpers to resolve transition requests and infos // @Nullable @@ -442,11 +482,11 @@ public class PipTransition extends PipTransitionController { } private boolean isRemovePipTransition(@NonNull TransitionInfo info) { - if (mPipTaskToken == null) { + if (mPipTransitionState.mPipTaskToken == null) { // PiP removal makes sense if enter-PiP has cached a valid pinned task token. return false; } - TransitionInfo.Change pipChange = info.getChange(mPipTaskToken); + TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.mPipTaskToken); if (pipChange == null) { // Search for the PiP change by token since the windowing mode might be FULLSCREEN now. return false; @@ -460,14 +500,43 @@ public class PipTransition extends PipTransitionController { return isPipMovedToBack || isPipClosed; } - /** - * TODO: b/275910498 Use a new implementation of the PiP animator here. - */ - private void startResizeAnimation(SurfaceControl leash, Rect startBounds, - Rect endBounds, int duration) {} + // + // Miscellaneous callbacks and listeners + // - private void onExitPip() { - mPipTaskToken = null; - mPipScheduler.onExitPip(); + private void onClientDrawAtTransitionEnd() { + if (mPipTransitionState.getSwipePipToHomeOverlay() != null) { + startOverlayFadeoutAnimation(); + } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { + // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint, + // and then we get a signal on client finishing its draw after the transition + // has ended, then we have fully entered PiP. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + } + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.ENTERING_PIP: + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + mPipTransitionState.mPipTaskToken = extra.getParcelable( + PIP_TASK_TOKEN, WindowContainerToken.class); + mPipTransitionState.mPinnedTaskLeash = extra.getParcelable( + PIP_TASK_LEASH, SurfaceControl.class); + boolean hasValidTokenAndLeash = mPipTransitionState.mPipTaskToken != null + && mPipTransitionState.mPinnedTaskLeash != null; + + Preconditions.checkState(hasValidTokenAndLeash, + "Unexpected bundle for " + mPipTransitionState); + break; + case PipTransitionState.EXITED_PIP: + mPipTransitionState.mPipTaskToken = null; + mPipTransitionState.mPinnedTaskLeash = null; + break; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java new file mode 100644 index 000000000000..f7bc622b6195 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone; + +import android.annotation.IntDef; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Contains the state relevant to carry out or probe the status of PiP transitions. + * + * <p>Existing and new PiP components can subscribe to PiP transition related state changes + * via <code>PipTransitionStateChangedListener</code>.</p> + * + * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering. + * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs + * to some other class <code>Bar</code>, a special care must be given when manipulating state A in + * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of + * the class <code>Bar</code>.</p> + * + * <p>Hence, the recommended usage for classes who want to subscribe to + * <code>PipTransitionState</code> changes is to manipulate only their own internal state or + * <code>PipTransitionState</code> state.</p> + * + * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should + * just be moved to <code>PipTransitionState</code> and become a shared state + * between Foo and Bar.</p> + * + * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code> + * receives a <code>Bundle</code> extra object that can be optionally set via + * <code>setState(state, extra)</code>. This can be used to resolve extra information to update + * relevant internal or <code>PipTransitionState</code> state. However, each listener + * needs to check for whether the extra passed is correct for a particular state, + * and throw an <code>IllegalStateException</code> otherwise.</p> + */ +public class PipTransitionState { + public static final int UNDEFINED = 0; + + // State for Launcher animating the swipe PiP to home animation. + public static final int SWIPING_TO_PIP = 1; + + // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation. + public static final int ENTERING_PIP = 2; + + // State for app finishing drawing in PiP mode as a final step in enter PiP flow. + public static final int ENTERED_PIP = 3; + + // State for scheduling a transition to change PiP bounds. + public static final int CHANGING_PIP_BOUNDS = 4; + + // State for app potentially finishing drawing in new PiP bounds after resize is complete. + public static final int CHANGED_PIP_BOUNDS = 5; + + // State for starting exiting PiP. + public static final int EXITING_PIP = 6; + + // State for finishing exit PiP flow. + public static final int EXITED_PIP = 7; + + private static final int FIRST_CUSTOM_STATE = 1000; + + private int mPrevCustomState = FIRST_CUSTOM_STATE; + + @IntDef(prefix = { "TRANSITION_STATE_" }, value = { + UNDEFINED, + SWIPING_TO_PIP, + ENTERING_PIP, + ENTERED_PIP, + CHANGING_PIP_BOUNDS, + CHANGED_PIP_BOUNDS, + EXITING_PIP, + EXITED_PIP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionState {} + + @TransitionState + private int mState; + + // + // Swipe up to enter PiP related state + // + + // true if Launcher has started swipe PiP to home animation + private boolean mInSwipePipToHomeTransition; + + // App bounds used when as a starting point to swipe PiP to home animation in Launcher; + // these are also used to calculate the app icon overlay buffer size. + @NonNull + private final Rect mSwipePipToHomeAppBounds = new Rect(); + + // + // Tokens and leashes + // + + // pinned PiP task's WC token + @Nullable + WindowContainerToken mPipTaskToken; + + // pinned PiP task's leash + @Nullable + SurfaceControl mPinnedTaskLeash; + + // Overlay leash potentially used during swipe PiP to home transition; + // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. + @Nullable + private SurfaceControl mSwipePipToHomeOverlay; + + /** + * An interface to track state updates as we progress through PiP transitions. + */ + public interface PipTransitionStateChangedListener { + + /** Reports changes in PiP transition state. */ + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra); + } + + private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>(); + + /** + * @return the state of PiP in the context of transitions. + */ + @TransitionState + public int getState() { + return mState; + } + + /** + * Sets the state of PiP in the context of transitions. + */ + public void setState(@TransitionState int state) { + setState(state, null /* extra */); + } + + /** + * Sets the state of PiP in the context of transitions + * + * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. + */ + public void setState(@TransitionState int state, @Nullable Bundle extra) { + if (state == ENTERING_PIP || state == SWIPING_TO_PIP) { + // Whenever we are entering PiP caller must provide extra state to set as well. + Preconditions.checkArgument(extra != null && !extra.isEmpty(), + "No extra bundle for either ENTERING_PIP or SWIPING_TO_PIP state."); + } + if (mState != state) { + dispatchPipTransitionStateChanged(mState, state, extra); + mState = state; + } + } + + private void dispatchPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra) { + mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra)); + } + + /** + * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. + */ + public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { + if (mCallbacks.contains(listener)) { + return; + } + mCallbacks.add(listener); + } + + /** + * @return true if provided {@link PipTransitionStateChangedListener} + * is registered before removing it. + */ + public boolean removePipTransitionStateChangedListener( + PipTransitionStateChangedListener listener) { + return mCallbacks.remove(listener); + } + + /** + * @return true if we have fully entered PiP. + */ + public boolean isInPip() { + return mState > ENTERING_PIP && mState < EXITING_PIP; + } + + void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash, + @NonNull Rect appBounds) { + mInSwipePipToHomeTransition = true; + if (overlayLeash != null && !appBounds.isEmpty()) { + mSwipePipToHomeOverlay = overlayLeash; + mSwipePipToHomeAppBounds.set(appBounds); + } + } + + void resetSwipePipToHomeState() { + mInSwipePipToHomeTransition = false; + mSwipePipToHomeOverlay = null; + mSwipePipToHomeAppBounds.setEmpty(); + } + + /** + * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. + */ + public boolean isInSwipePipToHomeTransition() { + return mInSwipePipToHomeTransition; + } + + /** + * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP; + * null if srcRectHint provided is valid. + */ + @Nullable + public SurfaceControl getSwipePipToHomeOverlay() { + return mSwipePipToHomeOverlay; + } + + /** + * @return app bounds used to calculate + */ + @NonNull + public Rect getSwipePipToHomeAppBounds() { + return mSwipePipToHomeAppBounds; + } + + /** + * @return a custom state solely for internal use by the caller. + */ + @TransitionState + public int getCustomState() { + return ++mPrevCustomState; + } + + private String stateToString() { + switch (mState) { + case UNDEFINED: return "undefined"; + case ENTERING_PIP: return "entering-pip"; + case ENTERED_PIP: return "entered-pip"; + case CHANGING_PIP_BOUNDS: return "changing-bounds"; + case CHANGED_PIP_BOUNDS: return "changed-bounds"; + case EXITING_PIP: return "exiting-pip"; + case EXITED_PIP: return "exited-pip"; + } + throw new IllegalStateException("Unknown state: " + mState); + } + + @Override + public String toString() { + return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", + stateToString(), mInSwipePipToHomeTransition); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 2a50b191b1e9..5e9451a09d41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -201,7 +201,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; private final TransactionPool mTransactionPool; - private final SplitScreenTransitions mSplitTransitions; + private SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; // Cache live tile tasks while entering recents, evict them from stages in finish transaction @@ -399,6 +399,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSplitTransitions; } + @VisibleForTesting + void setSplitTransitions(SplitScreenTransitions splitScreenTransitions) { + mSplitTransitions = splitScreenTransitions; + } + public boolean isSplitScreenVisible() { return mSideStageListener.mVisible && mMainStageListener.mVisible; } @@ -583,7 +588,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); wct.startTask(taskId, options); // If this should be mixed, send the task to avoid split handle transition directly. - if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(taskId, mTaskOrganizer)) { + if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) { mTaskOrganizer.applyTransaction(wct); return; } @@ -622,7 +627,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.sendPendingIntent(intent, fillInIntent, options); // If this should be mixed, just send the intent to avoid split handle transition directly. - if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(intent)) { + if (mMixedHandler != null && mMixedHandler.isIntentInPip(intent)) { mTaskOrganizer.applyTransaction(wct); return; } @@ -711,16 +716,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, taskId1, taskId2, splitPosition, snapPosition); final WindowContainerTransaction wct = new WindowContainerTransaction(); if (taskId2 == INVALID_TASK_ID) { - if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) { - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); - } - if (mRecentTasks.isPresent()) { - mRecentTasks.get().removeSplitPair(taskId1); - } - options1 = options1 != null ? options1 : new Bundle(); - addActivityOptions(options1, null); - wct.startTask(taskId1, options1); - mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + startSingleTask(taskId1, options1, wct, remoteTransition); return; } @@ -741,11 +737,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, "startIntentAndTask: intent=%s task1=%d position=%d snapPosition=%d", pendingIntent.getIntent(), taskId, splitPosition, snapPosition); final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (taskId == INVALID_TASK_ID) { - options1 = options1 != null ? options1 : new Bundle(); - addActivityOptions(options1, null); - wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent); + boolean secondTaskPipped = mMixedHandler.isTaskInPip(taskId, mTaskOrganizer); + if (taskId == INVALID_TASK_ID || secondTaskPipped) { + startSingleIntent(pendingIntent, fillInIntent, options1, wct, remoteTransition); + return; + } + + if (firstIntentPipped) { + startSingleTask(taskId, options2, wct, remoteTransition); return; } @@ -757,6 +757,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); } + /** + * @param taskId Starts this task in fullscreen, removing it from existing pairs if it was part + * of one. + */ + private void startSingleTask(int taskId, Bundle options, WindowContainerTransaction wct, + RemoteTransition remoteTransition) { + if (mMainStage.containsTask(taskId) || mSideStage.containsTask(taskId)) { + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + if (mRecentTasks.isPresent()) { + mRecentTasks.get().removeSplitPair(taskId); + } + options = options != null ? options : new Bundle(); + addActivityOptions(options, null); + wct.startTask(taskId, options); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } + /** Starts a shortcut and a task to a split pair in one transition. */ void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @@ -844,6 +862,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } + boolean handledForPipSplitLaunch = handlePippedSplitIntentsLaunch( + pendingIntent1, + pendingIntent2, + options1, + options2, + shortcutInfo1, + shortcutInfo2, + wct, + fillInIntent1, + fillInIntent2, + remoteTransition); + if (handledForPipSplitLaunch) { + return; + } + if (!mMainStage.isActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. @@ -878,6 +911,46 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setEnterInstanceId(instanceId); } + /** + * Checks if either of the apps in the desired split launch is currently in Pip. If so, it will + * launch the non-pipped app as a fullscreen app, otherwise no-op. + */ + private boolean handlePippedSplitIntentsLaunch(PendingIntent pendingIntent1, + PendingIntent pendingIntent2, Bundle options1, Bundle options2, + ShortcutInfo shortcutInfo1, ShortcutInfo shortcutInfo2, WindowContainerTransaction wct, + Intent fillInIntent1, Intent fillInIntent2, RemoteTransition remoteTransition) { + // If one of the split apps to start is in Pip, only launch the non-pip app in fullscreen + boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent1); + boolean secondIntentPipped = mMixedHandler.isIntentInPip(pendingIntent2); + if (firstIntentPipped || secondIntentPipped) { + Bundle options = secondIntentPipped ? options1 : options2; + options = options == null ? new Bundle() : options; + addActivityOptions(options, null); + if (shortcutInfo1 != null || shortcutInfo2 != null) { + ShortcutInfo infoToLaunch = secondIntentPipped ? shortcutInfo1 : shortcutInfo2; + wct.startShortcut(mContext.getPackageName(), infoToLaunch, options); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } else { + PendingIntent intentToLaunch = secondIntentPipped ? pendingIntent1 : pendingIntent2; + Intent fillInIntentToLaunch = secondIntentPipped ? fillInIntent1 : fillInIntent2; + startSingleIntent(intentToLaunch, fillInIntentToLaunch, options, wct, + remoteTransition); + } + return true; + } + return false; + } + + /** @param pendingIntent Starts this intent in fullscreen */ + private void startSingleIntent(PendingIntent pendingIntent, Intent fillInIntent, Bundle options, + WindowContainerTransaction wct, + RemoteTransition remoteTransition) { + Bundle optionsToLaunch = options != null ? options : new Bundle(); + addActivityOptions(optionsToLaunch, null); + wct.sendPendingIntent(pendingIntent, fillInIntent, optionsToLaunch); + mSplitTransitions.startFullscreenTransition(wct, remoteTransition); + } + /** Starts a pair of tasks using legacy transition. */ void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 4bc0dc00175b..4d02ec26e18e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -563,22 +563,23 @@ public class DefaultMixedHandler implements MixedTransitionHandler, /** Use to when split use intent to enter, check if this enter transition should be mixed or * not.*/ - public boolean shouldSplitEnterMixed(PendingIntent intent) { + public boolean isIntentInPip(PendingIntent intent) { // Check if this intent package is same as pip one or not, if true we want let the pip // task enter split. if (mPipHandler != null) { - return mPipHandler.isInPipPackage(SplitScreenUtils.getPackageName(intent.getIntent())); + return mPipHandler + .isPackageActiveInPip(SplitScreenUtils.getPackageName(intent.getIntent())); } return false; } /** Use to when split use taskId to enter, check if this enter transition should be mixed or * not.*/ - public boolean shouldSplitEnterMixed(int taskId, ShellTaskOrganizer shellTaskOrganizer) { + public boolean isTaskInPip(int taskId, ShellTaskOrganizer shellTaskOrganizer) { // Check if this intent package is same as pip one or not, if true we want let the pip // task enter split. if (mPipHandler != null) { - return mPipHandler.isInPipPackage( + return mPipHandler.isPackageActiveInPip( SplitScreenUtils.getPackageName(taskId, shellTaskOrganizer)); } return false; diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt index 75dfeba3e662..17cace0da739 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt @@ -23,6 +23,7 @@ import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry @@ -114,5 +115,30 @@ class DesktopModeFlickerScenarios { ) .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + + val CORNER_RESIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.CHANGE + } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowRemainInsideDisplayBounds(Components.DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt new file mode 100644 index 000000000000..8d1a53021683 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithCornerResizeLandscape : ResizeAppWithCornerResize(Rotation.ROTATION_90) { + @ExpectedScenarios(["CORNER_RESIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt new file mode 100644 index 000000000000..2d81c8c44799 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithCornerResizePortrait : ResizeAppWithCornerResize(Rotation.ROTATION_0) { + @ExpectedScenarios(["CORNER_RESIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt new file mode 100644 index 000000000000..289ca9f5d92e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + + +@Ignore("Base Test Class") +abstract class ResizeAppWithCornerResize +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode()) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun resizeAppWithCornerResize() { + testApp.cornerResize(wmHelper, device, DesktopModeAppHelper.Corners.RIGHT_TOP, 50, -50) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java new file mode 100644 index 000000000000..bd8ac379b86f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2; + +import android.os.Bundle; +import android.os.Parcelable; +import android.testing.AndroidTestingRunner; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.pip2.phone.PipTransitionState; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit test against {@link PhoneSizeSpecSource}. + * + * This test mocks the PiP2 flag to be true. + */ +@RunWith(AndroidTestingRunner.class) +public class PipTransitionStateTest extends ShellTestCase { + private static final String EXTRA_ENTRY_KEY = "extra_entry_key"; + private PipTransitionState mPipTransitionState; + private PipTransitionState.PipTransitionStateChangedListener mStateChangedListener; + private Parcelable mEmptyParcelable; + + @Before + public void setUp() { + mPipTransitionState = new PipTransitionState(); + mPipTransitionState.setState(PipTransitionState.UNDEFINED); + mEmptyParcelable = new Bundle(); + } + + @Test + public void testEnteredState_withoutExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNull(extra); + }; + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test + public void testEnteredState_withExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP, extra); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test(expected = IllegalArgumentException.class) + public void testEnteringState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP); + } + + @Test(expected = IllegalArgumentException.class) + public void testSwipingToPipState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP); + } + + @Test + public void testCustomState_withExtra_thenEntered_withoutExtra() { + final int customState = mPipTransitionState.getCustomState(); + mStateChangedListener = (oldState, newState, extra) -> { + if (newState == customState) { + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + return; + } else if (newState == PipTransitionState.ENTERED_PIP) { + Assert.assertNull(extra); + return; + } + Assert.fail("Neither custom not ENTERED_PIP state is received."); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(customState, extra); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d819261ecba2..d7c383523a6f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -40,10 +40,12 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.PendingIntent; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; @@ -51,6 +53,7 @@ import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.RemoteTransition; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -74,6 +77,7 @@ import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -111,6 +115,8 @@ public class StageCoordinatorTests extends ShellTestCase { private TransactionPool mTransactionPool; @Mock private LaunchAdjacentController mLaunchAdjacentController; + @Mock + private DefaultMixedHandler mDefaultMixedHandler; private final Rect mBounds1 = new Rect(10, 20, 30, 40); private final Rect mBounds2 = new Rect(5, 10, 15, 20); @@ -370,6 +376,96 @@ public class StageCoordinatorTests extends ShellTestCase { } } + @Test + public void testSplitIntentAndTaskWithPippedApp_launchFullscreen() { + int taskId = 9; + SplitScreenTransitions splitScreenTransitions = + spy(mStageCoordinator.getSplitTransitions()); + mStageCoordinator.setSplitTransitions(splitScreenTransitions); + mStageCoordinator.setMixedHandler(mDefaultMixedHandler); + PendingIntent pendingIntent = mock(PendingIntent.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + when(remoteTransition.getDebugName()).thenReturn(""); + // Test launching second task full screen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); + mStageCoordinator.startIntentAndTask( + pendingIntent, + null /*fillInIntent*/, + null /*option1*/, + taskId, + null /*option2*/, + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(1)) + .startFullscreenTransition(any(), any()); + + // Test launching first intent fullscreen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false); + when(mDefaultMixedHandler.isTaskInPip(taskId, mTaskOrganizer)).thenReturn(true); + mStageCoordinator.startIntentAndTask( + pendingIntent, + null /*fillInIntent*/, + null /*option1*/, + taskId, + null /*option2*/, + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(2)) + .startFullscreenTransition(any(), any()); + } + + @Test + public void testSplitIntentsWithPippedApp_launchFullscreen() { + SplitScreenTransitions splitScreenTransitions = + spy(mStageCoordinator.getSplitTransitions()); + mStageCoordinator.setSplitTransitions(splitScreenTransitions); + mStageCoordinator.setMixedHandler(mDefaultMixedHandler); + PendingIntent pendingIntent = mock(PendingIntent.class); + PendingIntent pendingIntent2 = mock(PendingIntent.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + when(remoteTransition.getDebugName()).thenReturn(""); + // Test launching second task full screen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); + mStageCoordinator.startIntents( + pendingIntent, + null /*fillInIntent*/, + null /*shortcutInfo1*/, + new Bundle(), + pendingIntent2, + null /*fillInIntent2*/, + null /*shortcutInfo1*/, + new Bundle(), + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(1)) + .startFullscreenTransition(any(), any()); + + // Test launching first intent fullscreen + when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false); + when(mDefaultMixedHandler.isIntentInPip(pendingIntent2)).thenReturn(true); + mStageCoordinator.startIntents( + pendingIntent, + null /*fillInIntent*/, + null /*shortcutInfo1*/, + new Bundle(), + pendingIntent2, + null /*fillInIntent2*/, + null /*shortcutInfo1*/, + new Bundle(), + 0 /*splitPosition*/, + 1 /*snapPosition*/, + remoteTransition /*remoteTransition*/, + null /*instanceId*/); + verify(splitScreenTransitions, times(2)) + .startFullscreenTransition(any(), any()); + } + private Transitions createTestTransitions() { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 1fcb6920db14..cfca48084d97 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,7 +34,9 @@ namespace flags = com::android::graphics::hwui::flags; namespace android { -inline constexpr int kHighContrastTextBorderWidth = 4; +// These should match the constants in framework/base/core/java/android/text/Layout.java +inline constexpr float kHighContrastTextBorderWidth = 4.0f; +inline constexpr float kHighContrastTextBorderWidthFactor = 0.2f; static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness, const Paint& paint, Canvas* canvas) { @@ -48,7 +50,16 @@ static void simplifyPaint(int color, Paint* paint) { paint->setShader(nullptr); paint->setColorFilter(nullptr); paint->setLooper(nullptr); - paint->setStrokeWidth(kHighContrastTextBorderWidth + 0.04 * paint->getSkFont().getSize()); + + if (flags::high_contrast_text_small_text_rect()) { + paint->setStrokeWidth( + std::max(kHighContrastTextBorderWidth, + kHighContrastTextBorderWidthFactor * paint->getSkFont().getSize())); + } else { + auto borderWidthFactor = 0.04f; + paint->setStrokeWidth(kHighContrastTextBorderWidth + + borderWidthFactor * paint->getSkFont().getSize()); + } paint->setStrokeJoin(SkPaint::kRound_Join); paint->setLooper(nullptr); } @@ -106,36 +117,7 @@ public: Paint outlinePaint(paint); simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint); outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style); - if (flags::high_contrast_text_small_text_rect()) { - const SkFont& font = paint.getSkFont(); - auto padding = kHighContrastTextBorderWidth + 0.1f * font.getSize(); - - // Draw the background only behind each glyph's bounds. We do this instead of using - // the bounds of the entire layout, because the layout includes alignment whitespace - // etc which can obscure other text from separate passes (e.g. emojis). - // Merge all the glyph bounds into one rect for this line, since drawing a rect for - // each glyph is expensive. - SkRect glyphBounds; - SkRect bgBounds; - for (size_t i = start; i < end; i++) { - auto glyph = layout.getGlyphId(i); - - font.getBounds(reinterpret_cast<const SkGlyphID*>(&glyph), 1, &glyphBounds, - &paint); - glyphBounds.offset(layout.getX(i), layout.getY(i)); - - bgBounds.join(glyphBounds); - } - - if (!bgBounds.isEmpty()) { - bgBounds.offset(x, y); - bgBounds.outset(padding, padding); - canvas->drawRect(bgBounds.fLeft, bgBounds.fTop, bgBounds.fRight, - bgBounds.fBottom, outlinePaint); - } - } else { - canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance); - } + canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance); // inner gDrawTextBlobMode = DrawTextBlobMode::HctInner; diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp index 64dcf6e8f573..395354ef8f20 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp @@ -34,6 +34,7 @@ android_test { "compatibility-device-util-axt", "platform-test-annotations", "truth", + "uiautomator-helpers", ], srcs: [ "src/**/*.java", diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 0ab99fac9ba3..66943d453873 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -30,6 +30,8 @@ import static com.android.systemui.accessibility.accessibilitymenu.Accessibility import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; +import static com.google.common.truth.Truth.assertWithMessage; + import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Instrumentation; import android.app.KeyguardManager; @@ -43,6 +45,8 @@ import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.os.PowerManager; +import android.os.RemoteException; +import android.platform.uiautomator_helpers.WaitUtils; import android.provider.Settings; import android.util.Log; import android.view.Display; @@ -51,6 +55,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.Configurator; +import androidx.test.uiautomator.UiDevice; import com.android.compatibility.common.util.TestUtils; import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId; @@ -60,7 +66,6 @@ import org.junit.AfterClass; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,11 +81,13 @@ public class AccessibilityMenuServiceTest { private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; private static final int TIMEOUT_UI_CHANGE_S = 5; private static final int NO_GLOBAL_ACTION = -1; - private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU) - .setPackage(PACKAGE_NAME); + private static final Intent INTENT_OPEN_MENU = + new Intent(INTENT_TOGGLE_MENU).setPackage(PACKAGE_NAME); + private static final String SERVICE_NAME = PACKAGE_NAME + "/.AccessibilityMenuService"; private static Instrumentation sInstrumentation; private static UiAutomation sUiAutomation; + private static UiDevice sUiDevice; private static final AtomicInteger sLastGlobalAction = new AtomicInteger(NO_GLOBAL_ACTION); private static final AtomicBoolean sOpenBlocked = new AtomicBoolean(false); @@ -91,12 +98,14 @@ public class AccessibilityMenuServiceTest { @BeforeClass public static void classSetup() throws Throwable { - final String serviceName = PACKAGE_NAME + "/.AccessibilityMenuService"; + Configurator.getInstance() + .setUiAutomationFlags(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); sInstrumentation = InstrumentationRegistry.getInstrumentation(); sUiAutomation = sInstrumentation.getUiAutomation( UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); sUiAutomation.adoptShellPermissionIdentity( UiAutomation.ALL_PERMISSIONS.toArray(new String[0])); + sUiDevice = UiDevice.getInstance(sInstrumentation); final Context context = sInstrumentation.getTargetContext(); sAccessibilityManager = context.getSystemService(AccessibilityManager.class); @@ -117,13 +126,13 @@ public class AccessibilityMenuServiceTest { // Enable a11yMenu service. Settings.Secure.putString(context.getContentResolver(), - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, serviceName); + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, SERVICE_NAME); TestUtils.waitUntil("Failed to enable service", TIMEOUT_SERVICE_STATUS_CHANGE_S, () -> sAccessibilityManager.getEnabledAccessibilityServiceList( AccessibilityServiceInfo.FEEDBACK_ALL_MASK).stream().filter( - info -> info.getId().contains(serviceName)).count() == 1); + info -> info.getId().contains(SERVICE_NAME)).count() == 1); context.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -159,8 +168,11 @@ public class AccessibilityMenuServiceTest { public void tearDown() throws Throwable { closeMenu(); sLastGlobalAction.set(NO_GLOBAL_ACTION); + // Leave the device in clean state when the test finished + unlockSignal(); // dismisses screenshot popup if present. - sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); + sUiDevice.pressBack(); + sUiDevice.pressHome(); } private static boolean isMenuVisible() { @@ -168,38 +180,25 @@ public class AccessibilityMenuServiceTest { return root != null && root.getPackageName().toString().equals(PACKAGE_NAME); } - private static void wakeUpScreen() throws Throwable { - sUiAutomation.executeShellCommand("input keyevent KEYCODE_WAKEUP"); - TestUtils.waitUntil("Screen did not wake up.", - TIMEOUT_UI_CHANGE_S, - () -> sPowerManager.isInteractive()); + private static void wakeUpScreen() throws RemoteException { + sUiDevice.wakeUp(); + WaitUtils.waitForValueToSettle("Screen On", AccessibilityMenuServiceTest::isScreenOn); + assertWithMessage("Screen is on").that(isScreenOn()).isTrue(); } private static void closeScreen() throws Throwable { - Display display = sDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); sUiAutomation.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN); - TestUtils.waitUntil("Screen did not close.", - TIMEOUT_UI_CHANGE_S, - () -> !sPowerManager.isInteractive() - && display.getState() == Display.STATE_OFF - ); + WaitUtils.waitForValueToSettle("Screen Off", AccessibilityMenuServiceTest::isScreenOff); + assertWithMessage("Screen is off").that(isScreenOff()).isTrue(); } private static void openMenu() throws Throwable { unlockSignal(); - sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); - - TestUtils.waitUntil("Timed out before menu could appear.", - TIMEOUT_UI_CHANGE_S, - () -> { - if (isMenuVisible()) { - return true; - } else { - unlockSignal(); - sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); - return false; - } - }); + if (!isMenuVisible()) { + sInstrumentation.getTargetContext().sendBroadcast(INTENT_OPEN_MENU); + sUiDevice.waitForIdle(); + WaitUtils.ensureThat("Accessibility Menu is visible", () -> isMenuVisible()); + } } private static void closeMenu() throws Throwable { @@ -342,7 +341,9 @@ public class AccessibilityMenuServiceTest { sUiAutomation.executeAndWaitForEvent( () -> assistantButton.performAction(CLICK_ID), - (event) -> expectedPackage.contains(event.getPackageName()), + (event) -> + event.getPackageName() != null + && expectedPackage.contains(event.getPackageName()), TIMEOUT_UI_CHANGE_S * 1000 ); } @@ -358,7 +359,9 @@ public class AccessibilityMenuServiceTest { sUiAutomation.executeAndWaitForEvent( () -> settingsButton.performAction(CLICK_ID), - (event) -> expectedPackage.contains(event.getPackageName()), + (event) -> + event.getPackageName() != null + && expectedPackage.contains(event.getPackageName()), TIMEOUT_UI_CHANGE_S * 1000 ); } @@ -454,24 +457,40 @@ public class AccessibilityMenuServiceTest { } @Test - @Ignore("Test failure in pre/postsubmit cannot be replicated on local devices. " - + "Coverage is low-impact.") public void testOnScreenLock_cannotOpenMenu() throws Throwable { closeScreen(); wakeUpScreen(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); + sUiDevice.waitForIdle(); TestUtils.waitUntil("Did not receive signal that menu cannot open", TIMEOUT_UI_CHANGE_S, - () -> { - sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); - return sOpenBlocked.get(); - }); + sOpenBlocked::get); + } + + private static void unlockSignal() throws RemoteException { + if (!sKeyguardManager.isKeyguardLocked()) { + return; + } + // go/adb-cheats#unlock-screen + wakeUpScreen(); + if (sKeyguardManager.isKeyguardLocked()) { + sUiDevice.pressMenu(); + } + WaitUtils.ensureThat( + "Device unlocked & isInteractive", + () -> isScreenOn() && !sKeyguardManager.isKeyguardLocked()); + } + + private static boolean isScreenOn() { + int display = Display.DEFAULT_DISPLAY; + return sPowerManager.isInteractive(display) + && sDisplayManager.getDisplay(display).getState() == Display.STATE_ON; } - private static void unlockSignal() { - // MENU unlocks screen, - // BACK closes any menu that may appear if the screen wasn't locked. - sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); - sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); + private static boolean isScreenOff() { + int display = Display.DEFAULT_DISPLAY; + return !sPowerManager.isInteractive(display) + && sDisplayManager.getDisplay(display).getState() == Display.STATE_OFF; } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index e07cd0539a46..ec3c0030c44f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -27,6 +27,7 @@ import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys +import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.res.R @@ -79,6 +80,7 @@ fun CommunalContainer( ) { val coroutineScope = rememberCoroutineScope() val currentSceneKey: SceneKey by viewModel.currentScene.collectAsState(CommunalScenes.Blank) + val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false) val state: MutableSceneTransitionLayoutState = remember { MutableSceneTransitionLayoutState( initialScene = currentSceneKey, @@ -128,6 +130,10 @@ fun CommunalContainer( CommunalScene(viewModel, colors, dialogFactory, modifier = modifier) } } + + // Touches on the notification shade in blank areas fall through to the glanceable hub. When the + // shade is showing, we block all touches in order to prevent this unwanted behavior. + Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed)) } /** Scene containing the glanceable hub UI. */ diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 338987a60227..dff9b3bae481 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -20,6 +20,8 @@ import android.appwidget.AppWidgetHostView import android.graphics.drawable.Icon import android.os.Bundle import android.util.SizeF +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS import android.widget.FrameLayout import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -103,6 +105,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign @@ -115,8 +118,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.core.view.setPadding import androidx.window.layout.WindowMetricsCalculator -import com.android.compose.modifiers.height -import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter @@ -300,7 +301,7 @@ fun CommunalHub( viewModel.onHidePopup() viewModel.onOpenWidgetEditor(selectedKey.value) }, - onHide = { viewModel.onHidePopup()} + onHide = { viewModel.onHidePopup() } ) } null -> {} @@ -374,7 +375,7 @@ private fun ScrollOnUpdatedLiveContentEffect( liveContentKeys.indexOfFirst { !prevLiveContentKeys.contains(it) } // Scroll if current position is behind the first updated content - if (indexOfFirstUpdatedContent in 0..<gridState.firstVisibleItemIndex) { + if (indexOfFirstUpdatedContent in 0 until gridState.firstVisibleItemIndex) { // Launching with a scope to prevent the job from being canceled in the case of a // recomposition during scrolling coroutineScope.launch { gridState.animateScrollToItem(indexOfFirstUpdatedContent) } @@ -841,17 +842,31 @@ private fun WidgetContent( widgetConfigurator: WidgetConfigurator?, modifier: Modifier = Modifier, ) { + val context = LocalContext.current + val isFocusable by viewModel.isFocusable.collectAsState(initial = false) + val accessibilityLabel = + remember(model, context) { + model.providerInfo.loadLabel(context.packageManager).toString().trim() + } + val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget) Box( modifier = - modifier.thenIf(!viewModel.isEditMode && model.inQuietMode) { - Modifier.pointerInput(Unit) { - // consume tap to prevent the child view from triggering interactions with the - // app widget - observeTaps(shouldConsume = true) { _ -> - viewModel.onOpenEnableWorkProfileDialog() + modifier + .thenIf(!viewModel.isEditMode && model.inQuietMode) { + Modifier.pointerInput(Unit) { + // consume tap to prevent the child view from triggering interactions with + // the app widget + observeTaps(shouldConsume = true) { _ -> + viewModel.onOpenEnableWorkProfileDialog() + } + } + } + .thenIf(viewModel.isEditMode) { + Modifier.semantics { + contentDescription = accessibilityLabel + onClick(label = clickActionLabel, action = null) } } - } ) { AndroidView( modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode), @@ -863,8 +878,19 @@ private fun WidgetContent( // Remove the extra padding applied to AppWidgetHostView to allow widgets to // occupy the entire box. setPadding(0) + accessibilityDelegate = viewModel.widgetAccessibilityDelegate } }, + update = { + it.apply { + importantForAccessibility = + if (isFocusable) { + IMPORTANT_FOR_ACCESSIBILITY_AUTO + } else { + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } + } + }, // For reusing composition in lazy lists. onReset = {}, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index e7bfee34e76a..33d2cc40cb06 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize @@ -274,6 +275,9 @@ fun LazyGridItemScope.DraggableItem( AnimatedVisibility( modifier = Modifier.matchParentSize() + // Avoid taking focus away from the content when using explore-by-touch with + // accessibility tools. + .clearAndSetSemantics {} // Do not consume motion events in the highlighted item and pass them down to // the content. .pointerInteropFilter { false }, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt index f354b80692f5..74af3ca19266 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt @@ -98,10 +98,10 @@ private class ClickableSliceView( } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { - return onClick != null || super.onInterceptTouchEvent(ev) + return (isSliceViewClickable && onClick != null) || super.onInterceptTouchEvent(ev) } override fun onClick(v: View?) { - onClick?.let { it() } ?: super.onClick(v) + onClick?.takeIf { isSliceViewClickable }?.let { it() } ?: super.onClick(v) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt index 4f3a6c84b13d..874c0a299d2b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt @@ -38,6 +38,8 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import com.android.systemui.common.ui.compose.Icon import com.android.systemui.volume.panel.component.button.ui.viewmodel.ButtonViewModel @@ -79,6 +81,12 @@ class ToggleButtonComponent( modifier = Modifier.fillMaxSize().padding(8.dp).semantics { role = Role.Switch + toggleableState = + if (viewModel.isActive) { + ToggleableState.On + } else { + ToggleableState.Off + } contentDescription = label }, onClick = { onCheckedChange(!viewModel.isActive) }, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 4273b4fbf7b8..ca643231e874 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -217,18 +217,18 @@ internal class ElementNode( maybePruneMaps(layoutImpl, prevElement, prevSceneState) } - override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean { + override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { // TODO(b/324191441): Investigate whether making this check more complex (checking if this // element is shared or transformed) would lead to better performance. - return layoutImpl.state.currentTransitions.isEmpty() + return layoutImpl.state.isTransitioning() } - override fun Placeable.PlacementScope.isPlacementApproachComplete( + override fun Placeable.PlacementScope.isPlacementApproachInProgress( lookaheadCoordinates: LayoutCoordinates ): Boolean { // TODO(b/324191441): Investigate whether making this check more complex (checking if this // element is shared or transformed) would lead to better performance. - return layoutImpl.state.currentTransitions.isEmpty() + return layoutImpl.state.isTransitioning() } @ExperimentalComposeUiApi diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index 7fb5a4d0cc27..339868c9fbc9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.intermediateLayout +import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex @@ -74,7 +74,9 @@ internal class Scene( Box( modifier .zIndex(zIndex) - .intermediateLayout { measurable, constraints -> + .approachLayout( + isMeasurementApproachInProgress = { scope.layoutState.isTransitioning() } + ) { measurable, constraints -> targetSize = lookaheadSize val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index ad691ba54607..d383cec324d3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -251,8 +251,8 @@ private data class LayoutElement(private val layoutImpl: SceneTransitionLayoutIm private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : Modifier.Node(), ApproachLayoutModifierNode { - override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean { - return layoutImpl.state.currentTransition == null + override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { + return layoutImpl.state.isTransitioning() } @ExperimentalComposeUiApi diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt index bd36cb8655ac..b392c679e49c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/modifiers/Size.kt @@ -18,15 +18,17 @@ package com.android.compose.animation.scene.modifiers import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.intermediateLayout +import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.unit.Constraints import com.android.compose.animation.scene.SceneTransitionLayoutState @OptIn(ExperimentalComposeUiApi::class) internal fun Modifier.noResizeDuringTransitions(layoutState: SceneTransitionLayoutState): Modifier { - return intermediateLayout { measurable, constraints -> + return approachLayout(isMeasurementApproachInProgress = { layoutState.isTransitioning() }) { + measurable, + constraints -> if (layoutState.currentTransition == null) { - return@intermediateLayout measurable.measure(constraints).run { + return@approachLayout measurable.measure(constraints).run { layout(width, height) { place(0, 0) } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index b1d7055573b6..92e1b2cd030c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.layout.intermediateLayout +import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo @@ -91,7 +91,9 @@ class ElementTest { modifier .offset(offset) .element(key) - .intermediateLayout { measurable, constraints -> + .approachLayout( + isMeasurementApproachInProgress = { layoutState.isTransitioning() } + ) { measurable, constraints -> onLayout() val placement = measurable.measure(constraints) layout(placement.width, placement.height) { @@ -525,7 +527,7 @@ class ElementTest { // page should be composed. HorizontalPager( pagerState, - outOfBoundsPageCount = 0, + beyondViewportPageCount = 0, ) { page -> when (page) { 0 -> Box(Modifier.element(TestElements.Foo).fillMaxSize()) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt index ac7717b41a5a..ce4c52757e78 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt @@ -202,11 +202,11 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic - fun data(): List<TestCase> = - listOf( - TestCase(NestedScrollSource.Drag), - TestCase(NestedScrollSource.Fling), - TestCase(NestedScrollSource.Wheel), + fun data(): List<TestCase> { + return listOf( + TestCase(NestedScrollSource.UserInput), + TestCase(NestedScrollSource.SideEffect), ) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index c96a8ce9e159..569116c6124a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -24,17 +24,21 @@ import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.widget.RemoteViews import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS @@ -45,8 +49,14 @@ import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.andSceneContainer import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager @@ -63,6 +73,7 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -94,6 +105,8 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var userRepository: FakeUserRepository private lateinit var shadeTestUtil: ShadeTestUtil + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var communalRepository: FakeCommunalRepository private lateinit var underTest: CommunalViewModel @@ -106,12 +119,14 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { MockitoAnnotations.initMocks(this) keyguardRepository = kosmos.fakeKeyguardRepository + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository tutorialRepository = kosmos.fakeCommunalTutorialRepository widgetRepository = kosmos.fakeCommunalWidgetRepository smartspaceRepository = kosmos.fakeSmartspaceRepository mediaRepository = kosmos.fakeCommunalMediaRepository userRepository = kosmos.fakeUserRepository shadeTestUtil = kosmos.shadeTestUtil + communalRepository = kosmos.fakeCommunalRepository kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) @@ -125,6 +140,8 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { underTest = CommunalViewModel( testScope, + context.resources, + kosmos.keyguardTransitionInteractor, kosmos.communalInteractor, kosmos.communalTutorialInteractor, kosmos.shadeInteractor, @@ -326,6 +343,129 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { assertThat(underTest.canChangeScene()).isFalse() } + @Test + fun touchesAllowed_shadeNotExpanded() = + testScope.runTest { + val touchesAllowed by collectLastValue(underTest.touchesAllowed) + + // On keyguard without any shade expansion. + kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + shadeTestUtil.setLockscreenShadeExpansion(0f) + runCurrent() + assertThat(touchesAllowed).isTrue() + } + + @Test + fun touchesAllowed_shadeExpanded() = + testScope.runTest { + val touchesAllowed by collectLastValue(underTest.touchesAllowed) + + // On keyguard with shade fully expanded. + kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + shadeTestUtil.setLockscreenShadeExpansion(1f) + runCurrent() + assertThat(touchesAllowed).isFalse() + } + + @Test + fun isFocusable_isFalse_whenTransitioningAwayFromGlanceableHub() = + testScope.runTest { + val isFocusable by collectLastValue(underTest.isFocusable) + + // Shade not expanded. + shadeTestUtil.setLockscreenShadeExpansion(0f) + // On communal scene. + communalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + // Open bouncer. + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.PRIMARY_BOUNCER, + transitionState = TransitionState.STARTED, + ) + ) + + keyguardTransitionRepository.sendTransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.PRIMARY_BOUNCER, + transitionState = TransitionState.RUNNING, + value = 0.5f, + ) + assertThat(isFocusable).isEqualTo(false) + + // Transitioned to bouncer. + keyguardTransitionRepository.sendTransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.PRIMARY_BOUNCER, + transitionState = TransitionState.FINISHED, + value = 1f, + ) + assertThat(isFocusable).isEqualTo(false) + } + + @Test + fun isFocusable_isFalse_whenNotOnCommunalScene() = + testScope.runTest { + val isFocusable by collectLastValue(underTest.isFocusable) + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope = testScope, + ) + shadeTestUtil.setLockscreenShadeExpansion(0f) + // Transitioned away from communal scene. + communalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Blank)) + ) + + assertThat(isFocusable).isEqualTo(false) + } + + @Test + fun isFocusable_isTrue_whenIdleOnCommunal_andShadeNotExpanded() = + testScope.runTest { + val isFocusable by collectLastValue(underTest.isFocusable) + + // On communal scene. + communalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + // Transitioned to Glanceable hub. + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope = testScope, + ) + // Shade not expanded. + shadeTestUtil.setLockscreenShadeExpansion(0f) + + assertThat(isFocusable).isEqualTo(true) + } + + @Test + fun isFocusable_isFalse_whenQsIsExpanded() = + testScope.runTest { + val isFocusable by collectLastValue(underTest.isFocusable) + + // On communal scene. + communalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + // Transitioned to Glanceable hub. + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope = testScope, + ) + // Qs is expanded. + shadeTestUtil.setQsExpansion(1f) + + assertThat(isFocusable).isEqualTo(false) + } + private suspend fun setIsMainUser(isMainUser: Boolean) { whenever(user.isMain).thenReturn(isMainUser) userRepository.setUserInfos(listOf(user)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index d7023307b0b9..360f284f3072 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -33,6 +33,7 @@ package com.android.systemui.keyguard.domain.interactor import android.os.PowerManager +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -46,6 +47,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor @@ -317,6 +319,51 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetectedInAod_fromGone() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + // Make sure we're GONE. + assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + + // Start going to AOD on first button push + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + transitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + transitionState = TransitionState.STARTED, + value = 0f + ), + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + transitionState = TransitionState.RUNNING, + value = 0.1f + ), + ), + testScope = testScope, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + advanceTimeBy(100) // account for debouncing + + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE) + } + + @Test fun testTransitionToLockscreen_onWakeUpFromAod_dismissibleKeyguard_securitySwipe() = testScope.runTest { kosmos.fakeKeyguardRepository.setKeyguardShowing(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt index 0a29821c0660..3b6f6a2d5e1a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,20 +12,19 @@ * 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.keyguard.domain.interactor -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository -import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState @@ -33,7 +32,10 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeLockscreenInteractor +import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor @@ -50,15 +52,18 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @ExperimentalCoroutinesApi @SmallTest -@RunWith(AndroidJUnit4::class) -class UdfpsKeyguardInteractorTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class UdfpsKeyguardInteractorTest(flags: FlagsParameterization?) : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope - val configRepository = kosmos.fakeConfigurationRepository val keyguardRepository = kosmos.fakeKeyguardRepository + val shadeRepository = kosmos.fakeShadeRepository + val shadeTestUtil by lazy { kosmos.shadeTestUtil } private val burnInProgress = 1f private val burnInYOffset = 20 @@ -67,7 +72,6 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() { private lateinit var bouncerRepository: KeyguardBouncerRepository private lateinit var fakeCommandQueue: FakeCommandQueue private lateinit var burnInInteractor: BurnInInteractor - private lateinit var shadeRepository: FakeShadeRepository private lateinit var powerInteractor: PowerInteractor @Mock private lateinit var burnInHelper: BurnInHelperWrapper @@ -75,11 +79,22 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() { private lateinit var underTest: UdfpsKeyguardInteractor + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) bouncerRepository = FakeKeyguardBouncerRepository() - shadeRepository = FakeShadeRepository() fakeCommandQueue = FakeCommandQueue() burnInInteractor = BurnInInteractor( @@ -93,10 +108,10 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() { underTest = UdfpsKeyguardInteractor( - configRepository, burnInInteractor, kosmos.keyguardInteractor, - shadeRepository, + kosmos.shadeInteractor, + kosmos.shadeLockscreenInteractor, dialogManager, ) } @@ -183,13 +198,13 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() { val qsProgress by collectLastValue(underTest.qsProgress) assertThat(qsProgress).isEqualTo(0f) - shadeRepository.setQsExpansion(.22f) + shadeTestUtil.setQsExpansion(.22f) assertThat(qsProgress).isEqualTo(.44f) - shadeRepository.setQsExpansion(.5f) + shadeTestUtil.setQsExpansion(.5f) assertThat(qsProgress).isEqualTo(1f) - shadeRepository.setQsExpansion(.7f) + shadeTestUtil.setQsExpansion(.7f) assertThat(qsProgress).isEqualTo(1f) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index e3eca6729188..030aa243db89 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -19,24 +19,25 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.flag.junit.FlagsParameterization import android.view.View -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.Flags as AConfigFlags import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalRepository import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController @@ -53,32 +54,50 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import com.android.systemui.Flags as AConfigFlags @SmallTest -@RunWith(AndroidJUnit4::class) -class KeyguardRootViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class KeyguardRootViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val communalRepository = kosmos.communalRepository - private val screenOffAnimationController = kosmos.screenOffAnimationController - private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository - private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor - private val dozeParameters = kosmos.dozeParameters - private val shadeRepository = kosmos.fakeShadeRepository + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val communalRepository by lazy { kosmos.communalRepository } + private val screenOffAnimationController by lazy { kosmos.screenOffAnimationController } + private val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository } + private val notificationsKeyguardInteractor by lazy { kosmos.notificationsKeyguardInteractor } + private val dozeParameters by lazy { kosmos.dozeParameters } + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } private val underTest by lazy { kosmos.keyguardRootViewModel } private val viewState = ViewStateAccessor() + // add to init block + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setUp() { - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) mSetFlagsRule.enableFlags(FLAG_NEW_AOD_TRANSITION) - mSetFlagsRule.disableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_REFACTOR_KEYGUARD_DISMISS_INTENT, - ) + if (!SceneContainerFlag.isEnabled) { + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) + mSetFlagsRule.disableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_REFACTOR_KEYGUARD_DISMISS_INTENT, + ) + } } @Test @@ -336,10 +355,10 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope, ) - shadeRepository.setQsExpansion(0f) + shadeTestUtil.setQsExpansion(0f) assertThat(alpha).isEqualTo(1f) - shadeRepository.setQsExpansion(0.5f) + shadeTestUtil.setQsExpansion(0.5f) assertThat(alpha).isEqualTo(0f) } @@ -356,11 +375,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { ) // Open the shade. - shadeRepository.setQsExpansion(1f) + shadeTestUtil.setQsExpansion(1f) assertThat(alpha).isEqualTo(0f) // Close the shade, alpha returns to 1. - shadeRepository.setQsExpansion(0f) + shadeTestUtil.setQsExpansion(0f) assertThat(alpha).isEqualTo(1f) } @@ -377,11 +396,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { ) // Open the shade. - shadeRepository.setQsExpansion(1f) + shadeTestUtil.setQsExpansion(1f) assertThat(alpha).isEqualTo(0f) // Close the shade, alpha is still 0 since we're not on the lockscreen. - shadeRepository.setQsExpansion(0f) + shadeTestUtil.setQsExpansion(0f) assertThat(alpha).isEqualTo(0f) } @@ -400,7 +419,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { assertThat(alpha).isEqualTo(0f) // Try pulling down shade and ensure the value doesn't change - shadeRepository.setQsExpansion(0.5f) + shadeTestUtil.setQsExpansion(0.5f) assertThat(alpha).isEqualTo(0f) } @@ -419,7 +438,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { assertThat(alpha).isEqualTo(0f) // Try pulling down shade and ensure the value doesn't change - shadeRepository.setQsExpansion(0.5f) + shadeTestUtil.setQsExpansion(0.5f) assertThat(alpha).isEqualTo(0f) } @@ -438,7 +457,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { assertThat(alpha).isEqualTo(0f) // Try pulling down shade and ensure the value doesn't change - shadeRepository.setQsExpansion(0.5f) + shadeTestUtil.setQsExpansion(0.5f) assertThat(alpha).isEqualTo(0f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 49073595af0c..ec2cb049836f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -16,13 +16,15 @@ package com.android.systemui.keyguard.ui.viewmodel -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.authController import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.Flags +import com.android.systemui.flags.andSceneContainer import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.shared.model.ClockSize @@ -40,15 +42,29 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class LockscreenContentViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class LockscreenContentViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos: Kosmos = testKosmos() lateinit var underTest: LockscreenContentViewModel + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setup() { with(kosmos) { @@ -77,6 +93,7 @@ class LockscreenContentViewModelTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun clockSize_withLargeClock_true() = with(kosmos) { testScope.runTest { @@ -87,6 +104,7 @@ class LockscreenContentViewModelTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun clockSize_withSmallClock_false() = with(kosmos) { testScope.runTest { @@ -109,6 +127,7 @@ class LockscreenContentViewModelTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun areNotificationsVisible_withSmallClock_true() = with(kosmos) { testScope.runTest { @@ -119,6 +138,7 @@ class LockscreenContentViewModelTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun areNotificationsVisible_withLargeClock_false() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 776f1a55fdcb..bc9d257b3836 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakefulnessState @@ -57,6 +58,7 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) +@EnableSceneContainer class LockscreenSceneViewModelTest : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt index 769a54aee862..13d6411382cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt @@ -58,13 +58,16 @@ import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupReposi import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Assume.assumeFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -@TestableLooper.RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class InternetTileDataInteractorTest : SysuiTestCase() { @@ -141,6 +144,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { underTest = InternetTileDataInteractor( context, + testScope.coroutineContext, testScope.backgroundScope, airplaneModeRepository, connectivityRepository, @@ -433,8 +437,44 @@ class InternetTileDataInteractorTest : SysuiTestCase() { .isEqualTo(expectedCd) } + /** + * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the + * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler() + * on the mentioned context. Since that context does not have a looper assigned to it, the + * handler instantiation will throw a RuntimeException. + * + * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception + * So either we should make Robolectric behvase similar to the device test, or change this + * test to look for a different signal than the exception, when run by Robolectric. For now + * we just assume the test is not Robolectric. + */ + @Test(expected = java.lang.RuntimeException::class) + fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() = + testScope.runTest { + assumeFalse(isRobolectricTest()); + + collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) + + connectivityRepository.setMobileConnected() + mobileConnectionsRepository.mobileIsDefault.value = true + mobileConnectionRepository.apply { + setAllLevels(3) + setAllRoaming(false) + networkName.value = NetworkNameModel.Default("test network") + } + + runCurrent() + } + + /** + * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the + * problem this test solves. The solution here is to assign a looper to the context via + * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for + * creating the SignalDrawable. + */ + @TestableLooper.RunWithLooper @Test - fun mobileDefault_usesNetworkNameAndIcon() = + fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() = testScope.runTest { val latest by collectLastValue( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt index 818c19ca9de0..4e0685556145 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -16,22 +16,26 @@ package com.android.systemui.volume.panel.ui.viewmodel +import android.content.Intent +import android.content.applicationContext import android.content.res.Configuration import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos import com.android.systemui.volume.panel.componentByKey import com.android.systemui.volume.panel.componentsLayoutManager import com.android.systemui.volume.panel.criteriaByKey -import com.android.systemui.volume.panel.dagger.factory.KosmosVolumePanelComponentFactory import com.android.systemui.volume.panel.mockVolumePanelUiComponentProvider import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager import com.android.systemui.volume.panel.unavailableCriteria +import com.android.systemui.volume.panel.volumePanelViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -51,67 +55,49 @@ class VolumePanelViewModelTest : SysuiTestCase() { private lateinit var underTest: VolumePanelViewModel - private fun initUnderTest() { - underTest = - VolumePanelViewModel( - testableResources.resources, - kosmos.testScope.backgroundScope, - KosmosVolumePanelComponentFactory(kosmos), - kosmos.fakeConfigurationController, - ) - } - @Test - fun dismissingPanel_changesVisibility() { - with(kosmos) { - testScope.runTest { - initUnderTest() - assertThat(underTest.volumePanelState.value.isVisible).isTrue() + fun dismissingPanel_changesVisibility() = test { + testScope.runTest { + assertThat(underTest.volumePanelState.value.isVisible).isTrue() - underTest.dismissPanel() - runCurrent() + underTest.dismissPanel() + runCurrent() - assertThat(underTest.volumePanelState.value.isVisible).isFalse() - } + assertThat(underTest.volumePanelState.value.isVisible).isFalse() } } @Test - fun orientationChanges_panelOrientationChanges() { - with(kosmos) { - testScope.runTest { - initUnderTest() - val volumePanelState by collectLastValue(underTest.volumePanelState) - testableResources.overrideConfiguration( - Configuration().apply { orientation = Configuration.ORIENTATION_PORTRAIT } - ) - assertThat(volumePanelState!!.orientation) - .isEqualTo(Configuration.ORIENTATION_PORTRAIT) + fun orientationChanges_panelOrientationChanges() = test { + testScope.runTest { + val volumePanelState by collectLastValue(underTest.volumePanelState) + testableResources.overrideConfiguration( + Configuration().apply { orientation = Configuration.ORIENTATION_PORTRAIT } + ) + assertThat(volumePanelState!!.orientation).isEqualTo(Configuration.ORIENTATION_PORTRAIT) - fakeConfigurationController.onConfigurationChanged( - Configuration().apply { orientation = Configuration.ORIENTATION_LANDSCAPE } - ) - runCurrent() + fakeConfigurationController.onConfigurationChanged( + Configuration().apply { orientation = Configuration.ORIENTATION_LANDSCAPE } + ) + runCurrent() - assertThat(volumePanelState!!.orientation) - .isEqualTo(Configuration.ORIENTATION_LANDSCAPE) - } + assertThat(volumePanelState!!.orientation) + .isEqualTo(Configuration.ORIENTATION_LANDSCAPE) } } @Test - fun components_areReturned() { - with(kosmos) { + fun components_areReturned() = + test({ + componentByKey = + mapOf( + COMPONENT_1 to mockVolumePanelUiComponentProvider, + COMPONENT_2 to mockVolumePanelUiComponentProvider, + BOTTOM_BAR to mockVolumePanelUiComponentProvider, + ) + criteriaByKey = mapOf(COMPONENT_2 to unavailableCriteria) + }) { testScope.runTest { - componentByKey = - mapOf( - COMPONENT_1 to mockVolumePanelUiComponentProvider, - COMPONENT_2 to mockVolumePanelUiComponentProvider, - BOTTOM_BAR to mockVolumePanelUiComponentProvider, - ) - criteriaByKey = mapOf(COMPONENT_2 to unavailableCriteria) - initUnderTest() - val componentsLayout by collectLastValue(underTest.componentsLayout) runCurrent() @@ -124,11 +110,45 @@ class VolumePanelViewModelTest : SysuiTestCase() { assertThat(componentsLayout!!.bottomBarComponent.isVisible).isTrue() } } + + @Test + fun dismissPanel_dismissesPanel() = test { + testScope.runTest { + val volumePanelState by collectLastValue(underTest.volumePanelState) + underTest.dismissPanel() + runCurrent() + + assertThat(volumePanelState!!.isVisible).isFalse() + } + } + + @Test + fun dismissBroadcast_dismissesPanel() = test { + testScope.runTest { + runCurrent() // run the flows to let allow the receiver to be registered + val volumePanelState by collectLastValue(underTest.volumePanelState) + broadcastDispatcher.sendIntentToMatchingReceiversOnly( + applicationContext, + Intent(DISMISS_ACTION), + ) + runCurrent() + + assertThat(volumePanelState!!.isVisible).isFalse() + } } + private fun test(setup: Kosmos.() -> Unit = {}, test: Kosmos.() -> Unit) = + with(kosmos) { + setup() + underTest = volumePanelViewModel + test() + } + private companion object { const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar" const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" + + const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" } } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index 3b6b5a0db393..2a8f1b596711 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -66,6 +66,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> @@ -127,4 +128,4 @@ </androidx.constraintlayout.motion.widget.MotionLayout> -</com.android.keyguard.KeyguardPasswordView>
\ No newline at end of file +</com.android.keyguard.KeyguardPasswordView> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 5aac65396532..76f6f599c54c 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -31,6 +31,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml index 6780e5721af3..5879c110d8a1 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml @@ -67,6 +67,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> @@ -107,4 +108,4 @@ </androidx.constraintlayout.motion.widget.MotionLayout> -</com.android.keyguard.KeyguardPatternView>
\ No newline at end of file +</com.android.keyguard.KeyguardPatternView> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index d991581c2665..3f7b02835357 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -35,6 +35,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml index 6c79d5a6095a..b464fb3bafed 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml @@ -74,6 +74,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> @@ -241,4 +242,4 @@ </androidx.constraintlayout.motion.widget.MotionLayout> -</com.android.keyguard.KeyguardPINView>
\ No newline at end of file +</com.android.keyguard.KeyguardPINView> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index f3cd9e49b49c..21580731aed2 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -32,6 +32,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" + android:importantForAccessibility="noHideDescendants" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml index 045c19eb09dc..65d6e2863130 100644 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml @@ -14,36 +14,20 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:width="16dp" + android:height="16dp" + android:viewportWidth="16.0" + android:viewportHeight="16.0"> <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:pathData="M15.632,8H14.902C14.707,8 14.553,8.155 14.543,8.35C14.367,11.692 11.693,14.417 8.356,14.552C8.158,14.56 8,14.712 8,14.909V15.552C8,15.84 8.172,16.009 8.377,16C12.493,15.808 15.808,12.494 16,8.378C16.009,8.172 15.837,8 15.632,8Z" + android:fillAlpha="0.18" android:fillColor="#fff"/> <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:pathData="M12.643,8C12.932,8 13.105,8.175 13.09,8.382C12.903,10.892 10.892,12.903 8.382,13.09C8.175,13.105 8,12.93 8,12.723V11.993C8,11.798 8.153,11.648 8.347,11.63C10.089,11.467 11.546,10.092 11.645,8.362C11.657,8.163 11.807,8 12.006,8L12.643,8Z" + android:fillAlpha="0.18" android:fillColor="#fff"/> <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillAlpha="0.3" + android:pathData="M7.743,10.057L6.14,8.451L5.205,9.386L6.105,10.266C6.684,10.832 6.69,11.76 6.119,12.333L5.065,13.391C4.78,13.676 4.407,13.818 4.035,13.818C3.662,13.818 3.289,13.676 3.004,13.391L0.425,10.807C-0.136,10.245 -0.143,9.338 0.41,8.768L1.404,7.716C1.69,7.421 2.07,7.273 2.45,7.273C2.817,7.273 3.184,7.411 3.467,7.688L4.425,8.624L5.369,7.68L1.712,4.026C1.57,3.884 1.57,3.654 1.712,3.512L3.513,1.711C3.655,1.57 3.885,1.57 4.027,1.711L7.684,5.366L8.608,4.442L7.701,3.534C7.14,2.972 7.134,2.065 7.686,1.495L8.68,0.443C8.966,0.148 9.346,0 9.726,0C10.093,0 10.46,0.138 10.743,0.415L13.381,2.992C13.959,3.557 13.965,4.488 13.394,5.061L12.341,6.118C12.056,6.403 11.684,6.545 11.311,6.545C10.938,6.545 10.565,6.403 10.281,6.118L9.379,5.214L8.456,6.137L10.059,7.743C10.2,7.885 10.2,8.115 10.058,8.257L8.257,10.057C8.115,10.199 7.885,10.199 7.743,10.057ZM10.412,4.182L11.053,4.83C11.195,4.973 11.427,4.974 11.569,4.831L12.104,4.294C12.247,4.151 12.245,3.919 12.101,3.777L11.451,3.142L10.412,4.182ZM8.99,2.744L9.645,3.406L10.671,2.379L9.989,1.713C9.844,1.571 9.61,1.576 9.471,1.724L8.984,2.239C8.85,2.381 8.852,2.604 8.99,2.744ZM3.395,9.653L2.713,8.986C2.568,8.844 2.334,8.849 2.194,8.997L1.708,9.512C1.574,9.654 1.576,9.878 1.714,10.017L2.369,10.679L3.395,9.653ZM4.825,11.051L4.176,10.416L3.136,11.455L3.777,12.103C3.919,12.247 4.151,12.247 4.293,12.104L4.828,11.567C4.971,11.424 4.969,11.192 4.825,11.051Z" android:fillColor="#fff"/> </vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml index 5e012ab7edbd..0399b81b859e 100644 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml @@ -16,34 +16,18 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:width="16dp" + android:height="16dp" + android:viewportWidth="16.0" + android:viewportHeight="16.0"> <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:pathData="M15.632,8H14.902C14.707,8 14.553,8.155 14.543,8.35C14.367,11.692 11.693,14.417 8.356,14.552C8.158,14.56 8,14.712 8,14.909V15.552C8,15.84 8.172,16.009 8.377,16C12.493,15.808 15.808,12.494 16,8.378C16.009,8.172 15.837,8 15.632,8Z" + android:fillAlpha="0.18" android:fillColor="#fff"/> <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:pathData="M12.643,8C12.932,8 13.105,8.175 13.09,8.382C12.903,10.892 10.892,12.903 8.382,13.09C8.175,13.105 8,12.93 8,12.723V11.993C8,11.798 8.153,11.648 8.347,11.63C10.089,11.467 11.546,10.092 11.645,8.362C11.657,8.163 11.807,8 12.006,8L12.643,8Z" android:fillColor="#fff"/> <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" + android:pathData="M7.743,10.057L6.14,8.451L5.205,9.386L6.105,10.266C6.684,10.832 6.69,11.76 6.119,12.333L5.065,13.391C4.78,13.676 4.407,13.818 4.035,13.818C3.662,13.818 3.289,13.676 3.004,13.391L0.425,10.807C-0.136,10.245 -0.143,9.338 0.41,8.768L1.404,7.716C1.69,7.421 2.07,7.273 2.45,7.273C2.817,7.273 3.184,7.411 3.467,7.688L4.425,8.624L5.369,7.68L1.712,4.026C1.57,3.884 1.57,3.654 1.712,3.512L3.513,1.711C3.655,1.57 3.885,1.57 4.027,1.711L7.684,5.366L8.608,4.442L7.701,3.534C7.14,2.972 7.134,2.065 7.686,1.495L8.68,0.443C8.966,0.148 9.346,0 9.726,0C10.093,0 10.46,0.138 10.743,0.415L13.381,2.992C13.959,3.557 13.965,4.488 13.394,5.061L12.341,6.118C12.056,6.403 11.684,6.545 11.311,6.545C10.938,6.545 10.565,6.403 10.281,6.118L9.379,5.214L8.456,6.137L10.059,7.743C10.2,7.885 10.2,8.115 10.058,8.257L8.257,10.057C8.115,10.199 7.885,10.199 7.743,10.057ZM10.412,4.182L11.053,4.83C11.195,4.973 11.427,4.974 11.569,4.831L12.104,4.294C12.247,4.151 12.245,3.919 12.101,3.777L11.451,3.142L10.412,4.182ZM8.99,2.744L9.645,3.406L10.671,2.379L9.989,1.713C9.844,1.571 9.61,1.576 9.471,1.724L8.984,2.239C8.85,2.381 8.852,2.604 8.99,2.744ZM3.395,9.653L2.713,8.986C2.568,8.844 2.334,8.849 2.194,8.997L1.708,9.512C1.574,9.654 1.576,9.878 1.714,10.017L2.369,10.679L3.395,9.653ZM4.825,11.051L4.176,10.416L3.136,11.455L3.777,12.103C3.919,12.247 4.151,12.247 4.293,12.104L4.828,11.567C4.971,11.424 4.969,11.192 4.825,11.051Z" android:fillColor="#fff"/> </vector> - diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml index d8a9a703260d..b0acbdccfb4e 100644 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml @@ -16,32 +16,17 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:width="16dp" + android:height="16dp" + android:viewportWidth="16.0" + android:viewportHeight="16.0"> <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:pathData="M15.632,8H14.902C14.707,8 14.553,8.155 14.543,8.35C14.367,11.692 11.693,14.417 8.356,14.552C8.158,14.56 8,14.712 8,14.909V15.552C8,15.84 8.172,16.009 8.377,16C12.493,15.808 15.808,12.494 16,8.378C16.009,8.172 15.837,8 15.632,8Z" android:fillColor="#fff"/> <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:pathData="M12.643,8C12.932,8 13.105,8.175 13.09,8.382C12.903,10.892 10.892,12.903 8.382,13.09C8.175,13.105 8,12.93 8,12.723V11.993C8,11.798 8.153,11.648 8.347,11.63C10.089,11.467 11.546,10.092 11.645,8.362C11.657,8.163 11.807,8 12.006,8L12.643,8Z" android:fillColor="#fff"/> <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" + android:pathData="M7.743,10.057L6.14,8.451L5.205,9.386L6.105,10.266C6.684,10.832 6.69,11.76 6.119,12.333L5.065,13.391C4.78,13.676 4.407,13.818 4.035,13.818C3.662,13.818 3.289,13.676 3.004,13.391L0.425,10.807C-0.136,10.245 -0.143,9.338 0.41,8.768L1.404,7.716C1.69,7.421 2.07,7.273 2.45,7.273C2.817,7.273 3.184,7.411 3.467,7.688L4.425,8.624L5.369,7.68L1.712,4.026C1.57,3.884 1.57,3.654 1.712,3.512L3.513,1.711C3.655,1.57 3.885,1.57 4.027,1.711L7.684,5.366L8.608,4.442L7.701,3.534C7.14,2.972 7.134,2.065 7.686,1.495L8.68,0.443C8.966,0.148 9.346,0 9.726,0C10.093,0 10.46,0.138 10.743,0.415L13.381,2.992C13.959,3.557 13.965,4.488 13.394,5.061L12.341,6.118C12.056,6.403 11.684,6.545 11.311,6.545C10.938,6.545 10.565,6.403 10.281,6.118L9.379,5.214L8.456,6.137L10.059,7.743C10.2,7.885 10.2,8.115 10.058,8.257L8.257,10.057C8.115,10.199 7.885,10.199 7.743,10.057ZM10.412,4.182L11.053,4.83C11.195,4.973 11.427,4.974 11.569,4.831L12.104,4.294C12.247,4.151 12.245,3.919 12.101,3.777L11.451,3.142L10.412,4.182ZM8.99,2.744L9.645,3.406L10.671,2.379L9.989,1.713C9.844,1.571 9.61,1.576 9.471,1.724L8.984,2.239C8.85,2.381 8.852,2.604 8.99,2.744ZM3.395,9.653L2.713,8.986C2.568,8.844 2.334,8.849 2.194,8.997L1.708,9.512C1.574,9.654 1.576,9.878 1.714,10.017L2.369,10.679L3.395,9.653ZM4.825,11.051L4.176,10.416L3.136,11.455L3.777,12.103C3.919,12.247 4.151,12.247 4.293,12.104L4.828,11.567C4.971,11.424 4.969,11.192 4.825,11.051Z" android:fillColor="#fff"/> </vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml index a80d3b4d0fa8..2cab0434d649 100644 --- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml +++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml @@ -16,28 +16,11 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0" - android:alpha="0.3" - > + android:width="14dp" + android:height="14dp" + android:viewportWidth="14.0" + android:viewportHeight="14.0"> <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" + android:pathData="M7.743,10.057L6.14,8.451L5.205,9.386L6.105,10.266C6.684,10.832 6.69,11.76 6.119,12.333L5.065,13.391C4.78,13.676 4.407,13.818 4.035,13.818C3.662,13.818 3.289,13.676 3.004,13.391L0.425,10.807C-0.136,10.245 -0.143,9.338 0.41,8.768L1.404,7.716C1.69,7.421 2.07,7.273 2.45,7.273C2.817,7.273 3.184,7.411 3.467,7.688L4.425,8.624L5.369,7.68L1.712,4.026C1.57,3.884 1.57,3.654 1.712,3.512L3.513,1.711C3.655,1.57 3.885,1.57 4.027,1.711L7.684,5.366L8.608,4.442L7.701,3.534C7.14,2.972 7.134,2.065 7.686,1.495L8.68,0.443C8.966,0.148 9.346,0 9.726,0C10.093,0 10.46,0.138 10.743,0.415L13.381,2.992C13.959,3.557 13.965,4.488 13.394,5.061L12.341,6.118C12.056,6.403 11.684,6.545 11.311,6.545C10.938,6.545 10.565,6.403 10.281,6.118L9.379,5.214L8.456,6.137L10.059,7.743C10.2,7.885 10.2,8.115 10.058,8.257L8.257,10.057C8.115,10.199 7.885,10.199 7.743,10.057ZM10.412,4.182L11.053,4.83C11.195,4.973 11.427,4.974 11.569,4.831L12.104,4.294C12.247,4.151 12.245,3.919 12.101,3.777L11.451,3.142L10.412,4.182ZM8.99,2.744L9.645,3.406L10.671,2.379L9.989,1.713C9.844,1.571 9.61,1.576 9.471,1.724L8.984,2.239C8.85,2.381 8.852,2.604 8.99,2.744ZM3.395,9.653L2.713,8.986C2.568,8.844 2.334,8.849 2.194,8.997L1.708,9.512C1.574,9.654 1.576,9.878 1.714,10.017L2.369,10.679L3.395,9.653ZM4.825,11.051L4.176,10.416L3.136,11.455L3.777,12.103C3.919,12.247 4.151,12.247 4.293,12.104L4.828,11.567C4.971,11.424 4.969,11.192 4.825,11.051Z" android:fillColor="#fff"/> </vector> diff --git a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml index d13efd25e353..f644584f747a 100644 --- a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml @@ -174,7 +174,7 @@ android:layout_height="match_parent"> android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" - android:accessibilityLiveRegion="polite" + android:accessibilityLiveRegion="assertive" android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" diff --git a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml index a6e660f940df..46b8e4665a22 100644 --- a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml @@ -152,7 +152,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" - android:accessibilityLiveRegion="polite" + android:accessibilityLiveRegion="assertive" android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index c724d24a31da..d51fe5849133 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -150,7 +150,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" - android:accessibilityLiveRegion="polite" + android:accessibilityLiveRegion="assertive" android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 19273ec3af82..6bfd0887c404 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -461,10 +461,11 @@ This name is in the ComponentName flattened format (package/class) --> <string name="config_screenshotEditor" translatable="false"></string> - <!-- ComponentName for the file browsing app that the system would expect to be used in work - profile. The icon for this app will be shown to the user when informing them that a - screenshot has been saved to work profile. If blank, a default icon will be shown. --> - <string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string> + <!-- ComponentName for the file browsing app that the system would expect to be used for + screenshots. The icon for this app will be shown to the user when informing them that a + screenshot has been saved to a different profile (e.g. work profile). If blank, a default + icon will be shown. --> + <string name="config_screenshotFilesApp" translatable="false"></string> <!-- The component name of the screenshot editing activity that provides the App Clips flow. The App Clips flow includes taking a screenshot, showing user screenshot cropping activity diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index b8e78a44a8ed..40d863d1178e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -257,6 +257,8 @@ <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string> <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] --> <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string> + <!-- Notification displayed when a screenshot is saved in the private profile. [CHAR LIMIT=NONE] --> + <string name="screenshot_private_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the private profile</string> <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] --> <string name="screenshot_default_files_app_name">Files</string> <!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] --> @@ -1182,6 +1184,8 @@ <string name="accessibility_action_label_edit_widgets">Customize widgets</string> <!-- Accessibility content description for communal hub. [CHAR LIMIT=NONE] --> <string name="accessibility_content_description_for_communal_hub">Widgets on lock screen</string> + <!-- Label for accessibility action to select a widget in edit mode. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_select_widget">select widget</string> <!-- Related to user switcher --><skip/> @@ -1719,9 +1723,7 @@ <string name="accessibility_status_bar_satellite_available">Satellite, connection available</string> <!-- Text displayed indicating that the user is connected to a satellite signal. --> - <string name="satellite_connected_carrier_text">Connected to satellite</string> - <!-- Text displayed indicating that the user is not connected to a satellite signal. --> - <string name="satellite_not_connected_carrier_text">Not connected to satellite</string> + <string name="satellite_connected_carrier_text">Satellite SOS</string> <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] --> <string name="accessibility_managed_profile">Work profile</string> @@ -2261,6 +2263,9 @@ <!-- Accessibility description when QS tile is to be added, indicating the destination position [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_add_to_position">Add to position <xliff:g id="position" example="5">%1$d</xliff:g></string> + <!-- Accessibility description when QS tile would be added or moved, but the current position is not valid for adding or moving to [CHAR LIMIT=NONE] --> + <string name="accessibilit_qs_edit_tile_add_move_invalid_position">Position invalid.</string> + <!-- Accessibility description indicating the currently selected tile's position. Only used for tiles that are currently in use [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_position">Position <xliff:g id="position" example="5">%1$d</xliff:g></string> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index c6716e44899f..68a69d34b75a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -192,6 +192,7 @@ public class SystemActions implements CoreStartable, ConfigurationController.Con private final ShadeController mShadeController; private final Lazy<PanelExpansionInteractor> mPanelExpansionInteractor; private final StatusBarWindowCallback mNotificationShadeCallback; + private final ScreenshotHelper mScreenshotHelper; private boolean mDismissNotificationShadeActionRegistered; @Inject @@ -221,6 +222,7 @@ public class SystemActions implements CoreStartable, ConfigurationController.Con (keyguardShowing, keyguardOccluded, keyguardGoingAway, bouncerShowing, mDozing, panelExpanded, isDreaming) -> registerOrUnregisterDismissNotificationShadeAction(); + mScreenshotHelper = new ScreenshotHelper(mContext); } @Override @@ -516,8 +518,7 @@ public class SystemActions implements CoreStartable, ConfigurationController.Con } private void handleTakeScreenshot() { - ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); - screenshotHelper.takeScreenshot( + mScreenshotHelper.takeScreenshot( SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index a21114798c8f..da5695163cb4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -112,10 +112,6 @@ object BiometricViewBinder { } // set selected to enable marquee unless a screen reader is enabled - logoView.isSelected = - !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled - logoDescriptionView.isSelected = - !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -419,19 +415,6 @@ object BiometricViewBinder { indicatorMessageView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled - - /** - * Note: Talkback 14.0 has new rate-limitation design to reduce frequency of - * TYPE_WINDOW_CONTENT_CHANGED events to once every 30 seconds. (context: - * b/281765653#comment18) Using {@link View#announceForAccessibility} - * instead as workaround since sending events exceeding this frequency is - * required. - */ - indicatorMessageView?.text?.let { - if (it.isNotBlank()) { - view.announceForAccessibility(it) - } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 511bdc4c6cea..db251fde187d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.ui.viewmodel import android.content.ComponentName import android.os.UserHandle +import android.view.View import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -37,8 +38,8 @@ abstract class BaseCommunalViewModel( ) { val currentScene: Flow<SceneKey> = communalInteractor.desiredScene - /** Whether communal hub can be focused to enable accessibility actions. */ - val isFocusable: Flow<Boolean> = communalInteractor.isIdleOnCommunal + /** Whether communal hub should be focused by accessibility tools. */ + open val isFocusable: Flow<Boolean> = MutableStateFlow(false) /** Whether widgets are currently being re-ordered. */ open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) @@ -49,6 +50,9 @@ abstract class BaseCommunalViewModel( val selectedKey: StateFlow<String?> get() = _selectedKey + /** Accessibility delegate to be set on CommunalAppWidgetHostView. */ + open val widgetAccessibilityDelegate: View.AccessibilityDelegate? = null + fun signalUserInteraction() { communalInteractor.signalUserInteraction() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 9dacf8cc4c5a..1120466c7acc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -16,12 +16,18 @@ package com.android.systemui.communal.ui.viewmodel +import android.content.res.Resources +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -29,7 +35,9 @@ import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule +import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineScope @@ -54,6 +62,8 @@ class CommunalViewModel @Inject constructor( @Application private val scope: CoroutineScope, + @Main private val resources: Resources, + keyguardTransitionInteractor: KeyguardTransitionInteractor, private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, private val shadeInteractor: ShadeInteractor, @@ -93,6 +103,37 @@ constructor( private val _currentPopup: MutableStateFlow<PopupType?> = MutableStateFlow(null) override val currentPopup: Flow<PopupType?> = _currentPopup.asStateFlow() + // The widget is focusable for accessibility when the hub is fully visible and shade is not + // opened. + override val isFocusable: Flow<Boolean> = + combine( + keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB), + communalInteractor.isIdleOnCommunal, + shadeInteractor.isAnyFullyExpanded, + ) { transitionedToGlanceableHub, isIdleOnCommunal, isAnyFullyExpanded -> + transitionedToGlanceableHub && isIdleOnCommunal && !isAnyFullyExpanded + } + .distinctUntilChanged() + + override val widgetAccessibilityDelegate = + object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + // Hint user to long press in order to enter edit mode + info.addAction( + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id, + resources + .getString(R.string.accessibility_action_label_edit_widgets) + .lowercase() + ) + ) + } + } + private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow() @@ -191,6 +232,14 @@ constructor( return !shadeInteractor.isAnyFullyExpanded.value } + /** + * Whether touches should be disabled in communal. + * + * This is needed because the notification shade does not block touches in blank areas and these + * fall through to the glanceable hub, which we don't want. + */ + val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded) + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 2fa42ec3904d..7ced9322ed38 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -24,7 +24,6 @@ import com.android.systemui.media.dialog.MediaOutputDialogReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetPinnedReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; import com.android.systemui.screenshot.SmartActionsReceiver; -import com.android.systemui.volume.VolumePanelDialogReceiver; import dagger.Binds; import dagger.Module; @@ -59,15 +58,6 @@ public abstract class DefaultBroadcastReceiverBinder { */ @Binds @IntoMap - @ClassKey(VolumePanelDialogReceiver.class) - public abstract BroadcastReceiver bindVolumePanelDialogReceiver( - VolumePanelDialogReceiver broadcastReceiver); - - /** - * - */ - @Binds - @IntoMap @ClassKey(PeopleSpaceWidgetPinnedReceiver.class) public abstract BroadcastReceiver bindPeopleSpaceWidgetPinnedReceiver( PeopleSpaceWidgetPinnedReceiver broadcastReceiver); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 1fba737c0566..c2843d839d15 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -168,11 +168,13 @@ constructor( keyguardInteractor.isKeyguardOccluded .filterRelevantKeyguardStateAnd { isOccluded -> isOccluded } .collect { - startTransitionTo( - toState = KeyguardState.OCCLUDED, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ownerReason = "isOccluded = true", - ) + if (!maybeHandleInsecurePowerGesture()) { + startTransitionTo( + toState = KeyguardState.OCCLUDED, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ownerReason = "isOccluded = true", + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt index 4abd6c6a6453..f3856710b1ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt @@ -77,7 +77,9 @@ constructor( // transition, to ensure we don't transition while moving between, for example, // *_BOUNCER -> LOCKSCREEN. return powerInteractor.detailedWakefulness.value.powerButtonLaunchGestureTriggered && - KeyguardState.deviceIsAsleepInState(transitionInteractor.getStartedState()) + KeyguardState.deviceIsAsleepInState( + transitionInteractor.currentTransitionInfoInternal.value.to + ) } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 2850165b0d1a..b2a24ca85b07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -225,10 +225,12 @@ sealed class TransitionInteractor( if (!KeyguardWmStateRefactor.isEnabled) { scope.launch { keyguardInteractor.onCameraLaunchDetected.filterRelevantKeyguardState().collect { - startTransitionTo( - toState = KeyguardState.OCCLUDED, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ) + if (!maybeHandleInsecurePowerGesture()) { + startTransitionTo( + toState = KeyguardState.OCCLUDED, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt index f5cd7676e4b1..63bfba604b76 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt @@ -19,10 +19,10 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.FloatEvaluator import android.animation.IntEvaluator -import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.hideAffordancesRequest import javax.inject.Inject @@ -38,17 +38,16 @@ import kotlinx.coroutines.flow.onStart class UdfpsKeyguardInteractor @Inject constructor( - configRepo: ConfigurationRepository, burnInInteractor: BurnInInteractor, keyguardInteractor: KeyguardInteractor, - shadeRepository: ShadeRepository, + shadeInteractor: ShadeInteractor, + shadeLockscreenInteractor: ShadeLockscreenInteractor, dialogManager: SystemUIDialogManager, ) { private val intEvaluator = IntEvaluator() private val floatEvaluator = FloatEvaluator() val dozeAmount = keyguardInteractor.dozeAmount - val scaleForResolution = configRepo.scaleForResolution /** Burn-in offsets for the UDFPS view to mitigate burn-in on AOD. */ val burnInOffsets: Flow<Offsets> = @@ -68,13 +67,14 @@ constructor( val dialogHideAffordancesRequest: Flow<Boolean> = dialogManager.hideAffordancesRequest val qsProgress: Flow<Float> = - shadeRepository.qsExpansion // swipe from top of LS + shadeInteractor.qsExpansion // swipe from top of LS .map { (it * 2).coerceIn(0f, 1f) } .onStart { emit(0f) } val shadeExpansion: Flow<Float> = combine( - shadeRepository.udfpsTransitionToFullShadeProgress, // swipe from middle of LS + shadeLockscreenInteractor + .udfpsTransitionToFullShadeProgress, // swipe from middle of LS keyguardInteractor.statusBarState, // quick swipe from middle of LS ) { shadeProgress, statusBarState -> if (statusBarState == StatusBarState.SHADE_LOCKED) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt index 7e39a884a69e..adc090de1d4a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt @@ -57,5 +57,15 @@ constructor( ) } + fun notificationAlpha(viewState: ViewStateAccessor): Flow<Float> { + var startAlpha = 1f + return transitionAnimation.sharedFlow( + duration = 200.milliseconds, + onStart = { startAlpha = viewState.alpha() }, + onStep = { startAlpha }, + onFinish = { 1f }, + ) + } + override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/people/NotificationHelper.java b/packages/SystemUI/src/com/android/systemui/people/NotificationHelper.java index d863dcce4fc7..710142b6705d 100644 --- a/packages/SystemUI/src/com/android/systemui/people/NotificationHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/NotificationHelper.java @@ -96,7 +96,7 @@ public class NotificationHelper { if (messages2 == null) { return -1; } - return (int) (n2.when - n1.when); + return (int) (n2.getWhen() - n1.getWhen()); } }; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index 58858df32f1b..829c419fc07f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -332,6 +332,14 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta return mTiles.size(); } + public int getItemCountForAccessibility() { + if (mAccessibilityAction == ACTION_MOVE) { + return mEditIndex; + } else { + return getItemCount(); + } + } + @Override public boolean onFailedToRecycleView(Holder holder) { holder.stopDrag(); @@ -406,6 +414,10 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta } else if (selectable && mAccessibilityAction == ACTION_MOVE) { info.state.contentDescription = mContext.getString( R.string.accessibility_qs_edit_tile_move_to_position, position); + } else if (!selectable && (mAccessibilityAction == ACTION_MOVE + || mAccessibilityAction == ACTION_ADD)) { + info.state.contentDescription = mContext.getString( + R.string.accessibilit_qs_edit_tile_add_move_invalid_position); } else { info.state.contentDescription = info.state.label; } @@ -424,14 +436,15 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta holder.mTileView.setOnClickListener(null); holder.mTileView.setFocusable(true); holder.mTileView.setFocusableInTouchMode(true); + holder.mTileView.setAccessibilityTraversalBefore(View.NO_ID); if (mAccessibilityAction != ACTION_NONE) { holder.mTileView.setClickable(selectable); holder.mTileView.setFocusable(selectable); holder.mTileView.setFocusableInTouchMode(selectable); - holder.mTileView.setImportantForAccessibility(selectable - ? View.IMPORTANT_FOR_ACCESSIBILITY_YES - : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); +// holder.mTileView.setImportantForAccessibility(selectable +// ? View.IMPORTANT_FOR_ACCESSIBILITY_YES +// : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); if (selectable) { holder.mTileView.setOnClickListener(new OnClickListener() { @Override @@ -911,4 +924,5 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta int estimatedTileViewHeight = mTempTextView.getMeasuredHeight() * 2 + padding * 2; mMinTileViewHeight = Math.max(minHeight, estimatedTileViewHeight); } + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index d5b05ef68288..60469c070bf7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -116,8 +116,6 @@ import javax.inject.Inject; public class InternetDialogController implements AccessPointController.AccessPointCallback { private static final String TAG = "InternetDialogController"; - private static final String ACTION_NETWORK_PROVIDER_SETTINGS = - "android.settings.NETWORK_PROVIDER_SETTINGS"; private static final String ACTION_WIFI_SCANNING_SETTINGS = "android.settings.WIFI_SCANNING_SETTINGS"; /** @@ -361,7 +359,8 @@ public class InternetDialogController implements AccessPointController.AccessPoi @VisibleForTesting protected Intent getSettingsIntent() { - return new Intent(ACTION_NETWORK_PROVIDER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return new Intent(Settings.ACTION_NETWORK_PROVIDER_SETTINGS).addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK); } @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index fdc596b3030c..eec5d3d915f8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -26,6 +26,7 @@ import com.android.systemui.common.shared.model.ContentDescription.Companion.loa import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel @@ -38,7 +39,9 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon +import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -48,6 +51,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) /** Observes internet state changes providing the [InternetTileModel]. */ @@ -55,6 +59,7 @@ class InternetTileDataInteractor @Inject constructor( private val context: Context, + @Main private val mainCoroutineContext: CoroutineContext, @Application private val scope: CoroutineScope, airplaneModeRepository: AirplaneModeRepository, private val connectivityRepository: ConnectivityRepository, @@ -111,42 +116,48 @@ constructor( notConnectedFlow } else { combine( - it.networkName, - it.signalLevelIcon, - mobileDataContentName, - ) { networkNameModel, signalIcon, dataContentDescription -> - when (signalIcon) { - is SignalIconModel.Cellular -> { - val secondary = - mobileDataContentConcat( - networkNameModel.name, - dataContentDescription - ) + it.networkName, + it.signalLevelIcon, + mobileDataContentName, + ) { networkNameModel, signalIcon, dataContentDescription -> + Triple(networkNameModel, signalIcon, dataContentDescription) + } + .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) -> + when (signalIcon) { + is SignalIconModel.Cellular -> { + val secondary = + mobileDataContentConcat( + networkNameModel.name, + dataContentDescription + ) - val stateLevel = signalIcon.level - val drawable = SignalDrawable(context) - drawable.setLevel(stateLevel) - val loadedIcon = Icon.Loaded(drawable, null) + val drawable = + withContext(mainCoroutineContext) { SignalDrawable(context) } + drawable.setLevel(signalIcon.level) + val loadedIcon = Icon.Loaded(drawable, null) - InternetTileModel.Active( - secondaryTitle = secondary, - icon = loadedIcon, - stateDescription = ContentDescription.Loaded(secondary.toString()), - contentDescription = ContentDescription.Loaded(internetLabel), - ) - } - is SignalIconModel.Satellite -> { - val secondary = - signalIcon.icon.contentDescription.loadContentDescription(context) - InternetTileModel.Active( - secondaryTitle = secondary, - iconId = signalIcon.icon.res, - stateDescription = ContentDescription.Loaded(secondary), - contentDescription = ContentDescription.Loaded(internetLabel), - ) + InternetTileModel.Active( + secondaryTitle = secondary, + icon = loadedIcon, + stateDescription = + ContentDescription.Loaded(secondary.toString()), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } + is SignalIconModel.Satellite -> { + val secondary = + signalIcon.icon.contentDescription.loadContentDescription( + context + ) + InternetTileModel.Active( + secondaryTitle = secondary, + iconId = signalIcon.icon.res, + stateDescription = ContentDescription.Loaded(secondary), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } } } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 864f29a5c5e0..d4e711e38b3c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -229,6 +229,8 @@ public class ImageExporter { return CallbackToFutureAdapter.getFuture( (completer) -> { executor.execute(() -> { + // save images as quickly as possible on the background thread + Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { completer.set(task.execute()); } catch (ImageExportException | InterruptedException e) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt index 7130fa1a1bd3..596046268984 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -3,15 +3,19 @@ package com.android.systemui.screenshot import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator -import android.os.UserHandle import android.view.View import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.view.ViewTreeObserver import android.view.animation.AccelerateDecelerateInterpolator import androidx.constraintlayout.widget.Guideline +import com.android.systemui.Flags.screenshotPrivateProfileBehaviorFix +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R +import com.android.systemui.screenshot.message.ProfileMessageController import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * MessageContainerController controls the display of content in the screenshot message container. @@ -20,7 +24,9 @@ class MessageContainerController @Inject constructor( private val workProfileMessageController: WorkProfileMessageController, + private val profileMessageController: ProfileMessageController, private val screenshotDetectionController: ScreenshotDetectionController, + @Application private val mainScope: CoroutineScope, ) { private lateinit var container: ViewGroup private lateinit var guideline: Guideline @@ -42,43 +48,52 @@ constructor( detectionNoticeView.visibility = View.GONE } - // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on. - fun onScreenshotTaken(userHandle: UserHandle) { - val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle) - if (workProfileData != null) { - workProfileFirstRunView.visibility = View.VISIBLE - detectionNoticeView.visibility = View.GONE - - workProfileMessageController.populateView( - workProfileFirstRunView, - workProfileData, - this::animateOutMessageContainer - ) - animateInMessageContainer() - } - } - fun onScreenshotTaken(screenshot: ScreenshotData) { - val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle) - var notifiedApps: List<CharSequence> = - screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) + if (screenshotPrivateProfileBehaviorFix()) { + mainScope.launch { + val profileData = profileMessageController.onScreenshotTaken(screenshot.userHandle) + var notifiedApps: List<CharSequence> = + screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) + + // If profile first run needs to show, bias towards that, otherwise show screenshot + // detection notification if needed. + if (profileData != null) { + workProfileFirstRunView.visibility = View.VISIBLE + detectionNoticeView.visibility = View.GONE + profileMessageController.bindView(workProfileFirstRunView, profileData) { + animateOutMessageContainer() + } + animateInMessageContainer() + } else if (notifiedApps.isNotEmpty()) { + detectionNoticeView.visibility = View.VISIBLE + workProfileFirstRunView.visibility = View.GONE + screenshotDetectionController.populateView(detectionNoticeView, notifiedApps) + animateInMessageContainer() + } + } + } else { + val workProfileData = + workProfileMessageController.onScreenshotTaken(screenshot.userHandle) + var notifiedApps: List<CharSequence> = + screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) - // If work profile first run needs to show, bias towards that, otherwise show screenshot - // detection notification if needed. - if (workProfileData != null) { - workProfileFirstRunView.visibility = View.VISIBLE - detectionNoticeView.visibility = View.GONE - workProfileMessageController.populateView( - workProfileFirstRunView, - workProfileData, - this::animateOutMessageContainer - ) - animateInMessageContainer() - } else if (notifiedApps.isNotEmpty()) { - detectionNoticeView.visibility = View.VISIBLE - workProfileFirstRunView.visibility = View.GONE - screenshotDetectionController.populateView(detectionNoticeView, notifiedApps) - animateInMessageContainer() + // If work profile first run needs to show, bias towards that, otherwise show screenshot + // detection notification if needed. + if (workProfileData != null) { + workProfileFirstRunView.visibility = View.VISIBLE + detectionNoticeView.visibility = View.GONE + workProfileMessageController.populateView( + workProfileFirstRunView, + workProfileData, + this::animateOutMessageContainer + ) + animateInMessageContainer() + } else if (notifiedApps.isNotEmpty()) { + detectionNoticeView.visibility = View.VISIBLE + workProfileFirstRunView.visibility = View.GONE + screenshotDetectionController.populateView(detectionNoticeView, notifiedApps) + animateInMessageContainer() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt index c801ca5aa8a1..b93aedd9dd52 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -53,8 +53,9 @@ constructor( if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) { var badgedIcon: Drawable? = null var label: CharSequence? = null - val fileManager = fileManagerComponentName() - ?: return WorkProfileFirstRunData(defaultFileAppName(), null) + val fileManager = + fileManagerComponentName() + ?: return WorkProfileFirstRunData(defaultFileAppName(), null) try { val info = packageManager.getActivityInfo(fileManager, ComponentInfoFlags.of(0L)) val icon = packageManager.getActivityIcon(fileManager) @@ -103,9 +104,7 @@ constructor( context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) private fun fileManagerComponentName() = - ComponentName.unflattenFromString( - context.getString(R.string.config_sceenshotWorkProfileFilesApp) - ) + ComponentName.unflattenFromString(context.getString(R.string.config_screenshotFilesApp)) private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 9b8d0472b583..8235325fffad 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -38,6 +38,7 @@ import com.android.systemui.screenshot.TakeScreenshotExecutorImpl; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.message.MessageModule; import com.android.systemui.screenshot.policy.ScreenshotPolicyModule; import com.android.systemui.screenshot.proxy.SystemUiProxyModule; import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; @@ -51,7 +52,7 @@ import dagger.multibindings.IntoMap; /** * Defines injectable resources for Screenshots */ -@Module(includes = {ScreenshotPolicyModule.class, SystemUiProxyModule.class}) +@Module(includes = {ScreenshotPolicyModule.class, SystemUiProxyModule.class, MessageModule.class}) public abstract class ScreenshotModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/message/MessageModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/message/MessageModule.kt new file mode 100644 index 000000000000..9d0f87f30efc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/message/MessageModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module + +@Module +interface MessageModule { + @Binds + @SysUISingleton + fun bindProfileFirstRunResources( + impl: ProfileFirstRunFileResourcesImpl + ): ProfileFirstRunFileResources + + @Binds + @SysUISingleton + fun bindPackageLabelIconProvider(impl: PackageLabelIconProviderImpl): PackageLabelIconProvider + + @Binds + @SysUISingleton + fun bindProfileFirstRunSettings(impl: ProfileFirstRunSettingsImpl): ProfileFirstRunSettings +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/message/PackageLabelIconProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/message/PackageLabelIconProvider.kt new file mode 100644 index 000000000000..fd073aecb50e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/message/PackageLabelIconProvider.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.UserHandle +import javax.inject.Inject + +data class LabeledIcon( + val label: CharSequence, + val badgedIcon: Drawable?, +) + +/** An object that can fetch a label and icon for a given component. */ +interface PackageLabelIconProvider { + /** + * @return the label and icon for the given component. + * @throws PackageManager.NameNotFoundException if the component was not found. + */ + suspend fun getPackageLabelIcon( + componentName: ComponentName, + userHandle: UserHandle + ): LabeledIcon +} + +class PackageLabelIconProviderImpl @Inject constructor(private val packageManager: PackageManager) : + PackageLabelIconProvider { + + override suspend fun getPackageLabelIcon( + componentName: ComponentName, + userHandle: UserHandle + ): LabeledIcon { + val info = + packageManager.getActivityInfo(componentName, PackageManager.ComponentInfoFlags.of(0L)) + val icon = packageManager.getActivityIcon(componentName) + val badgedIcon = packageManager.getUserBadgedIcon(icon, userHandle) + val label = info.loadLabel(packageManager) + return LabeledIcon(label, badgedIcon) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunFileResources.kt b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunFileResources.kt new file mode 100644 index 000000000000..e58c76de5752 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunFileResources.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import android.content.ComponentName +import android.content.Context +import com.android.systemui.res.R +import javax.inject.Inject + +/** Provides various configuration resource values for the profile first run flow. */ +interface ProfileFirstRunFileResources { + /** @return the ComponentName for the Files app, if available. */ + fun fileManagerComponentName(): ComponentName? + + /** @return a default LabeledIcon describing the files app */ + fun defaultFileApp(): LabeledIcon +} + +class ProfileFirstRunFileResourcesImpl @Inject constructor(private val context: Context) : + ProfileFirstRunFileResources { + override fun fileManagerComponentName() = + ComponentName.unflattenFromString(context.getString(R.string.config_screenshotFilesApp)) + + override fun defaultFileApp() = + LabeledIcon( + context.getString(R.string.screenshot_default_files_app_name), + badgedIcon = null + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunSettings.kt b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunSettings.kt new file mode 100644 index 000000000000..5ec14a3b5834 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileFirstRunSettings.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import android.content.Context +import javax.inject.Inject + +/** + * An interfaces for the settings related to the profile first run experience, storing a bit + * indicating whether the user has already dismissed the message for the given profile. + */ +interface ProfileFirstRunSettings { + /** @return true if the user has already dismissed the first run message for this profile. */ + fun messageAlreadyDismissed(profileType: ProfileMessageController.FirstRunProfile): Boolean + /** + * Update storage to reflect the fact that the user has dismissed a first run message for the + * given profile. + */ + fun onMessageDismissed(profileType: ProfileMessageController.FirstRunProfile) +} + +class ProfileFirstRunSettingsImpl @Inject constructor(private val context: Context) : + ProfileFirstRunSettings { + + override fun messageAlreadyDismissed( + profileType: ProfileMessageController.FirstRunProfile + ): Boolean { + val preferenceKey = preferenceKey(profileType) + return sharedPreference().getBoolean(preferenceKey, false) + } + + override fun onMessageDismissed(profileType: ProfileMessageController.FirstRunProfile) { + val preferenceKey = preferenceKey(profileType) + val editor = sharedPreference().edit() + editor.putBoolean(preferenceKey, true) + editor.apply() + } + + private fun preferenceKey(profileType: ProfileMessageController.FirstRunProfile): String { + return when (profileType) { + ProfileMessageController.FirstRunProfile.WORK -> WORK_PREFERENCE_KEY + ProfileMessageController.FirstRunProfile.PRIVATE -> PRIVATE_PREFERENCE_KEY + } + } + + private fun sharedPreference() = + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + + companion object { + const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot" + const val WORK_PREFERENCE_KEY = "work_profile_first_run" + const val PRIVATE_PREFERENCE_KEY = "private_profile_first_run" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileMessageController.kt new file mode 100644 index 000000000000..9212a6a875da --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/message/ProfileMessageController.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import android.os.UserHandle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.data.model.ProfileType +import com.android.systemui.screenshot.data.repository.ProfileTypeRepository +import javax.inject.Inject + +/** + * Handles work profile and private profile first run, determining whether a first run UI should be + * shown and populating that UI if needed. + */ +class ProfileMessageController +@Inject +constructor( + private val packageLabelIconProvider: PackageLabelIconProvider, + private val fileResources: ProfileFirstRunFileResources, + private val firstRunSettings: ProfileFirstRunSettings, + private val profileTypes: ProfileTypeRepository, +) { + + /** + * @return a populated ProfileFirstRunData object if a profile first run message should be + * shown, otherwise null. + */ + suspend fun onScreenshotTaken(userHandle: UserHandle?): ProfileFirstRunData? { + if (userHandle == null) return null + val profileType = + when (profileTypes.getProfileType(userHandle.identifier)) { + ProfileType.WORK -> FirstRunProfile.WORK + ProfileType.PRIVATE -> FirstRunProfile.PRIVATE + else -> return null + } + + if (firstRunSettings.messageAlreadyDismissed(profileType)) { + return null + } + + val fileApp = + runCatching { + fileResources.fileManagerComponentName()?.let { fileManager -> + packageLabelIconProvider.getPackageLabelIcon(fileManager, userHandle) + } + } + .getOrNull() ?: fileResources.defaultFileApp() + + return ProfileFirstRunData(fileApp, profileType) + } + + /** + * Use the provided ProfileFirstRunData to populate the profile first run UI in the given view. + */ + fun bindView(view: ViewGroup, data: ProfileFirstRunData, animateOut: () -> Unit) { + if (data.labeledIcon.badgedIcon != null) { + // Replace the default icon if one is provided. + val imageView = view.requireViewById<ImageView>(R.id.screenshot_message_icon) + imageView.setImageDrawable(data.labeledIcon.badgedIcon) + } + val messageContent = view.requireViewById<TextView>(R.id.screenshot_message_content) + messageContent.text = + view.context.getString(messageTemplate(data.profileType), data.labeledIcon.label) + view.requireViewById<View>(R.id.message_dismiss_button).setOnClickListener { + animateOut() + firstRunSettings.onMessageDismissed(data.profileType) + } + } + + private fun messageTemplate(profile: FirstRunProfile): Int { + return when (profile) { + FirstRunProfile.WORK -> R.string.screenshot_work_profile_notification + FirstRunProfile.PRIVATE -> R.string.screenshot_private_profile_notification + } + } + + data class ProfileFirstRunData( + val labeledIcon: LabeledIcon, + val profileType: FirstRunProfile, + ) + + enum class FirstRunProfile { + WORK, + PRIVATE + } + + companion object { + const val TAG = "PrivateProfileMessageCtrl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 67211b11a32f..403bece4a7c6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -244,6 +244,7 @@ import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.StateFlow; import java.io.PrintWriter; import java.util.ArrayList; @@ -4055,6 +4056,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @Override + public StateFlow<Float> getUdfpsTransitionToFullShadeProgress() { + return mShadeRepository.getUdfpsTransitionToFullShadeProgress(); + } + + @Override public Flow<Float> getLegacyPanelExpansion() { return mShadeRepository.getLegacyShadeExpansion(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 0c41efd513ad..9322d31fa2ce 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -25,6 +25,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.phone.HeadsUpAppearanceController import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf /** Empty implementation of ShadeViewController for variants with no shade. */ @@ -92,6 +93,7 @@ open class ShadeViewControllerEmptyImpl @Inject constructor() : override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() @Deprecated("Use SceneInteractor.currentScene instead.") override val legacyPanelExpansion = flowOf(0f) + override val udfpsTransitionToFullShadeProgress = MutableStateFlow(0f) } class ShadeHeadsUpTrackerEmptyImpl : ShadeHeadsUpTracker { diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index 5c79e1ee84f3..b934d63c17c5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -28,7 +28,7 @@ interface ShadeRepository { * Amount qs has expanded, [0-1]. 0 means fully collapsed, 1 means fully expanded. Quick * Settings can be expanded without the full shade expansion. */ - val qsExpansion: StateFlow<Float> + @Deprecated("Use ShadeInteractor.qsExpansion instead") val qsExpansion: StateFlow<Float> /** Amount shade has expanded with regard to the UDFPS location */ val udfpsTransitionToFullShadeProgress: StateFlow<Float> diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt index 2611092553ed..987c01699f2f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt @@ -15,9 +15,14 @@ */ package com.android.systemui.shade.domain.interactor +import kotlinx.coroutines.flow.StateFlow + /** Allows the lockscreen to control the shade. */ interface ShadeLockscreenInteractor { + /** Amount shade has expanded with regard to the UDFPS location */ + val udfpsTransitionToFullShadeProgress: StateFlow<Float> + /** * Expand shade so that notifications are visible. Non-split shade: just expanding shade or * collapsing QS when they're expanded. Split shade: only expanding shade, notifications are diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt index 318da557ed2a..6a8b9eec140c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt @@ -20,6 +20,7 @@ import com.android.keyguard.LockIconViewController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.data.repository.ShadeRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -32,7 +33,12 @@ constructor( private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, private val lockIconViewController: LockIconViewController, + shadeRepository: ShadeRepository, ) : ShadeLockscreenInteractor { + + override val udfpsTransitionToFullShadeProgress = + shadeRepository.udfpsTransitionToFullShadeProgress + override fun expandToNotifications() { changeToShadeScene() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index d465973a5d12..4d7dacd336ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -29,6 +29,7 @@ import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.systemui.Dumpable; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.shared.model.MediaData; @@ -47,6 +48,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.Executor; /** * Handles tasks and state related to media notifications. For example, there is a 'current' media @@ -75,6 +77,8 @@ public class NotificationMediaManager implements Dumpable { private final Context mContext; private final ArrayList<MediaListener> mMediaListeners; + private final Executor mBackgroundExecutor; + protected NotificationPresenter mPresenter; private MediaController mMediaController; private String mMediaNotificationKey; @@ -115,13 +119,15 @@ public class NotificationMediaManager implements Dumpable { NotifPipeline notifPipeline, NotifCollection notifCollection, MediaDataManager mediaDataManager, - DumpManager dumpManager) { + DumpManager dumpManager, + @Background Executor backgroundExecutor) { mContext = context; mMediaListeners = new ArrayList<>(); mVisibilityProvider = visibilityProvider; mMediaDataManager = mediaDataManager; mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; + mBackgroundExecutor = backgroundExecutor; setupNotifPipeline(); @@ -381,9 +387,11 @@ public class NotificationMediaManager implements Dumpable { Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: " + mMediaController.getPackageName()); } - mMediaController.unregisterCallback(mMediaListener); + mBackgroundExecutor.execute(() -> { + mMediaController.unregisterCallback(mMediaListener); + mMediaController = null; + }); } - mMediaController = null; } public interface MediaListener { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 8a53e0ce94c4..c17da4b3b4e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -31,6 +31,7 @@ import com.android.systemui.animation.AnimationFeatureFlags; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -68,6 +69,8 @@ import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; +import java.util.concurrent.Executor; + import javax.inject.Provider; /** @@ -94,14 +97,16 @@ public interface CentralSurfacesDependenciesModule { NotifPipeline notifPipeline, NotifCollection notifCollection, MediaDataManager mediaDataManager, - DumpManager dumpManager) { + DumpManager dumpManager, + @Background Executor backgroundExecutor) { return new NotificationMediaManager( context, visibilityProvider, notifPipeline, notifCollection, mediaDataManager, - dumpManager); + dumpManager, + backgroundExecutor); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 240ae0c13ac7..9c1d0735a65b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -1243,8 +1243,9 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { if (cmp != 0) return cmp; cmp = -1 * Long.compare( - o1.getRepresentativeEntry().getSbn().getNotification().when, - o2.getRepresentativeEntry().getSbn().getNotification().when); + o1.getRepresentativeEntry().getSbn().getNotification().getWhen(), + o2.getRepresentativeEntry().getSbn().getNotification().getWhen()); + return cmp; }; @@ -1256,8 +1257,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { if (cmp != 0) return cmp; cmp = -1 * Long.compare( - o1.getRepresentativeEntry().getSbn().getNotification().when, - o2.getRepresentativeEntry().getSbn().getNotification().when); + o1.getRepresentativeEntry().getSbn().getNotification().getWhen(), + o2.getRepresentativeEntry().getSbn().getNotification().getWhen()); return cmp; }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt index 5ce1db2b6acd..f253100b3661 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupWhenCoordinator.kt @@ -89,7 +89,7 @@ constructor( var futureTime = Long.MAX_VALUE groupEntry.children .asSequence() - .mapNotNull { child -> child.sbn.notification.`when`.takeIf { it > 0 } } + .mapNotNull { child -> child.sbn.notification.getWhen().takeIf { it > 0 } } .forEach { time -> val isInThePast = currentTimeMillis - time > 0 if (isInThePast) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index fb67f7cc0b0f..f98f77ec4ac2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -296,7 +296,9 @@ class HeadsUpCoordinator @Inject constructor( locationLookupByKey: (String) -> GroupLocation, ): NotificationEntry? = postedEntries.asSequence() .filter { posted -> !posted.entry.sbn.notification.isGroupSummary } - .sortedBy { posted -> -posted.entry.sbn.notification.`when` } + .sortedBy { posted -> + -posted.entry.sbn.notification.getWhen() + } .firstOrNull() ?.let { posted -> posted.entry.takeIf { entry -> @@ -317,7 +319,7 @@ class HeadsUpCoordinator @Inject constructor( .filter { locationLookupByKey(it.key) != GroupLocation.Detached } .sortedWith(compareBy( { !mPostedEntries.contains(it.key) }, - { -it.sbn.notification.`when` }, + { -it.sbn.notification.getWhen() }, )) .firstOrNull() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 9619acaed441..bfc5932c1db0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -153,12 +153,12 @@ class PeekOldWhenSuppressor(private val systemClock: SystemClock) : uiEventId = HUN_SUPPRESSED_OLD_WHEN ) { private fun whenAge(entry: NotificationEntry) = - systemClock.currentTimeMillis() - entry.sbn.notification.`when` + systemClock.currentTimeMillis() - entry.sbn.notification.getWhen() override fun shouldSuppress(entry: NotificationEntry): Boolean = when { // Ignore a "when" of 0, as it is unlikely to be a meaningful timestamp. - entry.sbn.notification.`when` <= 0L -> false + entry.sbn.notification.getWhen() <= 0L -> false // Assume all HUNs with FSIs, foreground services, or user-initiated jobs are // time-sensitive, regardless of their "when". @@ -278,7 +278,7 @@ class AvalancheSuppressor( private fun calculateState(entry: NotificationEntry): State { if ( entry.ranking.isConversation && - entry.sbn.notification.`when` > avalancheProvider.startTime + entry.sbn.notification.getWhen() > avalancheProvider.startTime ) { return State.ALLOW_CONVERSATION_AFTER_AVALANCHE } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index d5911141fc0f..9c6a42384a53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -623,7 +623,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter return false; } - final long when = notification.when; + final long when = notification.getWhen(); final long now = mSystemClock.currentTimeMillis(); final long age = now - when; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index bdeaabf53b07..5e3df7b5e60f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -2769,7 +2769,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (!mIsSummaryWithChildren && wasSummary) { // Reset the 'when' once the row stops being a summary - mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().when); + mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().getWhen()); } getShowingLayout().updateBackgroundColor(false /* animate */); mPrivateLayout.updateExpandButtons(isExpandable()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 37bbbd0466d4..ac4bd09b1687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -41,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -106,6 +107,7 @@ constructor( private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, + private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, @@ -464,6 +466,7 @@ constructor( val alphaTransitions = merge( alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState), + aodToGoneTransitionViewModel.notificationAlpha(viewState), aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index b2b2ceaa9017..7630d43c3c7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -855,6 +855,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mWakefulnessLifecycle.addObserver(mWakefulnessObserver); mUiModeManager = mContext.getSystemService(UiModeManager.class); mBubblesOptional.ifPresent(this::initBubbles); + mKeyguardBypassController.listenForQsExpandedChange(); mStatusBarSignalPolicy.init(); mKeyguardIndicationController.init(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index a8941bb182ae..97791acfb43a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -21,7 +21,6 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.hardware.biometrics.BiometricSourceType import android.provider.Settings -import androidx.annotation.VisibleForTesting import com.android.app.tracing.ListenersTracing.forEachTraced import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton @@ -32,7 +31,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm @@ -41,23 +40,24 @@ import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POST import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.tuner.TunerService +import dagger.Lazy +import java.io.PrintWriter +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.io.PrintWriter -import javax.inject.Inject @SysUISingleton class KeyguardBypassController @Inject constructor( @Main resources: Resources, packageManager: PackageManager, - @Application applicationScope: CoroutineScope, + @Application private val applicationScope: CoroutineScope, tunerService: TunerService, private val statusBarStateController: StatusBarStateController, lockscreenUserManager: NotificationLockscreenUserManager, private val keyguardStateController: KeyguardStateController, - private val shadeRepository: ShadeRepository, + private val shadeInteractorLazy: Lazy<ShadeInteractor>, devicePostureController: DevicePostureController, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, dumpManager: DumpManager @@ -144,7 +144,6 @@ class KeyguardBypassController @Inject constructor( } } }) - listenForQsExpandedChange(applicationScope) val dismissByDefault = if (resources.getBoolean( com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0 tunerService.addTunable({ key, _ -> @@ -159,10 +158,9 @@ class KeyguardBypassController @Inject constructor( } } - @VisibleForTesting - fun listenForQsExpandedChange(scope: CoroutineScope) = - scope.launch { - shadeRepository.qsExpansion.map { it > 0f }.distinctUntilChanged() + fun listenForQsExpandedChange() = + applicationScope.launch { + shadeInteractorLazy.get().qsExpansion.map { it > 0f }.distinctUntilChanged() .collect { isQsExpanded -> val changed = qsExpanded != isQsExpanded qsExpanded = isQsExpanded diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt index 68d54e73774e..6b685118a462 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt @@ -131,7 +131,12 @@ constructor( val runnable = Runnable { assistManagerLazy.get().hideAssist() - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.flags = + if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) { + Intent.FLAG_ACTIVITY_NEW_TASK + } else { + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } intent.addFlags(flags) val result = intArrayOf(ActivityManager.START_CANCELED) activityTransitionAnimator.startIntentWithAnimation( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index a20468f9e3ba..ec88b6c56477 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -98,7 +98,7 @@ class OngoingCallController @Inject constructor( (entry.sbn.key == callNotificationInfo?.key)) { val newOngoingCallInfo = CallNotificationInfo( entry.sbn.key, - entry.sbn.notification.`when`, + entry.sbn.notification.getWhen(), entry.sbn.notification.contentIntent, entry.sbn.uid, entry.sbn.notification.extras.getInt( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt index f2255f3248e0..332c1210f8cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -126,10 +126,9 @@ constructor( ) { shouldShow, connectionState -> if (shouldShow) { when (connectionState) { + SatelliteConnectionState.On, SatelliteConnectionState.Connected -> context.getString(R.string.satellite_connected_carrier_text) - SatelliteConnectionState.On -> - context.getString(R.string.satellite_not_connected_carrier_text) SatelliteConnectionState.Off, SatelliteConnectionState.Unknown -> null } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt index 1e6556645afb..b3ea9dcb7cd1 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt @@ -6,8 +6,8 @@ import android.os.VibrationEffect import android.os.Vibrator import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.updates.FoldProvider -import com.android.systemui.unfold.updates.FoldProvider.FoldCallback import java.util.concurrent.Executor import javax.inject.Inject @@ -18,6 +18,7 @@ class UnfoldHapticsPlayer constructor( unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, foldProvider: FoldProvider, + transitionConfig: UnfoldTransitionConfig, @Main private val mainExecutor: Executor, private val vibrator: Vibrator? ) : TransitionProgressListener { @@ -27,22 +28,17 @@ constructor( VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK) init { - if (vibrator != null) { + if (vibrator != null && transitionConfig.isHapticsEnabled) { // We don't need to remove the callback because we should listen to it // the whole time when SystemUI process is alive unfoldTransitionProgressProvider.addCallback(this) - } - foldProvider.registerCallback( - object : FoldCallback { - override fun onFoldUpdated(isFolded: Boolean) { - if (isFolded) { - isFirstAnimationAfterUnfold = true - } + foldProvider.registerCallback({ isFolded -> + if (isFolded) { + isFirstAnimationAfterUnfold = true } - }, - mainExecutor - ) + }, mainExecutor) + } } private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN diff --git a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java index 0dcbe9b2bfc4..229bdce62cf6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java @@ -26,8 +26,9 @@ import android.util.IndentingPrintWriter; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; -import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.annotations.WeaklyReferencedCallback; @@ -118,7 +119,7 @@ public class ObservableServiceConnection<T> implements ServiceConnection { private final Intent mServiceIntent; private final UserTracker mUserTracker; private final int mFlags; - private final Executor mExecutor; + private final Executor mBgExecutor; private final ServiceTransformer<T> mTransformer; private final ArrayList<WeakReference<Callback<T>>> mCallbacks; private Optional<Integer> mLastDisconnectReason; @@ -130,30 +131,34 @@ public class ObservableServiceConnection<T> implements ServiceConnection { * Default constructor for {@link ObservableServiceConnection}. * @param context The context from which the service will be bound with. * @param serviceIntent The intent to bind service with. - * @param executor The executor for connection callbacks to be delivered on + * @param bgExecutor The executor for connection callbacks to be delivered on * @param transformer A {@link ServiceTransformer} for transforming the resulting service * into a desired type. */ @Inject public ObservableServiceConnection(Context context, Intent serviceIntent, UserTracker userTracker, - @Main Executor executor, + @Background Executor bgExecutor, ServiceTransformer<T> transformer) { mContext = context; mServiceIntent = serviceIntent; mUserTracker = userTracker; mFlags = Context.BIND_AUTO_CREATE; - mExecutor = executor; + mBgExecutor = bgExecutor; mTransformer = transformer; mCallbacks = new ArrayList<>(); mLastDisconnectReason = Optional.empty(); } /** - * Initiate binding to the service. - * @return {@code true} if initiating binding succeed, {@code false} otherwise. + * Initiate binding to the service in the background. */ - public boolean bind() { + public void bind() { + mBgExecutor.execute(this::bindInternal); + } + + @WorkerThread + private void bindInternal() { boolean bindResult = false; try { bindResult = mContext.bindServiceAsUser(mServiceIntent, this, mFlags, @@ -166,18 +171,17 @@ public class ObservableServiceConnection<T> implements ServiceConnection { if (DEBUG) { Log.d(TAG, "bind. bound:" + bindResult); } - return bindResult; } /** * Disconnect from the service if bound. */ public void unbind() { - onDisconnected(DISCONNECT_REASON_UNBIND); + mBgExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_UNBIND)); } /** - * Adds a callback for receiving connection updates. + * Adds a callback for receiving connection updates. The callback is executed in the background. * @param callback The {@link Callback} to receive future updates. */ public void addCallback(Callback<T> callback) { @@ -185,7 +189,7 @@ public class ObservableServiceConnection<T> implements ServiceConnection { Log.d(TAG, "addCallback:" + callback); } - mExecutor.execute(() -> { + mBgExecutor.execute(() -> { final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { @@ -210,14 +214,15 @@ public class ObservableServiceConnection<T> implements ServiceConnection { * Removes previously added callback from receiving future connection updates. * @param callback The {@link Callback} to be removed. */ - public void removeCallback(Callback callback) { + public void removeCallback(Callback<T> callback) { if (DEBUG) { Log.d(TAG, "removeCallback:" + callback); } - mExecutor.execute(() -> mCallbacks.removeIf(el-> el.get() == callback)); + mBgExecutor.execute(() -> mCallbacks.removeIf(el-> el.get() == callback)); } + @WorkerThread private void onDisconnected(@DisconnectReason int reason) { if (DEBUG) { Log.d(TAG, "onDisconnected:" + reason); @@ -240,7 +245,7 @@ public class ObservableServiceConnection<T> implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { - mExecutor.execute(() -> { + mBgExecutor.execute(() -> { if (DEBUG) { Log.d(TAG, "onServiceConnected"); } @@ -268,7 +273,7 @@ public class ObservableServiceConnection<T> implements ServiceConnection { final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { - final Callback cb = iterator.next().get(); + final Callback<T> cb = iterator.next().get(); if (cb != null) { applicator.accept(cb); } else { @@ -279,16 +284,16 @@ public class ObservableServiceConnection<T> implements ServiceConnection { @Override public void onServiceDisconnected(ComponentName name) { - mExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_DISCONNECTED)); + mBgExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_DISCONNECTED)); } @Override public void onBindingDied(ComponentName name) { - mExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_BINDING_DIED)); + mBgExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_BINDING_DIED)); } @Override public void onNullBinding(ComponentName name) { - mExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_NULL_BINDING)); + mBgExecutor.execute(() -> onDisconnected(DISCONNECT_REASON_NULL_BINDING)); } } diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java index 5979f3e60cb9..64f8246118c8 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -48,7 +48,7 @@ public class PersistentConnectionManager<T> implements Dumpable { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final SystemClock mSystemClock; - private final DelayableExecutor mMainExecutor; + private final DelayableExecutor mBgExecutor; private final int mBaseReconnectDelayMs; private final int mMaxReconnectAttempts; private final int mMinConnectionDuration; @@ -71,8 +71,8 @@ public class PersistentConnectionManager<T> implements Dumpable { private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt(); - private final ObservableServiceConnection.Callback mConnectionCallback = - new ObservableServiceConnection.Callback() { + private final ObservableServiceConnection.Callback<T> mConnectionCallback = + new ObservableServiceConnection.Callback<>() { private long mStartTime; @Override @@ -95,12 +95,10 @@ public class PersistentConnectionManager<T> implements Dumpable { } }; - // TODO: b/326449074 - Ensure the DelayableExecutor is on the correct thread, and update the - // qualifier (to @Main) or name (to bgExecutor) to be consistent with that. @Inject public PersistentConnectionManager( SystemClock clock, - @Background DelayableExecutor mainExecutor, + @Background DelayableExecutor bgExecutor, DumpManager dumpManager, @Named(DUMPSYS_NAME) String dumpsysName, @Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection, @@ -109,7 +107,7 @@ public class PersistentConnectionManager<T> implements Dumpable { @Named(MIN_CONNECTION_DURATION_MS) int minConnectionDurationMs, @Named(OBSERVER) Observer observer) { mSystemClock = clock; - mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; mConnection = serviceConnection; mObserver = observer; mDumpManager = dumpManager; @@ -195,7 +193,7 @@ public class PersistentConnectionManager<T> implements Dumpable { "scheduling connection attempt in " + reconnectDelayMs + "milliseconds"); } - mCurrentReconnectCancelable = mMainExecutor.executeDelayed(mConnectRunnable, + mCurrentReconnectCancelable = mBgExecutor.executeDelayed(mConnectRunnable, reconnectDelayMs); mReconnectAttempts++; diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt index f11d5d18ac84..0968bde4d284 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt @@ -21,26 +21,29 @@ import android.content.Context import android.content.Intent import android.provider.Settings import android.text.TextUtils -import android.util.Log +import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor +import com.android.systemui.volume.ui.navigation.VolumeNavigator import javax.inject.Inject -private const val TAG = "VolumePanelDialogReceiver" -private const val LAUNCH_ACTION = "com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" -private const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" - -/** - * BroadcastReceiver for handling volume panel dialog intent - */ -class VolumePanelDialogReceiver @Inject constructor( - private val volumePanelFactory: VolumePanelFactory +/** [BroadcastReceiver] for handling volume panel dialog intent */ +class VolumePanelDialogReceiver +@Inject +constructor( + private val volumeNavigator: VolumeNavigator, + private val volumePanelNavigationInteractor: VolumePanelNavigationInteractor, ) : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { - Log.d(TAG, "onReceive intent" + intent.action) - if (TextUtils.equals(LAUNCH_ACTION, intent.action) || - TextUtils.equals(Settings.Panel.ACTION_VOLUME, intent.action)) { - volumePanelFactory.create(true, null) - } else if (TextUtils.equals(DISMISS_ACTION, intent.action)) { - volumePanelFactory.dismiss() + if ( + TextUtils.equals(LAUNCH_ACTION, intent.action) || + TextUtils.equals(Settings.Panel.ACTION_VOLUME, intent.action) + ) { + volumeNavigator.openVolumePanel(volumePanelNavigationInteractor.getVolumePanelRoute()) } } + + companion object { + const val LAUNCH_ACTION = "com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" + const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index fb92a0fcd4c5..dc1e8cf2ea01 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -16,6 +16,7 @@ package com.android.systemui.volume.dagger; +import android.content.BroadcastReceiver; import android.content.Context; import android.media.AudioManager; import android.os.Looper; @@ -37,6 +38,7 @@ import com.android.systemui.volume.CsdWarningDialog; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogImpl; +import com.android.systemui.volume.VolumePanelDialogReceiver; import com.android.systemui.volume.VolumeUI; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; import com.android.systemui.volume.panel.dagger.VolumePanelComponent; @@ -65,6 +67,15 @@ import dagger.multibindings.IntoSet; } ) public interface VolumeModule { + + /** + * Binds [VolumePanelDialogReceiver] + */ + @Binds + @IntoMap + @ClassKey(VolumePanelDialogReceiver.class) + BroadcastReceiver bindVolumePanelDialogReceiver(VolumePanelDialogReceiver receiver); + /** Starts VolumeUI. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index ee642a64242d..038633810fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.AudioManager +import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream @@ -144,6 +145,7 @@ constructor( if (isMutedOrNoVolume) { when (audioStream.value) { AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off + AudioManager.STREAM_BLUETOOTH_SCO -> R.drawable.ic_volume_off AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off AudioManager.STREAM_RING -> if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) { @@ -158,12 +160,18 @@ constructor( R.drawable.ic_volume_off } AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off - else -> null + else -> { + Log.wtf(TAG, "No icon for the stream: $audioStream") + R.drawable.ic_volume_off + } } } else { iconsByStream[audioStream] + ?: run { + Log.wtf(TAG, "No icon for the stream: $audioStream") + R.drawable.ic_music_note + } } - ?: error("No icon for the stream: $audioStream") return Icon.Resource(iconRes, null) } @@ -196,4 +204,8 @@ constructor( * when using [AudioStream] directly because it expects another type. */ class FactoryAudioStreamWrapper(val audioStream: AudioStream) + + private companion object { + const val TAG = "AudioStreamSliderViewModel" + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt index eff408a5cbdb..a30de1b89695 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -17,11 +17,14 @@ package com.android.systemui.volume.panel.ui.viewmodel import android.content.Context +import android.content.IntentFilter import android.content.res.Resources +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged +import com.android.systemui.volume.VolumePanelDialogReceiver import com.android.systemui.volume.panel.dagger.VolumePanelComponent import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.domain.VolumePanelStartable @@ -37,6 +40,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -49,6 +54,7 @@ class VolumePanelViewModel( coroutineScope: CoroutineScope, daggerComponentFactory: VolumePanelComponentFactory, configurationController: ConfigurationController, + broadcastDispatcher: BroadcastDispatcher, ) { private val volumePanelComponent: VolumePanelComponent = @@ -113,6 +119,10 @@ class VolumePanelViewModel( init { volumePanelComponent.volumePanelStartables().onEach(VolumePanelStartable::start) + broadcastDispatcher + .broadcastFlow(IntentFilter(VolumePanelDialogReceiver.DISMISS_ACTION)) + .onEach { dismissPanel() } + .launchIn(scope) } fun dismissPanel() { @@ -125,6 +135,7 @@ class VolumePanelViewModel( @Application private val context: Context, private val daggerComponentFactory: VolumePanelComponentFactory, private val configurationController: ConfigurationController, + private val broadcastDispatcher: BroadcastDispatcher, ) { fun create(coroutineScope: CoroutineScope): VolumePanelViewModel { @@ -133,6 +144,7 @@ class VolumePanelViewModel( coroutineScope, daggerComponentFactory, configurationController, + broadcastDispatcher ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt index 4684b805ceac..79e312fec5f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -27,9 +27,11 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter.OnDismissAction @@ -229,6 +231,11 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { givenOnOccludingApp(true) givenFingerprintAllowed(true) keyguardRepository.setIsDozing(true) + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OCCLUDED, + to = KeyguardState.DOZING, + testScope + ) runCurrent() // ERROR message @@ -254,7 +261,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { assertThat(message).isNull() } - private fun givenOnOccludingApp(isOnOccludingApp: Boolean) { + private suspend fun givenOnOccludingApp(isOnOccludingApp: Boolean) { powerRepository.setInteractive(true) keyguardRepository.setIsDozing(false) keyguardRepository.setKeyguardOccluded(isOnOccludingApp) @@ -262,6 +269,20 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { keyguardRepository.setDreaming(false) bouncerRepository.setPrimaryShow(!isOnOccludingApp) bouncerRepository.setAlternateVisible(!isOnOccludingApp) + + if (isOnOccludingApp) { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + testScope + ) + } else { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + testScope + ) + } } private fun givenFingerprintAllowed(allowed: Boolean) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 96b759682cfe..35659c12fad5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dock.DockManagerFake +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig @@ -83,6 +84,7 @@ import org.mockito.MockitoAnnotations ) @SmallTest @RunWith(Parameterized::class) +@DisableSceneContainer class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt new file mode 100644 index 000000000000..ef3183a8891f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.os.UserHandle +import androidx.test.filters.FlakyTest +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dock.DockManagerFake +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.Parameterized.Parameters +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.same +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@FlakyTest( + bugId = 292574995, + detail = "on certain architectures all permutations with startActivity=true is causing failures" +) +@SmallTest +@RunWith(Parameterized::class) +@EnableSceneContainer +class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { + + companion object { + private val INTENT = Intent("some.intent.action") + private val DRAWABLE = + mock<Icon> { + whenever(this.contentDescription) + .thenReturn( + ContentDescription.Resource( + res = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + ) + } + private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 + + @Parameters( + name = + "needStrongAuthAfterBoot={0}, canShowWhileLocked={1}," + + " keyguardIsUnlocked={2}, needsToUnlockFirst={3}, startActivity={4}" + ) + @JvmStatic + fun data() = + listOf( + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ false, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ false, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ false, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ true, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ true, + /* startActivity= */ false, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ false, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ false, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ false, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ false, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ false, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ true, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ false, + /* needsToUnlockFirst= */ true, + /* startActivity= */ true, + ), + arrayOf( + /* needStrongAuthAfterBoot= */ true, + /* canShowWhileLocked= */ true, + /* keyguardIsUnlocked= */ true, + /* needsToUnlockFirst= */ true, + /* startActivity= */ true, + ), + ) + + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var animationController: ActivityTransitionAnimator.Controller + @Mock private lateinit var expandable: Expandable + @Mock private lateinit var launchAnimator: DialogTransitionAnimator + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + + private lateinit var underTest: KeyguardQuickAffordanceInteractor + private lateinit var testScope: TestScope + + @JvmField @Parameter(0) var needStrongAuthAfterBoot: Boolean = false + @JvmField @Parameter(1) var canShowWhileLocked: Boolean = false + @JvmField @Parameter(2) var keyguardIsUnlocked: Boolean = false + @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false + @JvmField @Parameter(4) var startActivity: Boolean = false + private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig + private lateinit var dockManager: DockManagerFake + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var userTracker: UserTracker + + private val kosmos = testKosmos() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(expandable.activityTransitionController()).thenReturn(animationController) + + userTracker = FakeUserTracker() + homeControls = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + dockManager = DockManagerFake() + biometricSettingsRepository = FakeBiometricSettingsRepository() + val quickAccessWallet = + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) + val qrCodeScanner = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val localUserSelectionManager = + KeyguardQuickAffordanceLocalUserSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, + ) + val remoteUserSelectionManager = + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = scope, + userTracker = userTracker, + clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), + userHandle = UserHandle.SYSTEM, + ) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + appContext = context, + scope = scope, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = localUserSelectionManager, + ), + configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), + dumpManager = mock(), + userHandle = UserHandle.SYSTEM, + ) + val featureFlags = FakeFeatureFlags() + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + underTest = + KeyguardQuickAffordanceInteractor( + keyguardInteractor = + KeyguardInteractorFactory.create( + featureFlags = featureFlags, + ) + .keyguardInteractor, + shadeInteractor = kosmos.shadeInteractor, + lockPatternUtils = lockPatternUtils, + keyguardStateController = keyguardStateController, + userTracker = userTracker, + activityStarter = activityStarter, + featureFlags = featureFlags, + repository = { quickAffordanceRepository }, + launchAnimator = launchAnimator, + logger = logger, + devicePolicyManager = devicePolicyManager, + dockManager = dockManager, + biometricSettingsRepository = biometricSettingsRepository, + backgroundDispatcher = testDispatcher, + appContext = mContext, + sceneInteractor = { kosmos.sceneInteractor }, + ) + } + + @Test + fun onQuickAffordanceTriggered() = + testScope.runTest { + val key = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + setUpMocks( + needStrongAuthAfterBoot = needStrongAuthAfterBoot, + keyguardIsUnlocked = keyguardIsUnlocked, + ) + + homeControls.setState( + lockScreenState = + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = DRAWABLE, + ) + ) + homeControls.onTriggeredResult = + if (startActivity) { + KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = INTENT, + canShowWhileLocked = canShowWhileLocked, + ) + } else { + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + + underTest.onQuickAffordanceTriggered( + configKey = "${KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()}::$key", + expandable = expandable, + slotId = "", + ) + + if (startActivity) { + if (needsToUnlockFirst) { + verify(activityStarter) + .postStartActivityDismissingKeyguard( + any(), + /* delay= */ eq(0), + same(animationController), + ) + } else { + verify(activityStarter) + .startActivity( + any(), + /* dismissShade= */ eq(true), + same(animationController), + /* showOverLockscreenWhenLocked= */ eq(true), + ) + } + } else { + verifyZeroInteractions(activityStarter) + } + } + + private fun setUpMocks( + needStrongAuthAfterBoot: Boolean = true, + keyguardIsUnlocked: Boolean = false, + ) { + whenever(lockPatternUtils.getStrongAuthForUser(any())) + .thenReturn( + if (needStrongAuthAfterBoot) { + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT + } else { + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED + } + ) + whenever(keyguardStateController.isUnlocked).thenReturn(keyguardIsUnlocked) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 6b317ead2bfa..1881a9e0c205 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.app.admin.DevicePolicyManager import android.content.Intent import android.os.UserHandle +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.widget.LockPatternUtils @@ -32,6 +33,7 @@ import com.android.systemui.dock.DockManagerFake import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory @@ -75,17 +77,18 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(JUnit4::class) -class KeyguardBottomAreaViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { @Mock private lateinit var expandable: Expandable @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper @@ -111,6 +114,10 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -750,5 +757,11 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { companion object { private const val DEFAULT_BURN_IN_OFFSET = 5 private const val RETURNED_BURN_IN_OFFSET = 3 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 1d98dc3a8f47..0c98cff89ee2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -18,11 +18,12 @@ package com.android.systemui.keyguard.ui.viewmodel import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository @@ -45,14 +46,14 @@ import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith -import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(JUnit4::class) -@DisableSceneContainer -class KeyguardClockViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class KeyguardClockViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope val underTest = kosmos.keyguardClockViewModel @@ -65,6 +66,10 @@ class KeyguardClockViewModelTest : SysuiTestCase() { var config = ClockConfig("TEST", "Test", "") var faceConfig = ClockFaceConfig() + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -276,5 +281,11 @@ class KeyguardClockViewModelTest : SysuiTestCase() { companion object { private const val KEYGUARD_STATUS_BAR_HEIGHT = 20 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt index 72fc65b7c8d4..924b872546c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt @@ -2,6 +2,8 @@ package com.android.systemui.screenshot import android.graphics.drawable.Drawable import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.view.View import android.view.ViewGroup @@ -10,10 +12,16 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Guideline import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.screenshot.message.LabeledIcon +import com.android.systemui.screenshot.message.ProfileMessageController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,6 +36,7 @@ class MessageContainerControllerTest : SysuiTestCase() { lateinit var messageContainer: MessageContainerController @Mock lateinit var workProfileMessageController: WorkProfileMessageController + @Mock lateinit var profileMessageController: ProfileMessageController @Mock lateinit var screenshotDetectionController: ScreenshotDetectionController @@ -46,12 +55,15 @@ class MessageContainerControllerTest : SysuiTestCase() { lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData @Before + @ExperimentalCoroutinesApi fun setup() { MockitoAnnotations.initMocks(this) messageContainer = MessageContainerController( workProfileMessageController, + profileMessageController, screenshotDetectionController, + TestScope(UnconfinedTestDispatcher()) ) screenshotView = ConstraintLayout(mContext) workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon) @@ -78,20 +90,11 @@ class MessageContainerControllerTest : SysuiTestCase() { } @Test - fun testOnScreenshotTakenUserHandle_noWorkProfileFirstRun() { - // (just being explicit here) - whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(null) - - messageContainer.onScreenshotTaken(userHandle) - - verify(workProfileMessageController, never()).populateView(any(), any(), any()) - } - - @Test + @DisableFlags(com.android.systemui.Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX) fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() { whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))) .thenReturn(workProfileData) - messageContainer.onScreenshotTaken(userHandle) + messageContainer.onScreenshotTaken(screenshotData) verify(workProfileMessageController) .populateView(eq(workProfileFirstRunView), eq(workProfileData), any()) @@ -100,10 +103,28 @@ class MessageContainerControllerTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX) + fun testOnScreenshotTakenUserHandle_withProfileProfileFirstRun() = runTest { + val profileData = + ProfileMessageController.ProfileFirstRunData( + LabeledIcon(appName, icon), + ProfileMessageController.FirstRunProfile.PRIVATE + ) + whenever(profileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(profileData) + messageContainer.onScreenshotTaken(screenshotData) + + verify(profileMessageController) + .bindView(eq(workProfileFirstRunView), eq(profileData), any()) + assertEquals(View.VISIBLE, workProfileFirstRunView.visibility) + assertEquals(View.GONE, detectionNoticeView.visibility) + } + + @Test fun testOnScreenshotTakenScreenshotData_nothingToShow() { messageContainer.onScreenshotTaken(screenshotData) verify(workProfileMessageController, never()).populateView(any(), any(), any()) + verify(profileMessageController, never()).bindView(any(), any(), any()) verify(screenshotDetectionController, never()).populateView(any(), any()) assertEquals(View.GONE, container.visibility) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java index 4b7d5f0fff95..ebdc49f75558 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java @@ -87,7 +87,7 @@ public class WorkProfileMessageControllerTest extends SysuiTestCase { when(mMockContext.getSharedPreferences( eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME), eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences); - when(mMockContext.getString(R.string.config_sceenshotWorkProfileFilesApp)) + when(mMockContext.getString(R.string.config_screenshotFilesApp)) .thenReturn(FILES_APP_COMPONENT); when(mMockContext.getString(R.string.screenshot_default_files_app_name)) .thenReturn(DEFAULT_FILES_APP_LABEL); @@ -149,7 +149,7 @@ public class WorkProfileMessageControllerTest extends SysuiTestCase { @Test public void testOnScreenshotTaken_noFilesAppComponentDefined() { - when(mMockContext.getString(R.string.config_sceenshotWorkProfileFilesApp)) + when(mMockContext.getString(R.string.config_screenshotFilesApp)) .thenReturn(""); WorkProfileMessageController.WorkProfileFirstRunData data = diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/message/ProfileMessageControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/message/ProfileMessageControllerTest.kt new file mode 100644 index 000000000000..ce2facd59482 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/message/ProfileMessageControllerTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.message + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.drawable.Drawable +import android.os.UserHandle +import com.android.systemui.screenshot.data.model.ProfileType +import com.android.systemui.screenshot.data.repository.ProfileTypeRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ProfileMessageControllerTest { + @Test + fun personalScreenshot() = runTest { + assertThat( + getMessageController() + .onScreenshotTaken(UserHandle.of(profileTypeRepository.personalUser)) + ) + .isNull() + } + + @Test + fun communalScreenshot() = runTest { + assertThat( + getMessageController() + .onScreenshotTaken(UserHandle.of(profileTypeRepository.communalUser)) + ) + .isNull() + } + + @Test + fun noUserScreenshot() = runTest { + assertThat(getMessageController().onScreenshotTaken(null)).isNull() + } + + @Test + fun alreadyDismissed() = runTest { + val messageController = getMessageController() + profileFirstRunSettings.onMessageDismissed(ProfileMessageController.FirstRunProfile.WORK) + assertThat( + messageController.onScreenshotTaken(UserHandle.of(profileTypeRepository.workUser)) + ) + .isNull() + } + + @Test + fun noFileManager() = runTest { + val messageController = getMessageController(fileManagerComponent = null) + val data = + messageController.onScreenshotTaken(UserHandle.of(profileTypeRepository.workUser)) + assertThat(data?.profileType).isEqualTo(ProfileMessageController.FirstRunProfile.WORK) + assertThat(data?.labeledIcon?.label).isEqualTo(DEFAULT_APP_NAME) + assertThat(data?.labeledIcon?.badgedIcon).isNull() + } + + @Test + fun fileManagerNotFound() = runTest { + val messageController = + getMessageController(fileManagerComponent = ComponentName("Something", "Random")) + val data = + messageController.onScreenshotTaken(UserHandle.of(profileTypeRepository.privateUser)) + assertThat(data?.profileType).isEqualTo(ProfileMessageController.FirstRunProfile.PRIVATE) + assertThat(data?.labeledIcon?.label).isEqualTo(DEFAULT_APP_NAME) + assertThat(data?.labeledIcon?.badgedIcon).isNull() + } + + @Test + fun fileManagerFound() = runTest { + val messageController = getMessageController() + val data = + messageController.onScreenshotTaken(UserHandle.of(profileTypeRepository.privateUser)) + assertThat(data?.profileType).isEqualTo(ProfileMessageController.FirstRunProfile.PRIVATE) + assertThat(data?.labeledIcon?.label).isEqualTo(FILE_MANAGER_LABEL) + assertThat(data?.labeledIcon?.badgedIcon).isEqualTo(drawable) + } + + private val drawable = + object : Drawable() { + override fun draw(canvas: Canvas) {} + + override fun setAlpha(alpha: Int) {} + + override fun setColorFilter(colorFilter: ColorFilter?) {} + + override fun getOpacity(): Int = 0 + } + + private val packageLabelIconProvider = + object : PackageLabelIconProvider { + override suspend fun getPackageLabelIcon( + componentName: ComponentName, + userHandle: UserHandle + ): LabeledIcon { + if (componentName.equals(FILE_MANAGER_COMPONENT)) { + return LabeledIcon(FILE_MANAGER_LABEL, drawable) + } else { + throw PackageManager.NameNotFoundException() + } + } + } + + private class FakeProfileFirstRunResources(private val fileManager: ComponentName?) : + ProfileFirstRunFileResources { + override fun fileManagerComponentName(): ComponentName? { + return fileManager + } + + override fun defaultFileApp() = LabeledIcon(DEFAULT_APP_NAME, badgedIcon = null) + } + + private val profileFirstRunSettings = + object : ProfileFirstRunSettings { + private val dismissed = + mutableMapOf( + ProfileMessageController.FirstRunProfile.WORK to false, + ProfileMessageController.FirstRunProfile.PRIVATE to false, + ) + + override fun messageAlreadyDismissed( + profileType: ProfileMessageController.FirstRunProfile + ): Boolean { + return dismissed.getOrDefault(profileType, false) + } + + override fun onMessageDismissed(profileType: ProfileMessageController.FirstRunProfile) { + dismissed[profileType] = true + } + } + + private val profileTypeRepository = + object : ProfileTypeRepository { + override suspend fun getProfileType(userId: Int): ProfileType { + return when (userId) { + workUser -> ProfileType.WORK + privateUser -> ProfileType.PRIVATE + communalUser -> ProfileType.COMMUNAL + else -> ProfileType.NONE + } + } + + val personalUser = 0 + val workUser = 1 + val privateUser = 2 + val communalUser = 3 + } + + private fun getMessageController( + fileManagerComponent: ComponentName? = FILE_MANAGER_COMPONENT + ) = + ProfileMessageController( + packageLabelIconProvider, + FakeProfileFirstRunResources(fileManagerComponent), + profileFirstRunSettings, + profileTypeRepository + ) + + companion object { + val FILE_MANAGER_COMPONENT = ComponentName("package", "component") + const val DEFAULT_APP_NAME = "default app" + const val FILE_MANAGER_LABEL = "file manager" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index 0a9bac91004a..7ade053720e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -49,6 +49,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.Flags; import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; @@ -58,6 +59,7 @@ import android.hardware.display.AmbientDisplayConfiguration; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -469,6 +471,7 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_SORT_SECTION_BY_TIME) public void testShouldHeadsUp_oldWhen_whenZero() throws Exception { ensureStateForHeadsUpWhenAwake(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt index 4dd97bc90546..9b4f9312a7ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt @@ -17,19 +17,21 @@ package com.android.systemui.statusbar.phone import android.content.pm.PackageManager -import android.testing.AndroidTestingRunner +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED @@ -58,15 +60,17 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @TestableLooper.RunWithLooper -class KeyguardBypassControllerTest : SysuiTestCase() { +class KeyguardBypassControllerTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val featureFlags = FakeFeatureFlags() - private val shadeRepository = FakeShadeRepository() + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } private lateinit var keyguardBypassController: KeyguardBypassController private lateinit var postureControllerCallback: DevicePostureController.Callback @@ -79,6 +83,18 @@ class KeyguardBypassControllerTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var packageManager: PackageManager + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Captor private val postureCallbackCaptor = ArgumentCaptor.forClass(DevicePostureController.Callback::class.java) @@ -148,7 +164,7 @@ class KeyguardBypassControllerTest : SysuiTestCase() { statusBarStateController, lockscreenUserManager, keyguardStateController, - shadeRepository, + { kosmos.shadeInteractor }, devicePostureController, keyguardTransitionInteractor, dumpManager, @@ -293,13 +309,13 @@ class KeyguardBypassControllerTest : SysuiTestCase() { testScope.runTest { initKeyguardBypassController() assertThat(keyguardBypassController.qsExpanded).isFalse() - val job = keyguardBypassController.listenForQsExpandedChange(this) - shadeRepository.setQsExpansion(0.5f) + val job = keyguardBypassController.listenForQsExpandedChange() + shadeTestUtil.setQsExpansion(0.5f) runCurrent() assertThat(keyguardBypassController.qsExpanded).isTrue() - shadeRepository.setQsExpansion(0f) + shadeTestUtil.setQsExpansion(0f) runCurrent() assertThat(keyguardBypassController.qsExpanded).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index d365663e3621..ba38f871cf9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -314,6 +314,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { private class UnfoldConfig : UnfoldTransitionConfig { override var isEnabled: Boolean = false override var isHingeAngleEnabled: Boolean = false + override val isHapticsEnabled: Boolean = false override val halfFoldedTimeoutMillis: Int = 0 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index efd7f9969d49..05464f3b715a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -23,6 +23,7 @@ import android.app.Notification import android.app.PendingIntent import android.app.Person import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.service.notification.NotificationListenerService.REASON_USER_STOPPED import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -194,7 +195,7 @@ class OngoingCallControllerTest : SysuiTestCase() { /** Regression test for b/192379214. */ @Test - @DisableFlags(android.app.Flags.FLAG_UPDATE_RANKING_TIME) + @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) fun onEntryUpdated_notificationWhenIsZero_timeHidden() { val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) notification.modifyNotification(context).setWhen(0) @@ -210,6 +211,22 @@ class OngoingCallControllerTest : SysuiTestCase() { } @Test + @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) + fun onEntryUpdated_notificationWhenIsZero_timeShown() { + val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) + notification.modifyNotification(context).setWhen(0) + + notifCollectionListener.onEntryUpdated(notification.build()) + chipView.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + + assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth) + .isGreaterThan(0) + } + + @Test fun onEntryUpdated_notificationWhenIsValid_timeShown() { val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index 8eea29beb6dd..ceaae9e02e87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -587,7 +587,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { advanceTimeBy(10.seconds) assertThat(latest) - .isEqualTo(context.getString(R.string.satellite_not_connected_carrier_text)) + .isEqualTo(context.getString(R.string.satellite_connected_carrier_text)) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index bdd3d188ad91..cfa734a14811 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -16,71 +16,69 @@ package com.android.systemui.statusbar.ui.viewmodel +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository -import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor -import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.batteryController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito.verify +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest @OptIn(ExperimentalCoroutinesApi::class) -class KeyguardStatusBarViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class KeyguardStatusBarViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val keyguardRepository = FakeKeyguardRepository() - private val keyguardInteractor = - KeyguardInteractor( - keyguardRepository, - mock<CommandQueue>(), - PowerInteractorFactory.create().powerInteractor, - FakeKeyguardBouncerRepository(), - ConfigurationInteractor(FakeConfigurationRepository()), - FakeShadeRepository(), - kosmos.keyguardTransitionInteractor, - { kosmos.sceneInteractor }, - { kosmos.fromGoneTransitionInteractor }, - { kosmos.sharedNotificationContainerInteractor }, - testScope, - ) - private val keyguardStatusBarInteractor = - KeyguardStatusBarInteractor( - FakeKeyguardStatusBarRepository(), - ) - private val batteryController = mock<BatteryController>() - - private val underTest = - KeyguardStatusBarViewModel( - testScope.backgroundScope, - keyguardInteractor, - keyguardStatusBarInteractor, - batteryController, - ) + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val keyguardInteractor by lazy { kosmos.keyguardInteractor } + private val keyguardStatusBarInteractor by lazy { kosmos.keyguardStatusBarInteractor } + private val batteryController = kosmos.batteryController + + lateinit var underTest: KeyguardStatusBarViewModel + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + + @Before + fun setup() { + underTest = + KeyguardStatusBarViewModel( + testScope.backgroundScope, + keyguardInteractor, + keyguardStatusBarInteractor, + batteryController, + ) + } @Test fun isVisible_dozing_false() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt index fd513c9c9235..06f1a8848fad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt @@ -17,13 +17,12 @@ package com.android.systemui.unfold import android.os.VibrationAttributes import android.os.VibrationEffect -import android.os.Vibrator +import android.os.vibrator import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.unfold.util.TestFoldProvider +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,15 +34,20 @@ import org.mockito.Mockito.verify @SmallTest class UnfoldHapticsPlayerTest : SysuiTestCase() { - private val progressProvider = FakeUnfoldTransitionProvider() - private val vibrator: Vibrator = mock() - private val testFoldProvider = TestFoldProvider() + private val kosmos = testKosmos() + + private val progressProvider = kosmos.fakeUnfoldTransitionProgressProvider + private val vibrator = kosmos.vibrator + private val transitionConfig = kosmos.unfoldTransitionConfig + private val testFoldProvider = kosmos.foldProvider private lateinit var player: UnfoldHapticsPlayer @Before fun before() { - player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, Runnable::run, vibrator) + transitionConfig.isHapticsEnabled = true + player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, transitionConfig, + Runnable::run, vibrator) } @Test @@ -58,6 +62,21 @@ class UnfoldHapticsPlayerTest : SysuiTestCase() { } @Test + fun testHapticsDisabled_unfoldingTransitionFinishing_doesNotPlayHaptics() { + transitionConfig.isHapticsEnabled = false + player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, transitionConfig, + Runnable::run, vibrator) + + testFoldProvider.onFoldUpdate(isFolded = true) + testFoldProvider.onFoldUpdate(isFolded = false) + progressProvider.onTransitionStarted() + progressProvider.onTransitionProgress(0.5f) + progressProvider.onTransitionFinishing() + + verify(vibrator).vibrate(any<VibrationEffect>(), any<VibrationAttributes>()) + } + + @Test fun testUnfoldingTransitionFinishingLate_doesNotPlayHaptics() { testFoldProvider.onFoldUpdate(isFolded = true) testFoldProvider.onFoldUpdate(isFolded = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTestUtilsKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTestUtilsKosmos.kt new file mode 100644 index 000000000000..c073903bc005 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTestUtilsKosmos.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold + +import com.android.systemui.unfold.util.TestFoldProvider +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.unfold.config.TestUnfoldTransitionConfig + +var Kosmos.foldProvider: TestFoldProvider by Kosmos.Fixture { TestFoldProvider() } + +var Kosmos.unfoldTransitionConfig: TestUnfoldTransitionConfig + by Kosmos.Fixture { TestUnfoldTransitionConfig() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfigTest.kt index ab450e2506f9..3c539979c527 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfigTest.kt @@ -47,6 +47,12 @@ class ResourceUnfoldTransitionConfigTest : SysuiTestCase() { } @Test + fun testHapticsEnabled() { + assertThat(config.isHapticsEnabled).isEqualTo(mContext.resources + .getBoolean(com.android.internal.R.bool.config_unfoldTransitionHapticsEnabled)) + } + + @Test fun testHalfFoldedTimeout() { assertThat(config.halfFoldedTimeoutMillis).isEqualTo(mContext.resources .getInteger(com.android.internal.R.integer.config_unfoldTransitionHalfFoldedTimeout)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/config/TestUnfoldTransitionConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/config/TestUnfoldTransitionConfig.kt new file mode 100644 index 000000000000..058611507280 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/config/TestUnfoldTransitionConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold.config + +class TestUnfoldTransitionConfig( + override var isEnabled: Boolean = false, + override var isHingeAngleEnabled: Boolean = false, + override var isHapticsEnabled: Boolean = false, + override var halfFoldedTimeoutMillis: Int = 1000 +) : UnfoldTransitionConfig diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java index 5d341207ef6a..8d26c877f4cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java @@ -16,8 +16,6 @@ package com.android.systemui.util.service; -import static com.google.common.truth.Truth.assertThat; - import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; @@ -118,6 +116,7 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { connection.addCallback(mCallback); mExecutor.runAllReady(); connection.bind(); + mExecutor.runAllReady(); when(mTransformer.convert(eq(mBinder))).thenReturn(mResult); @@ -143,8 +142,8 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { when(mContext.bindServiceAsUser(eq(mIntent), eq(connection), anyInt(), eq(UserHandle.of(MAIN_USER_ID)))).thenReturn(true); connection.bind(); + mExecutor.runAllReady(); connection.onServiceDisconnected(mComponentName); - mExecutor.runAllReady(); // Ensure proper disconnect reason reported back @@ -157,6 +156,7 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { clearInvocations(mContext); // Ensure unbind after disconnect has no effect on the connection connection.unbind(); + mExecutor.runAllReady(); verify(mContext, never()).unbindService(eq(connection)); } @@ -197,7 +197,8 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { // Verify that the exception was caught and that bind returns false, and we properly // unbind. - assertThat(connection.bind()).isFalse(); + connection.bind(); + mExecutor.runAllReady(); verify(mContext).unbindService(connection); } @@ -212,13 +213,15 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { .thenThrow(new SecurityException()); // Verify that bind returns false and we properly unbind. - assertThat(connection.bind()).isFalse(); + connection.bind(); + mExecutor.runAllReady(); verify(mContext).unbindService(connection); clearInvocations(mContext); // Ensure unbind after the failed bind has no effect. connection.unbind(); + mExecutor.runAllReady(); verify(mContext, never()).unbindService(eq(connection)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 2017954c6b34..56e5e293c799 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -77,10 +77,10 @@ import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Pair; import android.util.SparseArray; @@ -99,46 +99,28 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.BubbleIconFactory; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.FakeFeatureFlagsClassic; +import com.android.systemui.flags.SceneContainerFlagParameterizationKt; import com.android.systemui.keyguard.KeyguardViewMediator; -import com.android.systemui.keyguard.data.repository.FakeCommandQueue; -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.data.repository.SceneContainerRepository; -import com.android.systemui.scene.domain.interactor.SceneInteractor; -import com.android.systemui.scene.shared.logger.SceneLogger; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.settings.UserTracker; -import com.android.systemui.shade.LargeScreenHeaderHelper; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeWindowLogger; -import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl; -import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.NotificationEntryHelper; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -154,7 +136,6 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderTestUtil; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; -import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -163,13 +144,10 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository; -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.systemui.util.FakeEventLog; import com.android.systemui.util.settings.FakeGlobalSettings; import com.android.systemui.util.settings.SystemSettings; @@ -207,8 +185,6 @@ import com.android.wm.shell.taskview.TaskView; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; -import kotlinx.coroutines.test.TestScope; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -227,8 +203,11 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubblesTest extends SysuiTestCase { @Mock @@ -355,11 +334,8 @@ public class BubblesTest extends SysuiTestCase { private Icon mAppBubbleIcon; @Mock private Display mDefaultDisplay; - @Mock - private LargeScreenHeaderHelper mLargeScreenHeaderHelper; private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private final TestScope mTestScope = mKosmos.getTestScope(); private ShadeInteractor mShadeInteractor; private ShellTaskOrganizer mShellTaskOrganizer; private TaskViewTransitions mTaskViewTransitions; @@ -376,8 +352,16 @@ public class BubblesTest extends SysuiTestCase { private UserHandle mUser0; private FakeBubbleProperties mBubbleProperties; - private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor; - private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor; + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag(); + } + + public BubblesTest(FlagsParameterization flags) { + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() throws Exception { @@ -402,77 +386,14 @@ public class BubblesTest extends SysuiTestCase { FakeDeviceProvisioningRepository deviceProvisioningRepository = - new FakeDeviceProvisioningRepository(); + mKosmos.getFakeDeviceProvisioningRepository(); deviceProvisioningRepository.setDeviceProvisioned(true); - FakeKeyguardRepository keyguardRepository = new FakeKeyguardRepository(); - FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); - FakeShadeRepository shadeRepository = new FakeShadeRepository(); - FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); - - PowerInteractor powerInteractor = new PowerInteractor( - mKosmos.getPowerRepository(), - mKosmos.getFalsingCollector(), - mock(ScreenOffAnimationController.class), - mStatusBarStateController); - - SceneInteractor sceneInteractor = new SceneInteractor( - mTestScope.getBackgroundScope(), - new SceneContainerRepository( - mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig(), - mKosmos.getSceneDataSource()), - mock(SceneLogger.class), - mKosmos.getDeviceUnlockedInteractor()); - - KeyguardTransitionInteractor keyguardTransitionInteractor = - mKosmos.getKeyguardTransitionInteractor(); - KeyguardInteractor keyguardInteractor = new KeyguardInteractor( - keyguardRepository, - new FakeCommandQueue(), - powerInteractor, - new FakeKeyguardBouncerRepository(), - new ConfigurationInteractor(configurationRepository), - shadeRepository, - keyguardTransitionInteractor, - () -> sceneInteractor, - () -> mKosmos.getFromGoneTransitionInteractor(), - () -> mKosmos.getSharedNotificationContainerInteractor(), - mTestScope); - - mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor(); - mFromPrimaryBouncerTransitionInteractor = - mKosmos.getFromPrimaryBouncerTransitionInteractor(); - - ResourcesSplitShadeStateController splitShadeStateController = - new ResourcesSplitShadeStateController(); DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor = mock(DeviceEntryUdfpsInteractor.class); when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); - mShadeInteractor = - new ShadeInteractorImpl( - mTestScope.getBackgroundScope(), - mKosmos.getDeviceProvisioningInteractor(), - new FakeDisableFlagsRepository(), - mDozeParameters, - keyguardRepository, - keyguardTransitionInteractor, - powerInteractor, - new FakeUserSetupRepository(), - mock(UserSwitcherInteractor.class), - new ShadeInteractorLegacyImpl( - mTestScope.getBackgroundScope(), keyguardRepository, - new SharedNotificationContainerInteractor( - configurationRepository, - mContext, - splitShadeStateController, - keyguardInteractor, - deviceEntryUdfpsInteractor, - () -> mLargeScreenHeaderHelper), - shadeRepository - ) - ); + mShadeInteractor = mKosmos.getShadeInteractor(); mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl( mContext, @@ -2467,6 +2388,10 @@ public class BubblesTest extends SysuiTestCase { mStateChangeCalls++; mLastUpdate = update; } + + @Override + public void animateBubbleBarLocation(BubbleBarLocation location) { + } } private static class FakeBubbleProperties implements BubbleProperties { diff --git a/packages/SystemUI/tests/utils/src/android/os/VibratorKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/VibratorKosmos.kt new file mode 100644 index 000000000000..872b25c81835 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/os/VibratorKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.vibrator by Kosmos.Fixture { mock<Vibrator>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt index 4221d06e27ed..297d1d8c2c0a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.shade.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.util.mockito.mock val Kosmos.shadeLockscreenInteractor by @@ -28,5 +29,6 @@ val Kosmos.shadeLockscreenInteractor by shadeInteractor = shadeInteractorImpl, sceneInteractor = sceneInteractor, lockIconViewController = mock(), + shadeRepository = shadeRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryKosmos.kt new file mode 100644 index 000000000000..da95ee90dade --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyguardStatusBarRepository: FakeKeyguardStatusBarRepository by + Kosmos.Fixture { fakeKeyguardStatusBarRepository } + +val Kosmos.fakeKeyguardStatusBarRepository: FakeKeyguardStatusBarRepository by + Kosmos.Fixture { FakeKeyguardStatusBarRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractorKosmos.kt new file mode 100644 index 000000000000..71ed5f696906 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.data.repository.keyguardStatusBarRepository + +val Kosmos.keyguardStatusBarInteractor: KeyguardStatusBarInteractor by + Kosmos.Fixture { + KeyguardStatusBarInteractor( + keyguardStatusBarRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index cbba80baee7f..d00eedf22efc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel @@ -60,6 +61,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { shadeInteractor = shadeInteractor, notificationStackAppearanceInteractor = notificationStackAppearanceInteractor, alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, + aodToGoneTransitionViewModel = aodToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt index 4a794523a86b..146f1093fe69 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel import android.content.res.mainResources +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.fakeConfigurationController @@ -72,5 +73,6 @@ var Kosmos.volumePanelViewModel: VolumePanelViewModel by testScope.backgroundScope, KosmosVolumePanelComponentFactory(this), fakeConfigurationController, + broadcastDispatcher, ) } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt index c51372975a67..ca1daf66f581 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt @@ -33,6 +33,12 @@ class ResourceUnfoldTransitionConfig @Inject constructor() : UnfoldTransitionCon Resources.getSystem().getBoolean(id) } + override val isHapticsEnabled: Boolean by lazy { + val id = Resources.getSystem() + .getIdentifier("config_unfoldTransitionHapticsEnabled", "bool", "android") + Resources.getSystem().getBoolean(id) + } + override val halfFoldedTimeoutMillis: Int by lazy { val id = Resources.getSystem() .getIdentifier("config_unfoldTransitionHalfFoldedTimeout", "integer", "android") diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt index 765e862aa00d..1084cb3f234b 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt @@ -18,5 +18,6 @@ package com.android.systemui.unfold.config interface UnfoldTransitionConfig { val isEnabled: Boolean val isHingeAngleEnabled: Boolean + val isHapticsEnabled: Boolean val halfFoldedTimeoutMillis: Int } diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index e77f846ffaa6..f6885e1e74ba 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -1,11 +1,18 @@ +// Keep the following two TEST_MAPPINGs in sync: +// frameworks/base/ravenwood/TEST_MAPPING +// frameworks/base/tools/hoststubgen/TEST_MAPPING { "presubmit": [ + { "name": "tiny-framework-dump-test" }, + { "name": "hoststubgentest" }, + { "name": "hoststubgen-invoke-test" }, { "name": "RavenwoodMockitoTest_device" }, { "name": "RavenwoodBivalentTest_device" }, + // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING { "name": "SystemUIGoogleTests", "options": [ @@ -18,6 +25,19 @@ ] } ], + "presubmit-large": [ + { + "name": "SystemUITests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ], "ravenwood-presubmit": [ { "name": "RavenwoodMinimumTest", diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh index b5843d0fa26b..beacde282d49 100755 --- a/ravenwood/scripts/ravenwood-stats-collector.sh +++ b/ravenwood/scripts/ravenwood-stats-collector.sh @@ -39,14 +39,18 @@ dump() { local jar=$1 local file=$2 - sed -e '1d' -e "s/^/$jar,/" $file + # Use sed to remove the header + prepend the jar filename. + sed -e '1d' -e "s/^/$jar,/" $file } collect_stats() { local out="$1" { - echo 'Jar,PackageName,ClassName,SupportedMethods,TotalMethods' - dump "framework-minus-apex" hoststubgen_framework-minus-apex_stats.csv + # Copy the header, with the first column appended. + echo -n "Jar," + head -n 1 hoststubgen_framework-minus-apex_stats.csv + + dump "framework-minus-apex" hoststubgen_framework-minus-apex_stats.csv dump "service.core" hoststubgen_services.core_stats.csv } > "$out" @@ -56,7 +60,10 @@ collect_stats() { collect_apis() { local out="$1" { - echo 'Jar,PackageName,ClassName,MethodName,Descriptor' + # Copy the header, with the first column appended. + echo -n "Jar," + head -n 1 hoststubgen_framework-minus-apex_apis.csv + dump "framework-minus-apex" hoststubgen_framework-minus-apex_apis.csv dump "service.core" hoststubgen_services.core_apis.csv } > "$out" diff --git a/services/core/Android.bp b/services/core/Android.bp index 300b147509a7..8ee560b811aa 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -256,6 +256,7 @@ java_library_static { "stats_flags_lib", "core_os_flags_lib", "connectivity_flags_lib", + "dreams_flags_lib", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 23891d23cf4f..ec0d8974603e 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -3864,10 +3864,12 @@ public final class ActiveServices { final long lastTopTime = sr.app.mState.getLastTopTime(); final long constantTimeLimit = getTimeLimitForFgsType(fgsType); final long nowUptime = SystemClock.uptimeMillis(); - if (constantTimeLimit > (nowUptime - lastTopTime)) { + if (lastTopTime != Long.MIN_VALUE && constantTimeLimit > (nowUptime - lastTopTime)) { + // Discard any other messages for this service + mFGSAnrTimer.discard(sr); + mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); // The app was in the TOP state after the FGS was started so its time allowance // should be counted from that time since this is considered a user interaction - mFGSAnrTimer.discard(sr); final Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr); mAm.mHandler.sendMessageAtTime(msg, lastTopTime + constantTimeLimit); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index c47e42dfd07e..1b59c1829d4c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5034,7 +5034,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public final void finishAttachApplication(long startSeq) { + public final void finishAttachApplication(long startSeq, long timestampApplicationOnCreateNs) { final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); @@ -5054,6 +5054,11 @@ public class ActivityManagerService extends IActivityManager.Stub } finally { Binder.restoreCallingIdentity(origId); } + + if (android.app.Flags.appStartInfoTimestamps() && timestampApplicationOnCreateNs > 0) { + addStartInfoTimestampInternal(ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE, + timestampApplicationOnCreateNs, UserHandle.getUserId(uid), uid); + } } private void handleBindApplicationTimeoutSoft(ProcessRecord app, int softTimeoutMillis) { @@ -10253,10 +10258,15 @@ public class ActivityManagerService extends IActivityManager.Stub mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, true, ALLOW_NON_FULL, "addStartInfoTimestamp", null); - final String packageName = Settings.getPackageNameForUid(mContext, callingUid); + addStartInfoTimestampInternal(key, timestampNs, userId, callingUid); + } - mProcessList.getAppStartInfoTracker().addTimestampToStart(packageName, - UserHandle.getUid(userId, UserHandle.getAppId(callingUid)), timestampNs, key); + private void addStartInfoTimestampInternal(int key, long timestampNs, int userId, int uid) { + mProcessList.getAppStartInfoTracker().addTimestampToStart( + Settings.getPackageNameForUid(mContext, uid), + UserHandle.getUid(userId, UserHandle.getAppId(uid)), + timestampNs, + key); } @Override diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 8b64538ac33c..9b83ede09da4 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -35,6 +35,10 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; /** * Maps system settings to system properties. @@ -320,15 +324,30 @@ public class SettingsToPropertiesMapper { NAMESPACE_REBOOT_STAGING, AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties properties) -> { - String scope = properties.getNamespace(); - for (String key : properties.getKeyset()) { - String aconfigPropertyName = makeAconfigFlagStagedPropertyName(key); - if (aconfigPropertyName == null) { - log("unable to construct system property for " + scope + "/" + key); - return; + + HashMap<String, HashMap<String, String>> propsToStage = + getStagedFlagsWithValueChange(properties); + + for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) { + String actualNamespace = entry.getKey(); + HashMap<String, String> flagValuesToStage = entry.getValue(); + + for (String flagName : flagValuesToStage.keySet()) { + String stagedValue = flagValuesToStage.get(flagName); + String propertyName = "next_boot." + makeAconfigFlagPropertyName( + actualNamespace, flagName); + + if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) + || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { + log("unable to construct system property for " + actualNamespace + + "/" + flagName); + continue; + } + + setProperty(propertyName, stagedValue); } - setProperty(aconfigPropertyName, properties.getString(key, null)); } + }); } @@ -401,25 +420,18 @@ public class SettingsToPropertiesMapper { } /** - * system property name constructing rule for staged aconfig flags, the flag name - * is in the form of [namespace]*[actual flag name], we should push the following - * to system properties - * "next_boot.[actual sys prop name]". + * system property name constructing rule for aconfig flags: + * "persist.device_config.aconfig_flags.[category_name].[flag_name]". * If the name contains invalid characters or substrings for system property name, * will return null. + * @param categoryName * @param flagName * @return */ @VisibleForTesting - static String makeAconfigFlagStagedPropertyName(String flagName) { - int idx = flagName.indexOf(NAMESPACE_REBOOT_STAGING_DELIMITER); - if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { - log("invalid staged flag: " + flagName); - return null; - } - - String propertyName = "next_boot." + makeAconfigFlagPropertyName( - flagName.substring(0, idx), flagName.substring(idx+1)); + static String makeAconfigFlagPropertyName(String categoryName, String flagName) { + String propertyName = SYSTEM_PROPERTY_PREFIX + "aconfig_flags." + + categoryName + "." + flagName; if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { @@ -430,25 +442,60 @@ public class SettingsToPropertiesMapper { } /** - * system property name constructing rule for aconfig flags: - * "persist.device_config.aconfig_flags.[category_name].[flag_name]". - * If the name contains invalid characters or substrings for system property name, - * will return null. - * @param categoryName - * @param flagName - * @return + * Get the flags that need to be staged in sys prop, only these with a real value + * change needs to be staged in sys prop. Otherwise, the flag stage is useless and + * create performance problem at sys prop side. + * @param properties + * @return a hash map of namespace name to actual flags to stage */ @VisibleForTesting - static String makeAconfigFlagPropertyName(String categoryName, String flagName) { - String propertyName = SYSTEM_PROPERTY_PREFIX + "aconfig_flags." + - categoryName + "." + flagName; + static HashMap<String, HashMap<String, String>> getStagedFlagsWithValueChange( + DeviceConfig.Properties properties) { - if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX) - || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) { - return null; + // sort flags by actual namespace of the flag + HashMap<String, HashMap<String, String>> stagedProps = new HashMap<>(); + for (String flagName : properties.getKeyset()) { + int idx = flagName.indexOf(NAMESPACE_REBOOT_STAGING_DELIMITER); + if (idx == -1 || idx == flagName.length() - 1 || idx == 0) { + log("invalid staged flag: " + flagName); + continue; + } + String actualNamespace = flagName.substring(0, idx); + String actualFlagName = flagName.substring(idx+1); + HashMap<String, String> flagStagedValues = stagedProps.get(actualNamespace); + if (flagStagedValues == null) { + flagStagedValues = new HashMap<String, String>(); + stagedProps.put(actualNamespace, flagStagedValues); + } + flagStagedValues.put(actualFlagName, properties.getString(flagName, null)); + } + + // for each namespace, find flags with real flag value change + HashMap<String, HashMap<String, String>> propsToStage = new HashMap<>(); + for (HashMap.Entry<String, HashMap<String, String>> entry : stagedProps.entrySet()) { + String actualNamespace = entry.getKey(); + HashMap<String, String> flagStagedValues = entry.getValue(); + Map<String, String> flagCurrentValues = Settings.Config.getStrings( + actualNamespace, new ArrayList<String>(flagStagedValues.keySet())); + + HashMap<String, String> flagsToStage = new HashMap<>(); + for (String flagName : flagStagedValues.keySet()) { + String stagedValue = flagStagedValues.get(flagName); + String currentValue = flagCurrentValues.get(flagName); + if (currentValue == null) { + currentValue = new String("false"); + } + if (stagedValue != null && !stagedValue.equalsIgnoreCase(currentValue)) { + flagsToStage.put(flagName, stagedValue); + } } - return propertyName; + if (!flagsToStage.isEmpty()) { + propsToStage.put(actualNamespace, flagsToStage); + } + } + + return propsToStage; } private void setProperty(String key, String value) { diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 94baf88e4853..2285826c0b58 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -24,6 +24,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; +import android.companion.virtual.VirtualDeviceManager; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; @@ -134,7 +135,7 @@ final class AttributedOp { AppOpsManager.OpEventProxyInfo proxyInfo = null; if (proxyUid != Process.INVALID_UID) { proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName, - proxyAttributionTag); + proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT); } AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key); @@ -855,7 +856,7 @@ final class AttributedOp { AppOpsManager.OpEventProxyInfo proxyInfo = null; if (proxyUid != Process.INVALID_UID) { proxyInfo = mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName, - proxyAttributionTag); + proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT); } if (recycled != null) { @@ -881,10 +882,11 @@ final class AttributedOp { AppOpsManager.OpEventProxyInfo acquire(@IntRange(from = 0) int uid, @Nullable String packageName, - @Nullable String attributionTag) { + @Nullable String attributionTag, + @Nullable String deviceId) { AppOpsManager.OpEventProxyInfo recycled = acquire(); if (recycled != null) { - recycled.reinit(uid, packageName, attributionTag); + recycled.reinit(uid, packageName, attributionTag, deviceId); return recycled; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 77654d4a5413..da528a2591a1 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1772,6 +1772,7 @@ public class AudioDeviceBroker { @Override public void handleMessage(Message msg) { + int muteCheckDelayMs = BTA2DP_MUTE_CHECK_DELAY_MS; switch (msg.what) { case MSG_RESTORE_DEVICES: synchronized (mSetModeLock) { @@ -1870,7 +1871,7 @@ public class AudioDeviceBroker { btInfo.mDevice, btInfo.mProfile, btInfo.mIsLeOutput, "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE"); synchronized (mDeviceStateLock) { - mDeviceInventory.onBluetoothDeviceConfigChange(btInfo, + muteCheckDelayMs += mDeviceInventory.onBluetoothDeviceConfigChange(btInfo, codecAndChanged.first, codecAndChanged.second, BtHelper.EVENT_DEVICE_CONFIG_CHANGE); } @@ -2060,7 +2061,7 @@ public class AudioDeviceBroker { // Give some time to Bluetooth service to post a connection message // in case of active device switch if (MESSAGES_MUTE_MUSIC.contains(msg.what)) { - sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, BTA2DP_MUTE_CHECK_DELAY_MS); + sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, muteCheckDelayMs); } if (isMessageHandledUnderWakelock(msg.what)) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 9bdc51efb76f..c9612caf67a0 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -864,9 +864,25 @@ public class AudioDeviceInventory { } } + // Additional delay added to the music mute duration when a codec config change is executed. + static final int BT_CONFIG_CHANGE_MUTE_DELAY_MS = 500; + /** + * Handles a Bluetooth link codec configuration change communicated by the Bluetooth stack. + * Called when either A2DP or LE Audio codec encoding or sampling rate changes: + * the change is communicated to native audio policy to eventually reconfigure the audio + * path. + * Also used to notify a change in preferred mode (duplex or output) for Bluetooth profiles. + * + * @param btInfo contains all information on the Bluetooth device and profile + * @param codec the requested audio encoding (e.g SBC) + * @param codecChanged true if a codec parameter changed, false for preferred mode change + * @param event currently only EVENT_DEVICE_CONFIG_CHANGE + * @return an optional additional delay in milliseconds to add to the music mute period in + * case of an actual codec reconfiguration. + */ @GuardedBy("mDeviceBroker.mDeviceStateLock") - /*package*/ void onBluetoothDeviceConfigChange( + /*package*/ int onBluetoothDeviceConfigChange( @NonNull AudioDeviceBroker.BtDeviceInfo btInfo, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, boolean codecChanged, int event) { @@ -874,10 +890,11 @@ public class AudioDeviceInventory { + "onBluetoothDeviceConfigChange") .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event)); + int delayMs = 0; final BluetoothDevice btDevice = btInfo.mDevice; if (btDevice == null) { mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record(); - return; + return delayMs; } if (AudioService.DEBUG_DEVICES) { Log.d(TAG, "onBluetoothDeviceConfigChange btDevice=" + btDevice); @@ -899,7 +916,7 @@ public class AudioDeviceInventory { .printSlog(EventLogger.Event.ALOGI, TAG)); mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2dp config change ignored") .record(); - return; + return delayMs; } final String key = DeviceInfo.makeDeviceListKey( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); @@ -907,7 +924,7 @@ public class AudioDeviceInventory { if (di == null) { Log.e(TAG, "invalid null DeviceInfo in onBluetoothDeviceConfigChange"); mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record(); - return; + return delayMs; } mmi.set(MediaMetrics.Property.ADDRESS, address) @@ -915,7 +932,6 @@ public class AudioDeviceInventory { .set(MediaMetrics.Property.INDEX, volume) .set(MediaMetrics.Property.NAME, di.mDeviceName); - if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { if (btInfo.mProfile == BluetoothProfile.A2DP || btInfo.mProfile == BluetoothProfile.LE_AUDIO @@ -943,6 +959,7 @@ public class AudioDeviceInventory { + address + " codec=" + AudioSystem.audioFormatToString(codec)) .printSlog(EventLogger.Event.ALOGI, TAG)); + delayMs = BT_CONFIG_CHANGE_MUTE_DELAY_MS; } } } @@ -952,6 +969,7 @@ public class AudioDeviceInventory { } } mmi.record(); + return delayMs; } /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java index d669c8d4656f..030ce12f5063 100644 --- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java +++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java @@ -61,6 +61,8 @@ class AudioManagerShellCommand extends ShellCommand { return getSoundDoseValue(); case "reset-sound-dose-timeout": return resetSoundDoseTimeout(); + case "set-ringer-mode": + return setRingerMode(); case "set-volume": return setVolume(); case "set-device-volume": @@ -100,6 +102,8 @@ class AudioManagerShellCommand extends ShellCommand { pw.println(" Returns the current sound dose value"); pw.println(" reset-sound-dose-timeout"); pw.println(" Resets the sound dose timeout used for momentary exposure"); + pw.println(" set-ringer-mode NORMAL|SILENT|VIBRATE"); + pw.println(" Sets the Ringer mode to one of NORMAL|SILENT|VIBRATE"); pw.println(" set-volume STREAM_TYPE VOLUME_INDEX"); pw.println(" Sets the volume for STREAM_TYPE to VOLUME_INDEX"); pw.println(" set-device-volume STREAM_TYPE VOLUME_INDEX NATIVE_DEVICE_TYPE"); @@ -150,6 +154,34 @@ class AudioManagerShellCommand extends ShellCommand { return 0; } + private int setRingerMode() { + String ringerModeText = getNextArg(); + if (ringerModeText == null) { + getErrPrintWriter().println("Error: no ringer mode specified"); + return 1; + } + + final int ringerMode = getRingerMode(ringerModeText); + if (!AudioManager.isValidRingerMode(ringerMode)) { + getErrPrintWriter().println( + "Error: invalid value of ringerMode, should be one of NORMAL|SILENT|VIBRATE"); + return 1; + } + + final AudioManager am = mService.mContext.getSystemService(AudioManager.class); + am.setRingerModeInternal(ringerMode); + return 0; + } + + private int getRingerMode(String ringerModeText) { + return switch (ringerModeText) { + case "NORMAL" -> AudioManager.RINGER_MODE_NORMAL; + case "VIBRATE" -> AudioManager.RINGER_MODE_VIBRATE; + case "SILENT" -> AudioManager.RINGER_MODE_SILENT; + default -> -1; + }; + } + private int getIsSurroundFormatEnabled() { String surroundFormatText = getNextArg(); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index 559462a50f4f..b0e7575689ba 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -218,6 +218,7 @@ public class Sensor { } @VisibleForTesting @Nullable protected AidlSession getSessionForUser(int userId) { + Slog.d(TAG, "getSessionForUser: mCurrentSession: " + mCurrentSession); if (mCurrentSession != null && mCurrentSession.getUserId() == userId) { return mCurrentSession; } else { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java index b7e3f70a5763..1c6dfe0f5b24 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java @@ -242,6 +242,7 @@ public class Sensor { } @Nullable protected AidlSession getSessionForUser(int userId) { + Slog.d(TAG, "getSessionForUser: mCurrentSession: " + mCurrentSession); if (mCurrentSession != null && mCurrentSession.getUserId() == userId) { return mCurrentSession; } else { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 68e2bd685fac..7106e894ac31 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2764,21 +2764,9 @@ public final class DisplayManagerService extends SystemService { display.setHasContentLocked(hasContent); shouldScheduleTraversal = true; } - if (requestedModeId == 0 && requestedRefreshRate != 0) { - // Scan supported modes returned by display.getInfo() to find a mode with the same - // size as the default display mode but with the specified refresh rate instead. - Display.Mode mode = display.getDisplayInfoLocked().findDefaultModeByRefreshRate( - requestedRefreshRate); - if (mode != null) { - requestedModeId = mode.getModeId(); - } else { - Slog.e(TAG, "Couldn't find a mode for the requestedRefreshRate: " - + requestedRefreshRate + " on Display: " + displayId); - } - } - mDisplayModeDirector.getAppRequestObserver().setAppRequest( - displayId, requestedModeId, requestedMinRefreshRate, requestedMaxRefreshRate); + mDisplayModeDirector.getAppRequestObserver().setAppRequest(displayId, requestedModeId, + requestedRefreshRate, requestedMinRefreshRate, requestedMaxRefreshRate); // TODO(b/202378408) set minimal post-processing only if it's supported once we have a // separate API for disabling on-device processing. diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index d3de71ea1299..cd07f5a399ed 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -159,6 +159,11 @@ public class DisplayManagerFlags { Flags::enablePeakRefreshRatePhysicalLimit ); + private final FlagState mIgnoreAppPreferredRefreshRate = new FlagState( + Flags.FLAG_IGNORE_APP_PREFERRED_REFRESH_RATE_REQUEST, + Flags::ignoreAppPreferredRefreshRateRequest + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -322,6 +327,13 @@ public class DisplayManagerFlags { } /** + * @return Whether to ignore preferredRefreshRate app request or not + */ + public boolean ignoreAppPreferredRefreshRateRequest() { + return mIgnoreAppPreferredRefreshRate.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 3d64fc2b160c..a15a8e8e85d1 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -255,3 +255,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "ignore_app_preferred_refresh_rate_request" + namespace: "display_manager" + description: "Feature flag for DisplayManager to ignore preferred refresh rate app request. It will be handled by SF only." + bug: "330810426" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 3084daeaa382..91bd80eb9037 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -222,7 +222,7 @@ public class DisplayModeDirector { displayManagerFlags.isRefreshRateVotingTelemetryEnabled()); mSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); - mAppRequestObserver = new AppRequestObserver(); + mAppRequestObserver = new AppRequestObserver(displayManagerFlags); mConfigParameterProvider = new DeviceConfigParameterProvider(injector.getDeviceConfig()); mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); mSettingsObserver = new SettingsObserver(context, handler, displayManagerFlags); @@ -1205,17 +1205,32 @@ public class DisplayModeDirector { public final class AppRequestObserver { private final SparseArray<Display.Mode> mAppRequestedModeByDisplay; private final SparseArray<RefreshRateRange> mAppPreferredRefreshRateRangeByDisplay; + private final boolean mIgnorePreferredRefreshRate; - AppRequestObserver() { + AppRequestObserver(DisplayManagerFlags flags) { mAppRequestedModeByDisplay = new SparseArray<>(); mAppPreferredRefreshRateRangeByDisplay = new SparseArray<>(); + mIgnorePreferredRefreshRate = flags.ignoreAppPreferredRefreshRateRequest(); } /** * Sets refresh rates from app request */ - public void setAppRequest(int displayId, int modeId, + public void setAppRequest(int displayId, int modeId, float requestedRefreshRate, float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) { + + if (modeId == 0 && requestedRefreshRate != 0 && !mIgnorePreferredRefreshRate) { + // Scan supported modes returned to find a mode with the same + // size as the default display mode but with the specified refresh rate instead. + Display.Mode mode = findDefaultModeByRefreshRate(displayId, requestedRefreshRate); + if (mode != null) { + modeId = mode.getModeId(); + } else { + Slog.e(TAG, "Couldn't find a mode for the requestedRefreshRate: " + + requestedRefreshRate + " on Display: " + displayId); + } + } + synchronized (mLock) { setAppRequestedModeLocked(displayId, modeId); setAppPreferredRefreshRateRangeLocked(displayId, requestedMinRefreshRateRange, @@ -1223,6 +1238,23 @@ public class DisplayModeDirector { } } + @Nullable + private Display.Mode findDefaultModeByRefreshRate(int displayId, float refreshRate) { + Display.Mode[] modes; + Display.Mode defaultMode; + synchronized (mLock) { + modes = mSupportedModesByDisplay.get(displayId); + defaultMode = mDefaultModeByDisplay.get(displayId); + } + for (int i = 0; i < modes.length; i++) { + if (modes[i].matches(defaultMode.getPhysicalWidth(), + defaultMode.getPhysicalHeight(), refreshRate)) { + return modes[i]; + } + } + return null; + } + private void setAppRequestedModeLocked(int displayId, int modeId) { final Display.Mode requestedMode = findModeByIdLocked(displayId, modeId); if (Objects.equals(requestedMode, mAppRequestedModeByDisplay.get(displayId))) { diff --git a/services/core/java/com/android/server/dreams/Android.bp b/services/core/java/com/android/server/dreams/Android.bp new file mode 100644 index 000000000000..4078a42a09ec --- /dev/null +++ b/services/core/java/com/android/server/dreams/Android.bp @@ -0,0 +1,11 @@ +aconfig_declarations { + name: "dreams_flags", + package: "com.android.server.dreams", + container: "system", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "dreams_flags_lib", + aconfig_declarations: "dreams_flags", +} diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index fc63494d3c99..2def5aed2478 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -45,6 +45,7 @@ import android.database.ContentObserver; import android.hardware.display.AmbientDisplayConfiguration; import android.net.Uri; import android.os.BatteryManager; +import android.os.BatteryManagerInternal; import android.os.Binder; import android.os.Build; import android.os.Handler; @@ -118,6 +119,7 @@ public final class DreamManagerService extends SystemService { private final DreamController mController; private final PowerManager mPowerManager; private final PowerManagerInternal mPowerManagerInternal; + private final BatteryManagerInternal mBatteryManagerInternal; private final PowerManager.WakeLock mDozeWakeLock; private final ActivityTaskManagerInternal mAtmInternal; private final PackageManagerInternal mPmInternal; @@ -186,7 +188,11 @@ public final class DreamManagerService extends SystemService { private final BroadcastReceiver mChargingReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - mIsCharging = (BatteryManager.ACTION_CHARGING.equals(intent.getAction())); + if (Flags.useBatteryChangedBroadcast()) { + mIsCharging = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + } else { + mIsCharging = (BatteryManager.ACTION_CHARGING.equals(intent.getAction())); + } } }; @@ -251,6 +257,12 @@ public final class DreamManagerService extends SystemService { com.android.internal.R.bool.config_keepDreamingWhenUnplugging); mDreamsDisabledByAmbientModeSuppressionConfig = mContext.getResources().getBoolean( com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); + + if (Flags.useBatteryChangedBroadcast()) { + mBatteryManagerInternal = getLocalService(BatteryManagerInternal.class); + } else { + mBatteryManagerInternal = null; + } } @Override @@ -279,9 +291,15 @@ public final class DreamManagerService extends SystemService { mContext.registerReceiver( mDockStateReceiver, new IntentFilter(Intent.ACTION_DOCK_EVENT)); + IntentFilter chargingIntentFilter = new IntentFilter(); - chargingIntentFilter.addAction(BatteryManager.ACTION_CHARGING); - chargingIntentFilter.addAction(BatteryManager.ACTION_DISCHARGING); + if (Flags.useBatteryChangedBroadcast()) { + chargingIntentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); + chargingIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + } else { + chargingIntentFilter.addAction(BatteryManager.ACTION_CHARGING); + chargingIntentFilter.addAction(BatteryManager.ACTION_DISCHARGING); + } mContext.registerReceiver(mChargingReceiver, chargingIntentFilter); mSettingsObserver = new SettingsObserver(mHandler); diff --git a/services/core/java/com/android/server/dreams/flags.aconfig b/services/core/java/com/android/server/dreams/flags.aconfig new file mode 100644 index 000000000000..5d35ebd629cd --- /dev/null +++ b/services/core/java/com/android/server/dreams/flags.aconfig @@ -0,0 +1,12 @@ +package: "com.android.server.dreams" +container: "system" + +flag { + name: "use_battery_changed_broadcast" + namespace: "communal" + description: "Use ACTION_BATTERY_CHANGED broadcast to track charging state" + bug: "329125239" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/input/InputShellCommand.java b/services/core/java/com/android/server/input/InputShellCommand.java index 138186ba6191..4c5a3c27e156 100644 --- a/services/core/java/com/android/server/input/InputShellCommand.java +++ b/services/core/java/com/android/server/input/InputShellCommand.java @@ -333,8 +333,8 @@ public class InputShellCommand extends ShellCommand { out.println(); out.println("The commands and default sources are:"); out.println(" text <string> (Default: keyboard)"); - out.println(" keyevent [--longpress|--doubletap|--async" - + "|--delay <duration between keycodes in ms>]" + out.println(" keyevent [--longpress|--duration <duration to hold key down in ms>]" + + " [--doubletap] [--async] [--delay <duration between keycodes in ms>]" + " <key code number or name> ..." + " (Default: keyboard)"); out.println(" tap <x> <y> (Default: touchscreen)"); @@ -402,6 +402,7 @@ public class InputShellCommand extends ShellCommand { boolean async = false; boolean doubleTap = false; long delayMs = 0; + long durationMs = 0; String arg = getNextArgRequired(); do { @@ -411,9 +412,21 @@ public class InputShellCommand extends ShellCommand { doubleTap = (doubleTap || arg.equals("--doubletap")); if (arg.equals("--delay")) { delayMs = Long.parseLong(getNextArgRequired()); + } else if (arg.equals("--duration")) { + durationMs = Long.parseLong(getNextArgRequired()); } } while ((arg = getNextArg()) != null); + if (durationMs > 0 && longPress) { + getErrPrintWriter().println( + "--duration and --longpress cannot be used at the same time."); + throw new IllegalArgumentException( + "keyevent args should only contain either durationMs or longPress"); + } + if (longPress) { + durationMs = ViewConfiguration.getLongPressTimeout(); + } + boolean firstInput = true; do { if (!firstInput && delayMs > 0) { @@ -422,16 +435,17 @@ public class InputShellCommand extends ShellCommand { firstInput = false; final int keyCode = KeyEvent.keyCodeFromString(arg); - sendKeyEvent(inputSource, keyCode, longPress, displayId, async); + sendKeyEvent(inputSource, keyCode, durationMs, displayId, async); if (doubleTap) { sleep(ViewConfiguration.getDoubleTapMinTime()); - sendKeyEvent(inputSource, keyCode, longPress, displayId, async); + sendKeyEvent(inputSource, keyCode, durationMs, displayId, async); } } while ((arg = getNextArg()) != null); } private void sendKeyEvent( - int inputSource, int keyCode, boolean longPress, int displayId, boolean async) { + int inputSource, int keyCode, long durationMs, int displayId, + boolean async) { final long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0 /* repeatCount */, @@ -440,13 +454,23 @@ public class InputShellCommand extends ShellCommand { event.setDisplayId(displayId); injectKeyEvent(event, async); - if (longPress) { - sleep(ViewConfiguration.getLongPressTimeout()); - // Some long press behavior would check the event time, we set a new event time here. - final long nextEventTime = now + ViewConfiguration.getLongPressTimeout(); - KeyEvent longPressEvent = KeyEvent.changeTimeRepeat( - event, nextEventTime, 1 /* repeatCount */, KeyEvent.FLAG_LONG_PRESS); - injectKeyEvent(longPressEvent, async); + long firstSleepDurationMs = Math.min(durationMs, ViewConfiguration.getLongPressTimeout()); + if (firstSleepDurationMs > 0) { + sleep(firstSleepDurationMs); + // Send FLAG_LONG_PRESS right after `longPressTimeout`, and resume sleep if needed. + if (durationMs >= ViewConfiguration.getLongPressTimeout()) { + // Some long press behavior would check the event time, we set a new event time + // here. + final long nextEventTime = now + ViewConfiguration.getLongPressTimeout(); + KeyEvent longPressEvent = KeyEvent.changeTimeRepeat(event, nextEventTime, + 1 /* repeatCount */, KeyEvent.FLAG_LONG_PRESS); + injectKeyEvent(longPressEvent, async); + + long secondSleepDurationMs = durationMs - firstSleepDurationMs; + if (secondSleepDurationMs > 0) { + sleep(secondSleepDurationMs); + } + } } injectKeyEvent(KeyEvent.changeAction(event, KeyEvent.ACTION_UP), async); } diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING index 4fc1a17032e9..ad6b0ca71527 100644 --- a/services/core/java/com/android/server/net/TEST_MAPPING +++ b/services/core/java/com/android/server/net/TEST_MAPPING @@ -1,7 +1,7 @@ { "presubmit-large": [ { - "name": "CtsHostsideNetworkTests", + "name": "CtsHostsideNetworkPolicyTests", "options": [ { "exclude-annotation": "androidx.test.filters.FlakyTest" diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 96f32f30877c..bf49671e2d82 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -16,7 +16,7 @@ package com.android.server.notification; -import static android.app.Flags.updateRankingTime; +import static android.app.Flags.sortSectionByTime; import static android.app.Notification.FLAG_INSISTENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; import static android.app.NotificationManager.IMPORTANCE_MIN; @@ -497,7 +497,7 @@ public final class NotificationAttentionHelper { Slog.v(TAG, "INTERRUPTIVENESS: " + record.getKey() + " is interruptive: alerted"); } - if (updateRankingTime()) { + if (sortSectionByTime()) { if (buzz || beep) { record.resetRankingTime(); } @@ -1528,7 +1528,7 @@ public final class NotificationAttentionHelper { // recent conversation if (record.isConversation() - && record.getNotification().when > mLastAvalancheTriggerTimestamp) { + && record.getNotification().getWhen() > mLastAvalancheTriggerTimestamp) { return true; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9d4ab11d7001..ca6ae63e74fc 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -25,6 +25,7 @@ import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.app.Flags.lifetimeExtensionRefactor; +import static android.app.Flags.sortSectionByTime; import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; import static android.app.Notification.EXTRA_LARGE_ICON_BIG; @@ -8593,7 +8594,7 @@ public class NotificationManagerService extends SystemService { r.isUpdate = true; final boolean isInterruptive = isVisuallyInterruptive(old, r); r.setTextChanged(isInterruptive); - if (android.app.Flags.updateRankingTime()) { + if (sortSectionByTime()) { if (isInterruptive) { r.resetRankingTime(); } @@ -8738,7 +8739,7 @@ public class NotificationManagerService extends SystemService { return false; } - if (android.app.Flags.updateRankingTime()) { + if (sortSectionByTime()) { // Ignore visual interruptions from FGS/UIJs because users // consider them one 'session'. Count them for everything else. if (r.getSbn().getNotification().isFgsOrUij()) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 38c95f771601..0c6a6c84b69b 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -18,7 +18,7 @@ package com.android.server.notification; import static android.app.Flags.restrictAudioAttributesAlarm; import static android.app.Flags.restrictAudioAttributesCall; import static android.app.Flags.restrictAudioAttributesMedia; -import static android.app.Flags.updateRankingTime; +import static android.app.Flags.sortSectionByTime; import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; @@ -580,7 +580,7 @@ public final class NotificationRecord { pw.println(prefix + "deleteIntent=" + notification.deleteIntent); pw.println(prefix + "number=" + notification.number); pw.println(prefix + "groupAlertBehavior=" + notification.getGroupAlertBehavior()); - pw.println(prefix + "when=" + notification.when); + pw.println(prefix + "when=" + notification.when + "/" + notification.getWhen()); pw.print(prefix + "tickerText="); if (!TextUtils.isEmpty(notification.tickerText)) { @@ -1092,9 +1092,9 @@ public final class NotificationRecord { private long calculateRankingTimeMs(long previousRankingTimeMs) { Notification n = getNotification(); // Take developer provided 'when', unless it's in the future. - if (updateRankingTime()) { - if (n.hasAppProvidedWhen() && n.when <= getSbn().getPostTime()){ - return n.when; + if (sortSectionByTime()) { + if (n.hasAppProvidedWhen() && n.getWhen() <= getSbn().getPostTime()){ + return n.getWhen(); } } else { if (n.when != 0 && n.when <= getSbn().getPostTime()) { @@ -1211,7 +1211,7 @@ public final class NotificationRecord { } public void resetRankingTime() { - if (updateRankingTime()) { + if (sortSectionByTime()) { mRankingTimeMs = calculateRankingTimeMs(getSbn().getPostTime()); } } diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java index 9a6ea2c2aeb8..65ef53f1df14 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java +++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java @@ -542,7 +542,7 @@ interface NotificationRecordLogger { this.is_locked = p.r.isLocked(); this.age_in_minutes = NotificationRecordLogger.getAgeInMinutes( - p.r.getSbn().getPostTime(), p.r.getSbn().getNotification().when); + p.r.getSbn().getPostTime(), p.r.getSbn().getNotification().getWhen()); } } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 1f2ad07ea684..309e9450a01d 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -26,6 +26,7 @@ import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; +import static android.os.UserHandle.USER_SYSTEM; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES; @@ -139,6 +140,8 @@ public class PreferencesHelper implements RankingConfig { private static final String ATT_VERSION = "version"; private static final String ATT_NAME = "name"; private static final String ATT_UID = "uid"; + + private static final String ATT_USERID = "userid"; private static final String ATT_ID = "id"; private static final String ATT_ALLOW_BUBBLE = "allow_bubble"; private static final String ATT_PRIORITY = "priority"; @@ -268,7 +271,7 @@ public class PreferencesHelper implements RankingConfig { } if (type == XmlPullParser.START_TAG) { if (TAG_STATUS_ICONS.equals(tag)) { - if (forRestore && userId != UserHandle.USER_SYSTEM) { + if (forRestore && userId != USER_SYSTEM) { continue; } mHideSilentStatusBarIcons = parser.getAttributeBoolean(null, @@ -311,8 +314,16 @@ public class PreferencesHelper implements RankingConfig { : parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE); int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); + // when data is loaded from disk it's loaded as USER_ALL, but restored data that + // is pending app install needs the user id that the data was restored to + int fixedUserId = userId; + if (Flags.persistIncompleteRestoreData()) { + if (!forRestore && uid == UNKNOWN_UID) { + fixedUserId = parser.getAttributeInt(null, ATT_USERID, USER_SYSTEM); + } + } PackagePreferences r = getOrCreatePackagePreferencesLocked( - name, userId, uid, + name, fixedUserId, uid, appImportance, parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY), parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY), @@ -504,6 +515,9 @@ public class PreferencesHelper implements RankingConfig { } if (r.uid == UNKNOWN_UID) { + if (Flags.persistIncompleteRestoreData()) { + r.userId = userId; + } mRestoredWithoutUids.put(unrestoredPackageKey(pkg, userId), r); } else { mPackagePreferences.put(key, r); @@ -674,6 +688,7 @@ public class PreferencesHelper implements RankingConfig { if (Flags.persistIncompleteRestoreData() && r.uid == UNKNOWN_UID) { out.attributeLong(null, ATT_CREATION_TIME, r.creationTime); + out.attributeInt(null, ATT_USERID, r.userId); } if (!forBackup) { @@ -2947,6 +2962,8 @@ public class PreferencesHelper implements RankingConfig { boolean migrateToPm = false; long creationTime; + @UserIdInt int userId; + Delegate delegate = null; ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>(); diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 77568015fe79..03dd9351efc7 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -147,11 +147,9 @@ public class RankingHelper { if (sortSectionByTime()) { final String groupKey = record.getGroupKey(); NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey); - // summaries are mostly hidden in systemui - if there is a child notification - // with better information, use its rank - if (existingProxy == null - || (existingProxy.getNotification().isGroupSummary() - && !existingProxy.getNotification().hasAppProvidedWhen())) { + // summaries are mostly hidden in systemui - if there is a child notification, + // use its rank + if (existingProxy == null || existingProxy.getNotification().isGroupSummary()) { mProxyByGroupTmp.put(groupKey, record); } } else { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 474768935bbd..143bc5cb20ff 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1725,7 +1725,28 @@ public class ZenModeHelper { synchronized (mConfigLock) { if (policy == null || mConfig == null) return; final ZenModeConfig newConfig = mConfig.copy(); - newConfig.applyNotificationPolicy(policy); + if (Flags.modesApi() && !Flags.modesUi()) { + // Fix for b/337193321 -- propagate changes to notificationPolicy to rules where + // the user cannot edit zen policy to emulate the previous "inheritance". + ZenPolicy previousPolicy = ZenAdapters.notificationPolicyToZenPolicy( + newConfig.toNotificationPolicy()); + ZenPolicy newPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); + + newConfig.applyNotificationPolicy(policy); + + if (!previousPolicy.equals(newPolicy)) { + for (ZenRule rule : newConfig.automaticRules.values()) { + if (!SystemZenRules.isSystemOwnedRule(rule) + && rule.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS + && (rule.zenPolicy == null || rule.zenPolicy.equals(previousPolicy) + || rule.zenPolicy.equals(getDefaultZenPolicy()))) { + rule.zenPolicy = newPolicy; + } + } + } + } else { + newConfig.applyNotificationPolicy(policy); + } setConfigLocked(newConfig, null, origin, "setNotificationPolicy", callingUid); } } diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index c8fd7e47d80a..8a853287738b 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -19,6 +19,7 @@ package com.android.server.os; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.AppOpsManager; @@ -68,6 +69,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.OptionalInt; @@ -335,14 +337,22 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } static class Injector { + class RoleManagerWrapper { + List<String> getRoleHolders(@NonNull String roleName) { + return mContext.getSystemService(RoleManager.class).getRoleHolders(roleName); + } + } + Context mContext; ArraySet<String> mAllowlistedPackages; AtomicFile mMappingFile; + RoleManagerWrapper mRoleManagerWrapper; Injector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile) { mContext = context; mAllowlistedPackages = allowlistedPackages; mMappingFile = mappingFile; + mRoleManagerWrapper = new RoleManagerWrapper(); } Context getContext() { @@ -368,6 +378,10 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { void setSystemProperty(String key, String value) { SystemProperties.set(key, value); } + + RoleManagerWrapper getRoleManagerWrapper() { + return mRoleManagerWrapper; + } } BugreportManagerServiceImpl(Context context) { @@ -546,7 +560,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { if (!allowlisted) { final long token = Binder.clearCallingIdentity(); try { - allowlisted = mContext.getSystemService(RoleManager.class).getRoleHolders( + allowlisted = mInjector.getRoleManagerWrapper().getRoleHolders( ROLE_SYSTEM_AUTOMOTIVE_PROJECTION).contains(callingPackage); } finally { Binder.restoreCallingIdentity(token); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 0f4e4821dee8..ae485ede1bec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -3984,6 +3984,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService // packageName -> list of components to send broadcasts now final ArrayMap<String, ArrayList<String>> sendNowBroadcasts = new ArrayMap<>(targetSize); + final List<PackageMetrics.ComponentStateMetrics> componentStateMetricsList = + new ArrayList<PackageMetrics.ComponentStateMetrics>(); synchronized (mLock) { Computer computer = snapshotComputer(); boolean scheduleBroadcastMessage = false; @@ -3997,11 +3999,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService // update enabled settings final ComponentEnabledSetting setting = settings.get(i); final String packageName = setting.getPackageName(); - if (!setEnabledSettingInternalLocked(computer, pkgSettings.get(packageName), - setting, userId, callingPackage)) { + final PackageSetting packageSetting = pkgSettings.get(packageName); + final PackageMetrics.ComponentStateMetrics componentStateMetrics = + new PackageMetrics.ComponentStateMetrics(setting, + UserHandle.getUid(userId, packageSetting.getAppId()), + packageSetting.getEnabled(userId)); + if (!setEnabledSettingInternalLocked(computer, packageSetting, setting, userId, + callingPackage)) { continue; } anyChanged = true; + componentStateMetricsList.add(componentStateMetrics); if ((setting.getEnabledFlags() & PackageManager.SYNCHRONOUS) != 0) { isSynchronous = true; @@ -4029,6 +4037,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService return; } + // Log the metrics when the component state is changed. + PackageMetrics.reportComponentStateChanged(computer, componentStateMetricsList, userId); + if (isSynchronous) { flushPackageRestrictionsAsUserInternalLocked(userId); } else { diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java index a0b6897a080e..20598f91a51d 100644 --- a/services/core/java/com/android/server/pm/PackageMetrics.java +++ b/services/core/java/com/android/server/pm/PackageMetrics.java @@ -19,12 +19,21 @@ package com.android.server.pm; import static android.os.Process.INVALID_UID; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.admin.SecurityLog; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.content.pm.Flags; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.parsing.ApkLiteParseUtils; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Pair; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.util.FrameworkStatsLog; @@ -41,12 +50,14 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; /** * Metrics class for reporting stats to logging infrastructures like statsd */ final class PackageMetrics { + private static final String TAG = "PackageMetrics"; public static final int STEP_PREPARE = 1; public static final int STEP_SCAN = 2; public static final int STEP_RECONCILE = 3; @@ -344,4 +355,76 @@ final class PackageMetrics { SecurityLog.writeEvent(SecurityLog.TAG_PACKAGE_UNINSTALLED, packageName, versionCode, userId); } + + public static class ComponentStateMetrics { + public int mUid; + public int mComponentOldState; + public int mComponentNewState; + public boolean mIsForWholeApp; + @NonNull private String mPackageName; + @Nullable private String mClassName; + + ComponentStateMetrics(@NonNull PackageManager.ComponentEnabledSetting setting, int uid, + int componentOldState) { + mUid = uid; + mComponentOldState = componentOldState; + mComponentNewState = setting.getEnabledState(); + mIsForWholeApp = !setting.isComponent(); + mPackageName = setting.getPackageName(); + mClassName = setting.getClassName(); + } + + public boolean isSameComponent(ActivityInfo activityInfo) { + if (activityInfo == null) { + return false; + } + return mIsForWholeApp ? TextUtils.equals(activityInfo.packageName, mPackageName) + : activityInfo.getComponentName().equals( + new ComponentName(mPackageName, mClassName)); + } + } + + public static void reportComponentStateChanged(@NonNull Computer computer, + List<ComponentStateMetrics> componentStateMetricsList, @UserIdInt int userId) { + if (!Flags.componentStateChangedMetrics()) { + return; + } + if (componentStateMetricsList == null || componentStateMetricsList.isEmpty()) { + Slog.d(TAG, "Fail to report component state due to metrics is empty"); + return; + } + boolean isLauncher = false; + final List<ResolveInfo> resolveInfosForLauncher = getHomeActivitiesResolveInfoAsUser( + computer, userId); + final int resolveInfosForLauncherSize = + resolveInfosForLauncher != null ? resolveInfosForLauncher.size() : 0; + final int metricsSize = componentStateMetricsList.size(); + for (int i = 0; i < metricsSize; i++) { + final ComponentStateMetrics componentStateMetrics = componentStateMetricsList.get(i); + for (int j = 0; j < resolveInfosForLauncherSize; j++) { + ResolveInfo resolveInfo = resolveInfosForLauncher.get(j); + if (componentStateMetrics.isSameComponent(resolveInfo.activityInfo)) { + isLauncher = true; + break; + } + } + reportComponentStateChanged(componentStateMetrics.mUid, + componentStateMetrics.mComponentOldState, + componentStateMetrics.mComponentNewState, + isLauncher, + componentStateMetrics.mIsForWholeApp); + } + } + + private static void reportComponentStateChanged(int uid, int componentOldState, + int componentNewState, boolean isLauncher, boolean isForWholeApp) { + FrameworkStatsLog.write(FrameworkStatsLog.COMPONENT_STATE_CHANGED_REPORTED, + uid, componentOldState, componentNewState, isLauncher, isForWholeApp); + } + + private static List<ResolveInfo> getHomeActivitiesResolveInfoAsUser(@NonNull Computer computer, + @UserIdInt int userId) { + return computer.queryIntentActivitiesInternal(computer.getHomeIntent(), /* resolvedType */ + null, /* flags */ 0, userId); + } } diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java index 5e2467304b1f..00582bfa90c5 100644 --- a/services/core/java/com/android/server/pm/ShortcutLauncher.java +++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java @@ -46,7 +46,7 @@ import java.util.List; /** * Launcher information used by {@link ShortcutService}. * - * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. + * All methods should be guarded by {@code ShortcutPackageItem#mPackageItemLock}. */ class ShortcutLauncher extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; @@ -66,7 +66,7 @@ class ShortcutLauncher extends ShortcutPackageItem { /** * Package name -> IDs. */ - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private final ArrayMap<UserPackage, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>(); private ShortcutLauncher(@NonNull ShortcutUser shortcutUser, @@ -99,7 +99,7 @@ class ShortcutLauncher extends ShortcutPackageItem { */ private void onRestoreBlocked() { final ArrayList<UserPackage> pinnedPackages; - synchronized (mLock) { + synchronized (mPackageItemLock) { pinnedPackages = new ArrayList<>(mPinnedShortcuts.keySet()); mPinnedShortcuts.clear(); } @@ -138,7 +138,7 @@ class ShortcutLauncher extends ShortcutPackageItem { final int idSize = ids.size(); if (idSize == 0) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mPinnedShortcuts.remove(up); } } else { @@ -165,7 +165,7 @@ class ShortcutLauncher extends ShortcutPackageItem { floatingSet.add(id); } } - synchronized (mLock) { + synchronized (mPackageItemLock) { final ArraySet<String> prevSet = mPinnedShortcuts.get(up); if (prevSet != null) { for (String id : floatingSet) { @@ -187,7 +187,7 @@ class ShortcutLauncher extends ShortcutPackageItem { @Nullable public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName, @UserIdInt int packageUserId) { - synchronized (mLock) { + synchronized (mPackageItemLock) { final ArraySet<String> pinnedShortcuts = mPinnedShortcuts.get( UserPackage.of(packageUserId, packageName)); return pinnedShortcuts == null ? null : new ArraySet<>(pinnedShortcuts); @@ -198,7 +198,7 @@ class ShortcutLauncher extends ShortcutPackageItem { * Return true if the given shortcut is pinned by this launcher.<code></code> */ public boolean hasPinned(ShortcutInfo shortcut) { - synchronized (mLock) { + synchronized (mPackageItemLock) { final ArraySet<String> pinned = mPinnedShortcuts.get( UserPackage.of(shortcut.getUserId(), shortcut.getPackage())); return (pinned != null) && pinned.contains(shortcut.getId()); @@ -211,7 +211,7 @@ class ShortcutLauncher extends ShortcutPackageItem { public void addPinnedShortcut(@NonNull String packageName, @UserIdInt int packageUserId, String id, boolean forPinRequest) { final ArrayList<String> pinnedList; - synchronized (mLock) { + synchronized (mPackageItemLock) { final ArraySet<String> pinnedSet = mPinnedShortcuts.get( UserPackage.of(packageUserId, packageName)); if (pinnedSet != null) { @@ -227,7 +227,7 @@ class ShortcutLauncher extends ShortcutPackageItem { } boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) { - synchronized (mLock) { + synchronized (mPackageItemLock) { return mPinnedShortcuts.remove(UserPackage.of(packageUserId, packageName)) != null; } } @@ -253,7 +253,7 @@ class ShortcutLauncher extends ShortcutPackageItem { return; } final ArrayMap<UserPackage, ArraySet<String>> pinnedShortcuts; - synchronized (mLock) { + synchronized (mPackageItemLock) { pinnedShortcuts = new ArrayMap<>(mPinnedShortcuts); } final int size = pinnedShortcuts.size(); @@ -366,7 +366,7 @@ class ShortcutLauncher extends ShortcutPackageItem { : ShortcutService.parseIntAttribute(parser, ATTR_PACKAGE_USER_ID, ownerUserId); ids = new ArraySet<>(); - synchronized (ret.mLock) { + synchronized (ret.mPackageItemLock) { ret.mPinnedShortcuts.put( UserPackage.of(packageUserId, packageName), ids); } @@ -407,7 +407,7 @@ class ShortcutLauncher extends ShortcutPackageItem { pw.println(); final ArrayMap<UserPackage, ArraySet<String>> pinnedShortcuts; - synchronized (mLock) { + synchronized (mPackageItemLock) { pinnedShortcuts = new ArrayMap<>(mPinnedShortcuts); } final int size = pinnedShortcuts.size(); diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 449e9ab8c5ec..c929c1f2fcfc 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -163,20 +163,20 @@ class ShortcutPackage extends ShortcutPackageItem { /** * An in-memory copy of shortcuts for this package that was loaded from xml, keyed on IDs. */ - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private final ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); /** * A temporary copy of shortcuts that are to be cleared once persisted into AppSearch, keyed on * IDs. */ - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private final ArrayMap<String, ShortcutInfo> mTransientShortcuts = new ArrayMap<>(0); /** * All the share targets from the package */ - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0); /** @@ -193,10 +193,10 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private long mLastReportedTime; - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private boolean mIsAppSearchSchemaUpToDate; private ShortcutPackage(ShortcutUser shortcutUser, @@ -233,7 +233,7 @@ class ShortcutPackage extends ShortcutPackageItem { } public int getShortcutCount() { - synchronized (mLock) { + synchronized (mPackageItemLock) { return mShortcuts.size(); } } @@ -276,7 +276,7 @@ class ShortcutPackage extends ShortcutPackageItem { @Nullable public ShortcutInfo findShortcutById(@Nullable final String id) { if (id == null) return null; - synchronized (mLock) { + synchronized (mPackageItemLock) { return mShortcuts.get(id); } } @@ -354,7 +354,7 @@ class ShortcutPackage extends ShortcutPackageItem { */ private ShortcutInfo forceDeleteShortcutInner(@NonNull String id) { final ShortcutInfo shortcut; - synchronized (mLock) { + synchronized (mPackageItemLock) { shortcut = mShortcuts.remove(id); if (shortcut != null) { removeIcon(shortcut); @@ -409,7 +409,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (newShortcut.isExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER)) { if (isAppSearchEnabled()) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mTransientShortcuts.put(newShortcut.getId(), newShortcut); } } @@ -482,7 +482,7 @@ class ShortcutPackage extends ShortcutPackageItem { if (newShortcut.isExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER)) { if (isAppSearchEnabled()) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mTransientShortcuts.put(newShortcut.getId(), newShortcut); } } @@ -506,7 +506,7 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutService service = mShortcutUser.mService; // Ensure the total number of shortcuts doesn't exceed the hard limit per app. final int maxShortcutPerApp = service.getMaxAppShortcuts(); - synchronized (mLock) { + synchronized (mPackageItemLock) { final List<ShortcutInfo> appShortcuts = mShortcuts.values().stream().filter(si -> !si.isPinned()).collect(Collectors.toList()); if (appShortcuts.size() >= maxShortcutPerApp) { @@ -555,7 +555,7 @@ class ShortcutPackage extends ShortcutPackageItem { public List<ShortcutInfo> deleteAllDynamicShortcuts() { final long now = mShortcutUser.mService.injectCurrentTimeMillis(); boolean changed = false; - synchronized (mLock) { + synchronized (mPackageItemLock) { for (int i = mShortcuts.size() - 1; i >= 0; i--) { ShortcutInfo si = mShortcuts.valueAt(i); if (si.isDynamic() && si.isVisibleToPublisher()) { @@ -914,7 +914,7 @@ class ShortcutPackage extends ShortcutPackageItem { List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets( @NonNull final IntentFilter filter, @Nullable final String pkgName) { - synchronized (mLock) { + synchronized (mPackageItemLock) { final List<ShareTargetInfo> matchedTargets = new ArrayList<>(); for (int i = 0; i < mShareTargets.size(); i++) { final ShareTargetInfo target = mShareTargets.get(i); @@ -967,7 +967,7 @@ class ShortcutPackage extends ShortcutPackageItem { } public boolean hasShareTargets() { - synchronized (mLock) { + synchronized (mPackageItemLock) { return !mShareTargets.isEmpty(); } } @@ -978,7 +978,7 @@ class ShortcutPackage extends ShortcutPackageItem { * the app's Xml resource. */ int getSharingShortcutCount() { - synchronized (mLock) { + synchronized (mPackageItemLock) { if (mShareTargets.isEmpty()) { return 0; } @@ -1017,7 +1017,7 @@ class ShortcutPackage extends ShortcutPackageItem { /** * Return the filenames (excluding path names) of icon bitmap files from this package. */ - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") private ArraySet<String> getUsedBitmapFilesLocked() { final ArraySet<String> usedFiles = new ArraySet<>(1); forEachShortcut(si -> { @@ -1029,7 +1029,7 @@ class ShortcutPackage extends ShortcutPackageItem { } public void cleanupDanglingBitmapFiles(@NonNull File path) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mShortcutBitmapSaver.waitForAllSavesLocked(); final ArraySet<String> usedFiles = getUsedBitmapFilesLocked(); @@ -1136,7 +1136,7 @@ class ShortcutPackage extends ShortcutPackageItem { // Now prepare to publish manifest shortcuts. List<ShortcutInfo> newManifestShortcutList = null; int shareTargetSize = 0; - synchronized (mLock) { + synchronized (mPackageItemLock) { try { shareTargetSize = mShareTargets.size(); newManifestShortcutList = ShortcutParser.parseShortcuts(mShortcutUser.mService, @@ -1680,7 +1680,7 @@ class ShortcutPackage extends ShortcutPackageItem { void reportShortcutUsed(@NonNull final UsageStatsManagerInternal usageStatsManagerInternal, @NonNull final String shortcutId) { - synchronized (mLock) { + synchronized (mPackageItemLock) { final long currentTS = SystemClock.elapsedRealtime(); final ShortcutService s = mShortcutUser.mService; if (currentTS - mLastReportedTime > s.mSaveDelayMillis) { @@ -1757,7 +1757,7 @@ class ShortcutPackage extends ShortcutPackageItem { pw.println(")"); pw.println(); - synchronized (mLock) { + synchronized (mPackageItemLock) { mShortcutBitmapSaver.dumpLocked(pw, " "); } } @@ -1827,7 +1827,7 @@ class ShortcutPackage extends ShortcutPackageItem { @Override public void saveToXml(@NonNull TypedXmlSerializer out, boolean forBackup) throws IOException, XmlPullParserException { - synchronized (mLock) { + synchronized (mPackageItemLock) { final int size = mShortcuts.size(); final int shareTargetSize = mShareTargets.size(); @@ -2037,7 +2037,7 @@ class ShortcutPackage extends ShortcutPackageItem { final ShortcutPackage ret = new ShortcutPackage(shortcutUser, shortcutUser.getUserId(), packageName); - synchronized (ret.mLock) { + synchronized (ret.mPackageItemLock) { ret.mIsAppSearchSchemaUpToDate = ShortcutService.parseIntAttribute( parser, ATTR_SCHEMA_VERSON, 0) == AppSearchShortcutInfo.SCHEMA_VERSION; @@ -2283,7 +2283,7 @@ class ShortcutPackage extends ShortcutPackageItem { @VisibleForTesting List<ShareTargetInfo> getAllShareTargetsForTest() { - synchronized (mLock) { + synchronized (mPackageItemLock) { return new ArrayList<>(mShareTargets); } } @@ -2404,7 +2404,7 @@ class ShortcutPackage extends ShortcutPackageItem { @NonNull final Consumer<ShortcutInfo> transform) { Objects.requireNonNull(id); Objects.requireNonNull(transform); - synchronized (mLock) { + synchronized (mPackageItemLock) { if (shortcut != null) { transform.accept(shortcut); } @@ -2424,7 +2424,7 @@ class ShortcutPackage extends ShortcutPackageItem { private void saveShortcut(@NonNull final Collection<ShortcutInfo> shortcuts) { Objects.requireNonNull(shortcuts); - synchronized (mLock) { + synchronized (mPackageItemLock) { for (ShortcutInfo si : shortcuts) { mShortcuts.put(si.getId(), si); } @@ -2433,7 +2433,7 @@ class ShortcutPackage extends ShortcutPackageItem { @Nullable List<ShortcutInfo> findAll(@NonNull final Collection<String> ids) { - synchronized (mLock) { + synchronized (mPackageItemLock) { return ids.stream().map(mShortcuts::get) .filter(Objects::nonNull).collect(Collectors.toList()); } @@ -2455,7 +2455,7 @@ class ShortcutPackage extends ShortcutPackageItem { private void forEachShortcutStopWhen( @NonNull final Function<ShortcutInfo, Boolean> cb) { - synchronized (mLock) { + synchronized (mPackageItemLock) { for (int i = mShortcuts.size() - 1; i >= 0; i--) { final ShortcutInfo si = mShortcuts.valueAt(i); if (cb.apply(si)) { @@ -2600,7 +2600,7 @@ class ShortcutPackage extends ShortcutPackageItem { }))); } - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") @Override void scheduleSaveToAppSearchLocked() { final Map<String, ShortcutInfo> copy = new ArrayMap<>(mShortcuts); @@ -2684,7 +2684,7 @@ class ShortcutPackage extends ShortcutPackageItem { .penaltyLog() // TODO: change this to penaltyDeath to fix the call-site .build()); future = mShortcutUser.getAppSearch(searchContext); - synchronized (mLock) { + synchronized (mPackageItemLock) { if (!mIsAppSearchSchemaUpToDate) { future = future.thenCompose(this::setupSchema); } diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java index 12115afcbeec..dfd2e08e9ba9 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java @@ -39,7 +39,7 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; /** - * All methods should be either guarded by {@code #mShortcutUser.mService.mLock} or {@code #mLock}. + * All methods should be either guarded by {@code #mPackageItemLock}. */ abstract class ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; @@ -52,10 +52,10 @@ abstract class ShortcutPackageItem { protected ShortcutUser mShortcutUser; - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") protected final ShortcutBitmapSaver mShortcutBitmapSaver; - protected final Object mLock = new Object(); + protected final Object mPackageItemLock = new Object(); protected ShortcutPackageItem(@NonNull ShortcutUser shortcutUser, int packageUserId, @NonNull String packageName, @@ -157,7 +157,7 @@ abstract class ShortcutPackageItem { public abstract void saveToXml(@NonNull TypedXmlSerializer out, boolean forBackup) throws IOException, XmlPullParserException; - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") public void saveToFileLocked(File path, boolean forBackup) { try (ResilientAtomicFile file = getResilientFile(path)) { FileOutputStream os = null; @@ -187,7 +187,7 @@ abstract class ShortcutPackageItem { } } - @GuardedBy("mLock") + @GuardedBy("mPackageItemLock") void scheduleSaveToAppSearchLocked() { } @@ -219,7 +219,7 @@ abstract class ShortcutPackageItem { if (ShortcutService.DEBUG || ShortcutService.DEBUG_REBOOT) { Slog.d(TAG, "Saving package item " + getPackageName() + " to " + path); } - synchronized (mLock) { + synchronized (mPackageItemLock) { path.getParentFile().mkdirs(); // TODO: Since we are persisting shortcuts into AppSearch, we should read from/write to // AppSearch as opposed to maintaining a separate XML file. @@ -229,14 +229,14 @@ abstract class ShortcutPackageItem { } public boolean waitForBitmapSaves() { - synchronized (mLock) { + synchronized (mPackageItemLock) { return mShortcutBitmapSaver.waitForAllSavesLocked(); } } public void saveBitmap(ShortcutInfo shortcut, int maxDimension, Bitmap.CompressFormat format, int quality) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mShortcutBitmapSaver.saveBitmapLocked(shortcut, maxDimension, format, quality); } } @@ -246,19 +246,19 @@ abstract class ShortcutPackageItem { */ @Nullable public String getBitmapPathMayWait(ShortcutInfo shortcut) { - synchronized (mLock) { + synchronized (mPackageItemLock) { return mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcut); } } public void removeIcon(ShortcutInfo shortcut) { - synchronized (mLock) { + synchronized (mPackageItemLock) { mShortcutBitmapSaver.removeIcon(shortcut); } } void removeShortcutPackageItem() { - synchronized (mLock) { + synchronized (mPackageItemLock) { getResilientFile(getShortcutPackageItemFile()).delete(); } } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index fe9c3f217841..3f5ec0678f85 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -295,7 +295,7 @@ public class ShortcutService extends IShortcutService.Stub { final Context mContext; - private final Object mLock = new Object(); + private final Object mServiceLock = new Object(); private final Object mNonPersistentUsersLock = new Object(); private final Object mWtfLock = new Object(); @@ -333,7 +333,7 @@ public class ShortcutService extends IShortcutService.Stub { /** * User ID -> UserShortcuts */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private final SparseArray<ShortcutUser> mUsers = new SparseArray<>(); /** @@ -388,13 +388,13 @@ public class ShortcutService extends IShortcutService.Stub { private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor; private final ShortcutDumpFiles mShortcutDumpFiles; - @GuardedBy("mLock") + @GuardedBy("mServiceLock") final SparseIntArray mUidState = new SparseIntArray(); - @GuardedBy("mLock") + @GuardedBy("mServiceLock") final SparseLongArray mUidLastForegroundElapsedTime = new SparseLongArray(); - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private List<Integer> mDirtyUserIds = new ArrayList<>(); private final AtomicBoolean mBootCompleted = new AtomicBoolean(); @@ -473,7 +473,7 @@ public class ShortcutService extends IShortcutService.Stub { @GuardedBy("mWtfLock") private Exception mLastWtfStacktrace; - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private final MetricsLogger mMetricsLogger = new MetricsLogger(); private final boolean mIsAppSearchEnabled; @@ -518,7 +518,7 @@ public class ShortcutService extends IShortcutService.Stub { mUriPermissionOwner = mUriGrantsManagerInternal.newUriPermissionOwner(TAG); mRoleManager = Objects.requireNonNull(mContext.getSystemService(RoleManager.class)); - mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock); + mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mServiceLock); mShortcutDumpFiles = new ShortcutDumpFiles(this); mIsAppSearchEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.SHORTCUT_APPSEARCH_INTEGRATION, false) @@ -595,7 +595,7 @@ public class ShortcutService extends IShortcutService.Stub { // Default launcher is removed or changed, revoke all URI permissions. mUriGrantsManagerInternal.revokeUriPermissionFromOwner(mUriPermissionOwner, null, ~0, 0); - synchronized (mLock) { + synchronized (mServiceLock) { // Clear the launcher cache for this user. It will be set again next time the default // launcher is read from RoleManager. if (isUserLoadedLocked(userId)) { @@ -622,7 +622,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, "onUidStateChanged: uid=" + uid + " state=" + procState); } Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutHandleOnUidStateChanged"); - synchronized (mLock) { + synchronized (mServiceLock) { mUidState.put(uid, procState); // We need to keep track of last time an app comes to foreground. @@ -639,7 +639,7 @@ public class ShortcutService extends IShortcutService.Stub { return processState <= PROCESS_STATE_FOREGROUND_THRESHOLD; } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") boolean isUidForegroundLocked(int uid) { if (uid == Process.SYSTEM_UID) { // IUidObserver doesn't report the state of SYSTEM, but it always has bound services, @@ -655,7 +655,7 @@ public class ShortcutService extends IShortcutService.Stub { return isProcessStateForeground(mActivityManagerInternal.getUidProcessState(uid)); } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") long getUidLastForegroundElapsedTimeLocked(int uid) { return mUidLastForegroundElapsedTime.get(uid); } @@ -729,7 +729,7 @@ public class ShortcutService extends IShortcutService.Stub { final long start = getStatStartTime(); injectRunOnNewThread(() -> { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutHandleUnlockUser"); - synchronized (mLock) { + synchronized (mServiceLock) { logDurationStat(Stats.ASYNC_PRELOAD_USER_DELAY, start); getUserShortcutsLocked(userId); } @@ -743,7 +743,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, "handleStopUser: user=" + userId); } Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutHandleStopUser"); - synchronized (mLock) { + synchronized (mServiceLock) { unloadUserLocked(userId); synchronized (mUnlockedUsers) { @@ -753,7 +753,7 @@ public class ShortcutService extends IShortcutService.Stub { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void unloadUserLocked(int userId) { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, "unloadUserLocked: user=" + userId); @@ -784,7 +784,7 @@ public class ShortcutService extends IShortcutService.Stub { * Init the instance. (load the state file, etc) */ private void initialize() { - synchronized (mLock) { + synchronized (mServiceLock) { loadConfigurationLocked(); loadBaseStateLocked(); } @@ -1003,7 +1003,7 @@ public class ShortcutService extends IShortcutService.Stub { FileOutputStream outs = null; try { - synchronized (mLock) { + synchronized (mServiceLock) { outs = file.startWrite(); } @@ -1029,7 +1029,7 @@ public class ShortcutService extends IShortcutService.Stub { } } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void loadBaseStateLocked() { mRawLastResetTime.set(0); @@ -1104,7 +1104,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, "Saving to " + file); } - synchronized (mLock) { + synchronized (mServiceLock) { os = file.startWrite(); saveUserInternalLocked(userId, os, /* forBackup= */ false); } @@ -1122,7 +1122,7 @@ public class ShortcutService extends IShortcutService.Stub { getUserShortcutsLocked(userId).logSharingShortcutStats(mMetricsLogger); } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void saveUserInternalLocked(@UserIdInt int userId, OutputStream os, boolean forBackup) throws IOException, XmlPullParserException { @@ -1224,7 +1224,7 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, "Scheduling to save for " + userId); } - synchronized (mLock) { + synchronized (mServiceLock) { if (!mDirtyUserIds.contains(userId)) { mDirtyUserIds.add(userId); } @@ -1245,7 +1245,7 @@ public class ShortcutService extends IShortcutService.Stub { try { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutSaveDirtyInfo"); List<Integer> dirtyUserIds = new ArrayList<>(); - synchronized (mLock) { + synchronized (mServiceLock) { List<Integer> tmp = mDirtyUserIds; mDirtyUserIds = dirtyUserIds; dirtyUserIds = tmp; @@ -1266,14 +1266,14 @@ public class ShortcutService extends IShortcutService.Stub { } /** Return the last reset time. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") long getLastResetTimeLocked() { updateTimesLocked(); return mRawLastResetTime.get(); } /** Return the next reset time. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") long getNextResetTimeLocked() { updateTimesLocked(); return mRawLastResetTime.get() + mResetInterval; @@ -1286,7 +1286,7 @@ public class ShortcutService extends IShortcutService.Stub { /** * Update the last reset time. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void updateTimesLocked() { final long now = injectCurrentTimeMillis(); @@ -1315,7 +1315,7 @@ public class ShortcutService extends IShortcutService.Stub { } } - // Requires mLock held, but "Locked" prefix would look weired so we just say "L". + // Requires mServiceLock held, but "Locked" prefix would look weird so we just say "L". protected boolean isUserUnlockedL(@UserIdInt int userId) { // First, check the local copy. synchronized (mUnlockedUsers) { @@ -1331,14 +1331,14 @@ public class ShortcutService extends IShortcutService.Stub { return mUserManagerInternal.isUserUnlockingOrUnlocked(userId); } - // Requires mLock held, but "Locked" prefix would look weired so we jsut say "L". + // Requires mServiceLock held, but "Locked" prefix would look weird so we just say "L". void throwIfUserLockedL(@UserIdInt int userId) { if (!isUserUnlockedL(userId)) { throw new IllegalStateException("User " + userId + " is locked or not running"); } } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @NonNull private boolean isUserLoadedLocked(@UserIdInt int userId) { return mUsers.get(userId) != null; @@ -1347,7 +1347,7 @@ public class ShortcutService extends IShortcutService.Stub { private int mLastLockedUser = -1; /** Return the per-user state. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @NonNull ShortcutUser getUserShortcutsLocked(@UserIdInt int userId) { if (!isUserUnlockedL(userId)) { @@ -1386,7 +1386,7 @@ public class ShortcutService extends IShortcutService.Stub { return ret; } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") void forEachLoadedUserLocked(@NonNull Consumer<ShortcutUser> c) { for (int i = mUsers.size() - 1; i >= 0; i--) { c.accept(mUsers.valueAt(i)); @@ -1397,7 +1397,7 @@ public class ShortcutService extends IShortcutService.Stub { * Return the per-user per-package state. If the caller is a publisher, use * {@link #getPackageShortcutsForPublisherLocked} instead. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @NonNull ShortcutPackage getPackageShortcutsLocked( @NonNull String packageName, @UserIdInt int userId) { @@ -1405,7 +1405,7 @@ public class ShortcutService extends IShortcutService.Stub { } /** Return the per-user per-package state. Use this when the caller is a publisher. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @NonNull ShortcutPackage getPackageShortcutsForPublisherLocked( @NonNull String packageName, @UserIdInt int userId) { @@ -1414,7 +1414,7 @@ public class ShortcutService extends IShortcutService.Stub { return ret; } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @NonNull ShortcutLauncher getLauncherShortcutsLocked( @NonNull String packageName, @UserIdInt int ownerUserId, @@ -1443,7 +1443,7 @@ public class ShortcutService extends IShortcutService.Stub { * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap * saves are going on. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void cleanupDanglingBitmapDirectoriesLocked(@UserIdInt int userId) { if (DEBUG) { Slog.d(TAG, "cleanupDanglingBitmaps: userId=" + userId); @@ -1780,7 +1780,7 @@ public class ShortcutService extends IShortcutService.Stub { void injectPostToHandlerDebounced(@NonNull final Object token, @NonNull final Runnable r) { Objects.requireNonNull(token); Objects.requireNonNull(r); - synchronized (mLock) { + synchronized (mServiceLock) { mHandler.removeCallbacksAndMessages(token); mHandler.postDelayed(r, token, CALLBACK_DELAY); } @@ -2015,7 +2015,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); @@ -2084,7 +2084,7 @@ public class ShortcutService extends IShortcutService.Stub { final List<ShortcutInfo> changedShortcuts = new ArrayList<>(1); final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); @@ -2184,7 +2184,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); @@ -2241,7 +2241,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); @@ -2306,7 +2306,7 @@ public class ShortcutService extends IShortcutService.Stub { verifyCaller(packageName, userId); verifyShortcutInfoPackage(packageName, shortcut); final Intent intent; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); // Send request to the launcher, if supported. intent = mShortcutRequestPinProcessor.createShortcutResultIntent(shortcut, userId); @@ -2337,7 +2337,7 @@ public class ShortcutService extends IShortcutService.Stub { } final boolean ret; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); Preconditions.checkState(isUidForegroundLocked(callingUid), @@ -2378,7 +2378,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds, @@ -2419,7 +2419,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(shortcutIds, "shortcutIds must be provided"); List<ShortcutInfo> changedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds, @@ -2449,7 +2449,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds, @@ -2487,7 +2487,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = new ArrayList<>(); List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); // Dynamic shortcuts that are either cached or pinned will not get deleted. @@ -2511,7 +2511,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); ps.ensureImmutableShortcutsNotIncludedWithIds((List<String>) shortcutIds, @@ -2545,7 +2545,7 @@ public class ShortcutService extends IShortcutService.Stub { public ParceledListSlice<ShortcutInfo> getShortcuts(String packageName, @ShortcutManager.ShortcutMatchFlags int matchFlags, @UserIdInt int userId) { verifyCaller(packageName, userId); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); final boolean matchDynamic = (matchFlags & ShortcutManager.FLAG_MATCH_DYNAMIC) != 0; final boolean matchPinned = (matchFlags & ShortcutManager.FLAG_MATCH_PINNED) != 0; @@ -2575,7 +2575,7 @@ public class ShortcutService extends IShortcutService.Stub { "getShareTargets"); final ComponentName chooser = injectChooserActivity(); final String pkg = chooser != null ? chooser.getPackageName() : mContext.getPackageName(); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>(); final ShortcutUser user = getUserShortcutsLocked(userId); @@ -2592,7 +2592,7 @@ public class ShortcutService extends IShortcutService.Stub { enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS, "hasShareTargets"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); return getPackageShortcutsLocked(packageToCheck, userId).hasShareTargets(); @@ -2606,7 +2606,7 @@ public class ShortcutService extends IShortcutService.Stub { enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APP_PREDICTIONS, "isSharingShortcut"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(callingUserId); @@ -2623,7 +2623,7 @@ public class ShortcutService extends IShortcutService.Stub { return false; } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName, @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> filter) { @@ -2649,7 +2649,7 @@ public class ShortcutService extends IShortcutService.Stub { final boolean unlimited = injectHasUnlimitedShortcutsApiCallsPermission( injectBinderCallingPid(), injectBinderCallingUid()); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); @@ -2661,7 +2661,7 @@ public class ShortcutService extends IShortcutService.Stub { public long getRateLimitResetTime(String packageName, @UserIdInt int userId) { verifyCaller(packageName, userId); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); return getNextResetTimeLocked(); @@ -2672,7 +2672,7 @@ public class ShortcutService extends IShortcutService.Stub { public int getIconMaxDimensions(String packageName, int userId) { verifyCaller(packageName, userId); - synchronized (mLock) { + synchronized (mServiceLock) { return mMaxIconDimension; } } @@ -2686,7 +2686,7 @@ public class ShortcutService extends IShortcutService.Stub { shortcutId, packageName, userId)); } final ShortcutPackage ps; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); ps = getPackageShortcutsForPublisherLocked(packageName, userId); if (ps.findShortcutById(shortcutId) == null) { @@ -2723,7 +2723,7 @@ public class ShortcutService extends IShortcutService.Stub { } void resetThrottlingInner(@UserIdInt int userId) { - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { Log.w(TAG, "User " + userId + " is locked or not running"); return; @@ -2747,7 +2747,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, "onApplicationActive: package=" + packageName + " userid=" + userId); } enforceResetThrottlingPermission(); - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { // This is called by system UI, so no need to throw. Just ignore. return; @@ -2804,7 +2804,7 @@ public class ShortcutService extends IShortcutService.Stub { // even when hasShortcutPermission() is overridden. @VisibleForTesting boolean hasShortcutHostPermissionInner(@NonNull String packageName, int userId) { - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); final String defaultLauncher = getDefaultLauncher(userId); @@ -2830,7 +2830,7 @@ public class ShortcutService extends IShortcutService.Stub { final long token = injectClearCallingIdentity(); boolean isSupported; try { - synchronized (mLock) { + synchronized (mServiceLock) { isSupported = !mUserManagerInternal.getUserProperties(userId) .areItemsRestrictedOnHomeScreen(); } @@ -2846,7 +2846,7 @@ public class ShortcutService extends IShortcutService.Stub { final long start = getStatStartTime(); final long token = injectClearCallingIdentity(); try { - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); final ShortcutUser user = getUserShortcutsLocked(userId); @@ -2890,7 +2890,7 @@ public class ShortcutService extends IShortcutService.Stub { private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId, boolean appStillExists) { - synchronized (mLock) { + synchronized (mServiceLock) { forEachLoadedUserLocked(user -> cleanUpPackageLocked(packageName, user.getUserId(), packageUserId, appStillExists)); @@ -2904,7 +2904,7 @@ public class ShortcutService extends IShortcutService.Stub { * * This is called when an app is uninstalled, or an app gets "clear data"ed. */ - @GuardedBy("mLock") + @GuardedBy("mServiceLock") @VisibleForTesting void cleanUpPackageLocked(String packageName, int owningUserId, int packageUserId, boolean appStillExists) { @@ -2979,7 +2979,7 @@ public class ShortcutService extends IShortcutService.Stub { shortcutIds = null; // LauncherAppsService already threw for it though. } - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3005,7 +3005,7 @@ public class ShortcutService extends IShortcutService.Stub { return setReturnedByServer(ret); } - @GuardedBy("ShortcutService.this.mLock") + @GuardedBy("ShortcutService.this.mServiceLock") private void getShortcutsInnerLocked(int launcherUserId, @NonNull String callingPackage, @Nullable String packageName, @Nullable List<String> shortcutIds, @Nullable List<LocusId> locusIds, long changedSince, @@ -3095,7 +3095,7 @@ public class ShortcutService extends IShortcutService.Stub { return; } final ShortcutPackage p; - synchronized (mLock) { + synchronized (mServiceLock) { p = getUserShortcutsLocked(userId).getPackageShortcutsIfExists(packageName); } if (p == null) { @@ -3129,7 +3129,7 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkStringNotEmpty(packageName, "packageName"); Preconditions.checkStringNotEmpty(shortcutId, "shortcutId"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3143,7 +3143,7 @@ public class ShortcutService extends IShortcutService.Stub { } } - @GuardedBy("ShortcutService.this.mLock") + @GuardedBy("ShortcutService.this.mServiceLock") private ShortcutInfo getShortcutInfoLocked( int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId, @@ -3176,7 +3176,7 @@ public class ShortcutService extends IShortcutService.Stub { throwIfUserLockedL(launcherUserId); final ShortcutPackage p; - synchronized (mLock) { + synchronized (mServiceLock) { p = getUserShortcutsLocked(userId).getPackageShortcutsIfExists(packageName); } if (p == null) { @@ -3198,7 +3198,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage sp; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3284,7 +3284,7 @@ public class ShortcutService extends IShortcutService.Stub { List<ShortcutInfo> changedShortcuts = null; List<ShortcutInfo> removedShortcuts = null; final ShortcutPackage sp; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3346,7 +3346,7 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty"); Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3380,7 +3380,7 @@ public class ShortcutService extends IShortcutService.Stub { Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty"); // Check in memory shortcut first - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3430,7 +3430,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(packageName, "packageName"); Objects.requireNonNull(shortcutId, "shortcutId"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3458,7 +3458,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(packageName, "packageName"); Objects.requireNonNull(shortcutId, "shortcutId"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3484,7 +3484,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(packageName, "packageName"); Objects.requireNonNull(shortcutId, "shortcutId"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3515,7 +3515,7 @@ public class ShortcutService extends IShortcutService.Stub { // Checks shortcuts in memory first final ShortcutPackage p; - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3568,7 +3568,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(packageName, "packageName"); Objects.requireNonNull(shortcutId, "shortcutId"); - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3599,7 +3599,7 @@ public class ShortcutService extends IShortcutService.Stub { Objects.requireNonNull(shortcutId, "shortcutId"); // Checks shortcuts in memory first - synchronized (mLock) { + synchronized (mServiceLock) { throwIfUserLockedL(userId); throwIfUserLockedL(launcherUserId); @@ -3702,7 +3702,7 @@ public class ShortcutService extends IShortcutService.Stub { if (!callingPackage.equals(defaultLauncher)) { return false; } - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUidForegroundLocked(callingUid)) { return false; } @@ -3733,7 +3733,7 @@ public class ShortcutService extends IShortcutService.Stub { } scheduleSaveBaseState(); - synchronized (mLock) { + synchronized (mServiceLock) { final long token = injectClearCallingIdentity(); try { forEachLoadedUserLocked(user -> user.detectLocaleChange()); @@ -3762,7 +3762,7 @@ public class ShortcutService extends IShortcutService.Stub { // but we still check it in unit tests. final long token = injectClearCallingIdentity(); try { - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { if (DEBUG) { Slog.d(TAG, "Ignoring package broadcast " + action @@ -3821,7 +3821,7 @@ public class ShortcutService extends IShortcutService.Stub { // Since it cleans up the shortcut directory and rewrite the ShortcutPackageItems // in odrder during saveToXml(), it could lead to shortcuts missing when shutdown. // We need it so that it can finish up saving before shutdown. - synchronized (mLock) { + synchronized (mServiceLock) { if (mHandler.hasCallbacks(mSaveDirtyInfoRunner)) { mHandler.removeCallbacks(mSaveDirtyInfoRunner); forEachLoadedUserLocked(ShortcutUser::cancelAllInFlightTasks); @@ -3852,7 +3852,7 @@ public class ShortcutService extends IShortcutService.Stub { try { final ArrayList<UserPackage> gonePackages = new ArrayList<>(); - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = getUserShortcutsLocked(ownerUserId); // Find packages that have been uninstalled. @@ -3885,7 +3885,7 @@ public class ShortcutService extends IShortcutService.Stub { verifyStates(); } - @GuardedBy("mLock") + @GuardedBy("mServiceLock") private void rescanUpdatedPackagesLocked(@UserIdInt int userId, long lastScanTime) { if (DEBUG_REBOOT) { Slog.d(TAG, "rescan updated package user=" + userId + " last scanned=" + lastScanTime); @@ -3916,7 +3916,7 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, String.format("handlePackageAdded: %s user=%d", packageName, userId)); } - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = getUserShortcutsLocked(userId); user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.rescanPackageIfNeeded(packageName, /* forceRescan=*/ true); @@ -3929,7 +3929,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, String.format("handlePackageUpdateFinished: %s user=%d", packageName, userId)); } - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = getUserShortcutsLocked(userId); user.attemptToRestoreIfNeededAndSave(this, packageName, userId); @@ -3972,7 +3972,7 @@ public class ShortcutService extends IShortcutService.Stub { } // Activities may be disabled or enabled. Just rescan the package. - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = getUserShortcutsLocked(packageUserId); user.rescanPackageIfNeeded(packageName, /* forceRescan=*/ true); @@ -4474,7 +4474,7 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG) { Slog.d(TAG, "Backing up user " + userId); } - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { wtf("Can't backup: user " + userId + " is locked or not running"); return null; @@ -4524,7 +4524,7 @@ public class ShortcutService extends IShortcutService.Stub { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, "Restoring user " + userId); } - synchronized (mLock) { + synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { wtf("Can't restore: user " + userId + " is locked or not running"); return; @@ -4762,7 +4762,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void dumpInner(PrintWriter pw, DumpFilter filter) { - synchronized (mLock) { + synchronized (mServiceLock) { if (filter.shouldDumpDetails()) { final long now = injectCurrentTimeMillis(); pw.print("Now: ["); @@ -4841,7 +4841,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void dumpUid(PrintWriter pw) { - synchronized (mLock) { + synchronized (mServiceLock) { pw.println("** SHORTCUT MANAGER UID STATES (dumpsys shortcut -n -u)"); for (int i = 0; i < mUidState.size(); i++) { @@ -4876,7 +4876,7 @@ public class ShortcutService extends IShortcutService.Stub { * behavior but shortcut service doesn't for now. */ private void dumpCheckin(PrintWriter pw, boolean clear) { - synchronized (mLock) { + synchronized (mServiceLock) { try { final JSONArray users = new JSONArray(); @@ -4898,7 +4898,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void dumpDumpFiles(PrintWriter pw) { - synchronized (mLock) { + synchronized (mServiceLock) { pw.println("** SHORTCUT MANAGER FILES (dumpsys shortcut -n -f)"); mShortcutDumpFiles.dumpAll(pw); } @@ -5051,7 +5051,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void handleResetThrottling() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); Slog.i(TAG, "cmd: handleResetThrottling: user=" + mUserId); @@ -5071,7 +5071,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.i(TAG, "cmd: handleOverrideConfig: " + config); - synchronized (mLock) { + synchronized (mServiceLock) { if (!updateConfigurationLocked(config)) { throw new CommandException("override-config failed. See logcat for details."); } @@ -5081,7 +5081,7 @@ public class ShortcutService extends IShortcutService.Stub { private void handleResetConfig() { Slog.i(TAG, "cmd: handleResetConfig"); - synchronized (mLock) { + synchronized (mServiceLock) { loadConfigurationLocked(); } } @@ -5090,7 +5090,7 @@ public class ShortcutService extends IShortcutService.Stub { // should query this information directly from RoleManager instead. Keeping the old behavior // by returning the result from package manager. private void handleGetDefaultLauncher() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); final String defaultLauncher = getDefaultLauncher(mUserId); @@ -5114,7 +5114,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void handleUnloadUser() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); Slog.i(TAG, "cmd: handleUnloadUser: user=" + mUserId); @@ -5124,7 +5124,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void handleClearShortcuts() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); final String packageName = getNextArgRequired(); @@ -5136,7 +5136,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void handleGetShortcuts() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); final String packageName = getNextArgRequired(); @@ -5162,7 +5162,7 @@ public class ShortcutService extends IShortcutService.Stub { } private void handleHasShortcutAccess() throws CommandException { - synchronized (mLock) { + synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); final String packageName = getNextArgRequired(); @@ -5318,7 +5318,7 @@ public class ShortcutService extends IShortcutService.Stub { @VisibleForTesting ShortcutPackage getPackageShortcutForTest(String packageName, int userId) { - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = mUsers.get(userId); if (user == null) return null; @@ -5328,7 +5328,7 @@ public class ShortcutService extends IShortcutService.Stub { @VisibleForTesting ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) { - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId); if (pkg == null) return null; @@ -5339,7 +5339,7 @@ public class ShortcutService extends IShortcutService.Stub { @VisibleForTesting void updatePackageShortcutForTest(String packageName, String shortcutId, int userId, Consumer<ShortcutInfo> cb) { - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId); if (pkg == null) return; cb.accept(pkg.findShortcutById(shortcutId)); @@ -5348,7 +5348,7 @@ public class ShortcutService extends IShortcutService.Stub { @VisibleForTesting ShortcutLauncher getLauncherShortcutForTest(String packageName, int userId) { - synchronized (mLock) { + synchronized (mServiceLock) { final ShortcutUser user = mUsers.get(userId); if (user == null) return null; @@ -5385,14 +5385,14 @@ public class ShortcutService extends IShortcutService.Stub { } private void verifyStatesInner() { - synchronized (mLock) { + synchronized (mServiceLock) { forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates)); } } @VisibleForTesting void waitForBitmapSavesForTest() { - synchronized (mLock) { + synchronized (mServiceLock) { forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::waitForBitmapSaves)); } diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java index 2d76c5092b92..4ad4353f602e 100644 --- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java +++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java @@ -38,6 +38,7 @@ import android.util.Log; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -66,6 +67,7 @@ public class SideFpsEventHandler implements View.OnClickListener { private final int mDismissDialogTimeout; @Nullable private SideFpsToast mDialog; + private final AccessibilityManager mAccessibilityManager; private final Runnable mTurnOffDialog = () -> { dismissDialog("mTurnOffDialog"); @@ -96,6 +98,7 @@ public class SideFpsEventHandler implements View.OnClickListener { DialogProvider provider) { mContext = context; mHandler = handler; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mPowerManager = powerManager; mBiometricState = STATE_IDLE; mSideFpsEventHandlerReady = new AtomicBoolean(false); @@ -157,7 +160,9 @@ public class SideFpsEventHandler implements View.OnClickListener { mHandler.removeCallbacks(mTurnOffDialog); } showDialog(eventTime, "Enroll Power Press"); - mHandler.postDelayed(mTurnOffDialog, mDismissDialogTimeout); + if (!mAccessibilityManager.isEnabled()) { + mHandler.postDelayed(mTurnOffDialog, mDismissDialogTimeout); + } }); return true; case STATE_BP_AUTH: @@ -231,6 +236,10 @@ public class SideFpsEventHandler implements View.OnClickListener { public void onBiometricAction( @BiometricStateListener.Action int action) { Log.d(TAG, "onBiometricAction " + action); + if (mAccessibilityManager != null + && mAccessibilityManager.isEnabled()) { + dismissDialog("mTurnOffDialog"); + } } }); mSideFpsEventHandlerReady.set(true); @@ -256,6 +265,9 @@ public class SideFpsEventHandler implements View.OnClickListener { mLastPowerPressTime = time; mDialog.show(); mDialog.setOnClickListener(this); + if (mAccessibilityManager.isEnabled()) { + mDialog.addAccessibilityDelegate(); + } } interface DialogProvider { diff --git a/services/core/java/com/android/server/policy/SideFpsToast.java b/services/core/java/com/android/server/policy/SideFpsToast.java index db074670de9b..c27753ce1c0f 100644 --- a/services/core/java/com/android/server/policy/SideFpsToast.java +++ b/services/core/java/com/android/server/policy/SideFpsToast.java @@ -16,6 +16,7 @@ package com.android.server.policy; +import android.annotation.NonNull; import android.app.Dialog; import android.content.Context; import android.os.Bundle; @@ -23,6 +24,7 @@ import android.view.Gravity; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; import android.widget.Button; import com.android.internal.R; @@ -34,7 +36,6 @@ import com.android.internal.R; * This dialog is used by {@link SideFpsEventHandler} */ public class SideFpsToast extends Dialog { - SideFpsToast(Context context) { super(context); } @@ -66,4 +67,27 @@ public class SideFpsToast extends Dialog { turnOffScreen.setOnClickListener(listener); } } + + /** + * When accessibility mode is on, add AccessibilityDelegate to dismiss dialog when focus is + * moved away from the dialog. + */ + public void addAccessibilityDelegate() { + final Button turnOffScreen = findViewById(R.id.turn_off_screen); + if (turnOffScreen != null) { + turnOffScreen.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent(@NonNull View host, + @NonNull AccessibilityEvent event) { + if (event.getEventType() + == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED + && isShowing()) { + dismiss(); + } + super.onInitializeAccessibilityEvent(host, event); + } + }); + + } + } } diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index 592d0396b349..12db21dbe41a 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -40,6 +40,7 @@ import java.lang.ref.WeakReference; import java.io.PrintWriter; import java.util.Arrays; import java.util.ArrayList; +import java.util.Comparator; import java.util.Objects; /** @@ -211,10 +212,6 @@ public abstract class AnrTimer<V> implements AutoCloseable { @GuardedBy("mLock") private int mTotalStarted = 0; - /** The total number of timers that were restarted without an explicit cancel. */ - @GuardedBy("mLock") - private int mTotalRestarted = 0; - /** The total number of errors detected. */ @GuardedBy("mLock") private int mTotalErrors = 0; @@ -368,7 +365,7 @@ public abstract class AnrTimer<V> implements AutoCloseable { abstract boolean enabled(); - abstract void dump(PrintWriter pw, boolean verbose); + abstract void dump(IndentingPrintWriter pw, boolean verbose); abstract void close(); } @@ -410,9 +407,14 @@ public abstract class AnrTimer<V> implements AutoCloseable { return false; } - /** dump() is a no-op when the feature is disabled. */ + /** Dump the limited statistics captured when the feature is disabled. */ @Override - void dump(PrintWriter pw, boolean verbose) { + void dump(IndentingPrintWriter pw, boolean verbose) { + synchronized (mLock) { + pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n", + mTotalStarted, mMaxStarted, mTimerIdMap.size(), + mTotalExpired, mTotalErrors); + } } /** close() is a no-op when the feature is disabled. */ @@ -441,6 +443,10 @@ public abstract class AnrTimer<V> implements AutoCloseable { */ private long mNative = 0; + /** The total number of timers that were restarted without an explicit cancel. */ + @GuardedBy("mLock") + private int mTotalRestarted = 0; + /** Fetch the native tag (an integer) for the given label. */ FeatureEnabled() { mNative = nativeAnrTimerCreate(mLabel); @@ -537,13 +543,22 @@ public abstract class AnrTimer<V> implements AutoCloseable { /** Dump statistics from the native layer. */ @Override - void dump(PrintWriter pw, boolean verbose) { + void dump(IndentingPrintWriter pw, boolean verbose) { synchronized (mLock) { - if (mNative != 0) { - nativeAnrTimerDump(mNative, verbose); - } else { + if (mNative == 0) { pw.println("closed"); + return; } + String[] nativeDump = nativeAnrTimerDump(mNative); + if (nativeDump == null) { + pw.println("no-data"); + return; + } + for (String s : nativeDump) { + pw.println(s); + } + // The following counter is only available at the Java level. + pw.println("restarted:" + mTotalRestarted); } } @@ -690,11 +705,8 @@ public abstract class AnrTimer<V> implements AutoCloseable { synchronized (mLock) { pw.format("timer: %s\n", mLabel); pw.increaseIndent(); - pw.format("started=%d maxStarted=%d restarted=%d running=%d expired=%d errors=%d\n", - mTotalStarted, mMaxStarted, mTotalRestarted, mTimerIdMap.size(), - mTotalExpired, mTotalErrors); - pw.decreaseIndent(); mFeature.dump(pw, false); + pw.decreaseIndent(); } } @@ -755,6 +767,14 @@ public abstract class AnrTimer<V> implements AutoCloseable { recordErrorLocked(operation, "notFound", arg); } + /** Compare two AnrTimers in display order. */ + private static final Comparator<AnrTimer> sComparator = + Comparator.nullsLast(new Comparator<>() { + @Override + public int compare(AnrTimer o1, AnrTimer o2) { + return o1.mLabel.compareTo(o2.mLabel); + }}); + /** Dumpsys output, allowing for overrides. */ @VisibleForTesting static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) { @@ -764,11 +784,18 @@ public abstract class AnrTimer<V> implements AutoCloseable { ipw.println("AnrTimer statistics"); ipw.increaseIndent(); synchronized (sAnrTimerList) { + // Find the currently live instances and sort them by their label. The goal is to + // have consistent output ordering. final int size = sAnrTimerList.size(); - ipw.println("reporting " + size + " timers"); + AnrTimer[] active = new AnrTimer[size]; + int valid = 0; for (int i = 0; i < size; i++) { AnrTimer a = sAnrTimerList.valueAt(i).get(); - if (a != null) a.dump(ipw); + if (a != null) active[valid++] = a; + } + Arrays.sort(active, 0, valid, sComparator); + for (int i = 0; i < valid; i++) { + if (active[i] != null) active[i].dump(ipw); } } if (verbose) dumpErrors(ipw); @@ -827,6 +854,6 @@ public abstract class AnrTimer<V> implements AutoCloseable { /** Discard an expired timer by ID. Return true if the timer was found. */ private static native boolean nativeAnrTimerDiscard(long service, int timerId); - /** Prod the native library to log a few statistics. */ - private static native void nativeAnrTimerDump(long service, boolean verbose); + /** Retrieve runtime dump information from the native layer. */ + private static native String[] nativeAnrTimerDump(long service); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 1f320dab99dd..7d057a9d3aca 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -9817,10 +9817,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldApplyUserMinAspectRatioOverride()) { return mLetterboxUiController.getUserMinAspectRatio(); } - if (!mLetterboxUiController.shouldOverrideMinAspectRatio()) { + if (!mLetterboxUiController.shouldOverrideMinAspectRatio() + && !mLetterboxUiController.shouldOverrideMinAspectRatioForCamera()) { return info.getMinAspectRatio(); } - if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY) && !ActivityInfo.isFixedOrientationPortrait( getOverrideOrientation())) { @@ -9941,6 +9941,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return updateReportedConfigurationAndSend(); } + /** + * @return {@code true} if the Camera is active for the current activity + */ + boolean isCameraActive() { + return mDisplayContent != null + && mDisplayContent.getDisplayRotationCompatPolicy() != null + && mDisplayContent.getDisplayRotationCompatPolicy() + .isCameraActive(this, /* mustBeFullscreen */ true); + } + boolean updateReportedConfigurationAndSend() { if (isConfigurationDispatchPaused()) { Slog.wtf(TAG, "trying to update reported(client) config while dispatch is paused"); diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index 8069a9335151..1c599777e497 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -48,15 +48,8 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { /** * Flag to indicate whether to restrict desktop mode to supported devices. */ - @VisibleForTesting - static final String ENFORCE_DEVICE_RESTRICTIONS_KEY = - "persist.wm.debug.desktop_mode_enforce_device_restrictions"; - - /** - * Flag to indicate whether to restrict desktop mode to supported devices. - */ private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( - ENFORCE_DEVICE_RESTRICTIONS_KEY, true); + "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); private StringBuilder mLogBuilder; @@ -118,19 +111,7 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { } if (phase == PHASE_WINDOWING_MODE) { - return RESULT_DONE; - } - - // TODO(b/336998072) - Find a better solution to this that makes use of the logic from - // TaskLaunchParamsModifier. Put logic in common utils, return RESULT_CONTINUE, inherit - // from parent class, etc. - if (outParams.mPreferredTaskDisplayArea == null && task.getRootTask() != null) { - appendLog("display-from-task=" + task.getRootTask().getDisplayId()); - outParams.mPreferredTaskDisplayArea = task.getRootTask().getDisplayArea(); - } - - if (phase == PHASE_DISPLAY_AREA) { - return RESULT_DONE; + return RESULT_CONTINUE; } if (!currentParams.mBounds.isEmpty()) { @@ -142,7 +123,7 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { appendLog("setting desktop mode task bounds to %s", outParams.mBounds); - return RESULT_DONE; + return RESULT_CONTINUE; } /** diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index f7e5dd8632fc..da2999810a10 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1318,6 +1318,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + /** + * @return The {@link DisplayRotationCompatPolicy} for this DisplayContent + */ + // TODO(b/335387481) Allow access to DisplayRotationCompatPolicy only with getters + @Nullable + DisplayRotationCompatPolicy getDisplayRotationCompatPolicy() { + return mDisplayRotationCompatPolicy; + } + @Override void migrateToNewSurfaceControl(Transaction t) { t.remove(mSurfaceControl); diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index d5f8df35d96f..eacf9a3fa759 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -64,7 +64,7 @@ import com.android.server.UiThread; * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. */ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path -final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraCompatStateListener { +class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraCompatStateListener { // Delay for updating display rotation after Camera connection is closed. Needed to avoid // rotation flickering when an app is flipping between front and rear cameras or when size @@ -306,7 +306,8 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) { return isTreatmentEnabledForDisplay() - && isCameraActive(activity, /* mustBeFullscreen */ true); + && isCameraActive(activity, /* mustBeFullscreen */ true) + && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } @@ -324,6 +325,13 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true); } + boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) { + // Checking windowing mode on activity level because we don't want to + // apply treatment in case of activity embedding. + return (!mustBeFullscreen || !activity.inMultiWindowMode()) + && mCameraStateMonitor.isCameraRunningForActivity(activity); + } + private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity, boolean mustBeFullscreen) { return activity != null && isCameraActive(activity, mustBeFullscreen) @@ -331,14 +339,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp // "locked" and "nosensor" values are often used by camera apps that can't // handle dynamic changes so we shouldn't force rotate them. && activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR - && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED; - } - - private boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) { - // Checking windowing mode on activity level because we don't want to - // apply treatment in case of activity embedding. - return (!mustBeFullscreen || !activity.inMultiWindowMode()) - && mCameraStateMonitor.isCameraRunningForActivity(activity) + && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 683efde4bb09..b38e666ea691 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -30,6 +30,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIE import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; @@ -510,6 +511,26 @@ final class LetterboxUiController { } /** + * Whether we should apply the min aspect ratio per-app override only when an app is connected + * to the camera. + * When this override is applied the min aspect ratio given in the app's manifest will be + * overridden to the largest enabled aspect ratio treatment unless the app's manifest value + * is higher. The treatment will also apply if no value is provided in the manifest. + * + * <p>This method returns {@code true} when the following conditions are met: + * <ul> + * <li>Opt-out component property isn't enabled + * <li>Per-app override is enabled + * </ul> + */ + boolean shouldOverrideMinAspectRatioForCamera() { + return mActivityRecord.isCameraActive() + && mAllowMinAspectRatioOverrideOptProp + .shouldEnableWithOptInOverrideAndOptOutProperty( + isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA)); + } + + /** * Whether we should apply the force resize per-app override. When this override is applied it * forces the packages it is applied to to be resizable. It won't change whether the app can be * put into multi-windowing mode, but allow the app to resize without going into size-compat @@ -962,7 +983,8 @@ final class LetterboxUiController { void recomputeConfigurationForCameraCompatIfNeeded() { if (isOverrideOrientationOnlyForCameraEnabled() - || isCameraCompatSplitScreenAspectRatioAllowed()) { + || isCameraCompatSplitScreenAspectRatioAllowed() + || shouldOverrideMinAspectRatioForCamera()) { mActivityRecord.recomputeConfiguration(); } } diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index da95666fb7c4..6509958defda 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -161,14 +161,14 @@ class AnrTimerService { // A timer has expired. void expire(timer_id_t); - // Dump a small amount of state to the log file. - void dump(bool verbose) const; - // Return the Java object associated with this instance. jweak jtimer() const { return notifierObject_; } + // Return the per-instance statistics. + std::vector<std::string> getDump() const; + private: // The service cannot be copied. AnrTimerService(AnrTimerService const &) = delete; @@ -199,7 +199,7 @@ class AnrTimerService { std::set<Timer> running_; // The maximum number of active timers. - size_t maxActive_; + size_t maxRunning_; // Simple counters struct Counters { @@ -209,6 +209,7 @@ class AnrTimerService { size_t accepted; size_t discarded; size_t expired; + size_t extended; // The number of times there were zero active timers. size_t drained; @@ -437,7 +438,9 @@ class AnrTimerService::Ticker { // Construct the ticker. This creates the timerfd file descriptor and starts the monitor // thread. The monitor thread is given a unique name. - Ticker() { + Ticker() : + id_(idGen_.fetch_add(1)) + { timerFd_ = timer_create(); if (timerFd_ < 0) { ALOGE("failed to create timerFd: %s", strerror(errno)); @@ -502,6 +505,11 @@ class AnrTimerService::Ticker { } } + // The unique ID of this particular ticker. Used for debug and logging. + size_t id() const { + return id_; + } + // Return the number of timers still running. size_t running() const { AutoMutex _l(lock_); @@ -617,8 +625,16 @@ class AnrTimerService::Ticker { // The list of timers that are scheduled. This set is sorted by timeout and then by timer // ID. A set is sufficient (as opposed to a multiset) because timer IDs are unique. std::set<Entry> running_; + + // A unique ID assigned to this instance. + const size_t id_; + + // The ID generator. + static std::atomic<size_t> idGen_; }; +std::atomic<size_t> AnrTimerService::Ticker::idGen_; + AnrTimerService::AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) : @@ -629,7 +645,7 @@ AnrTimerService::AnrTimerService(char const* label, ticker_(ticker) { // Zero the statistics - maxActive_ = 0; + maxRunning_ = 0; memset(&counters_, 0, sizeof(counters_)); ALOGI_IF(DEBUG, "initialized %s", label); @@ -739,6 +755,12 @@ void AnrTimerService::expire(timer_id_t timerId) { elapsed = now() - t.started; } + if (expired) { + counters_.expired++; + } else { + counters_.extended++; + } + // Deliver the notification outside of the lock. if (expired) { if (!notifier_(timerId, pid, uid, elapsed, notifierCookie_, notifierObject_)) { @@ -756,8 +778,8 @@ void AnrTimerService::insert(const Timer& t) { if (t.status == Running) { // Only forward running timers to the ticker. Expired timers are handled separately. ticker_->insert(t.scheduled, t.id, this); - maxActive_ = std::max(maxActive_, running_.size()); } + maxRunning_ = std::max(maxRunning_, running_.size()); } AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) { @@ -767,29 +789,32 @@ AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) { Timer result = *found; running_.erase(found); ticker_->remove(result.scheduled, result.id); + if (running_.size() == 0) counters_.drained++; return result; } return Timer(); } -void AnrTimerService::dump(bool verbose) const { +std::vector<std::string> AnrTimerService::getDump() const { + std::vector<std::string> r; AutoMutex _l(lock_); - ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu", - label_.c_str(), - counters_.started, counters_.canceled, counters_.accepted, - counters_.discarded, counters_.expired); - ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu", - label_.c_str(), - maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(), - counters_.error); - - if (verbose) { - nsecs_t time = now(); - for (auto i = running_.begin(); i != running_.end(); i++) { - Timer t = *i; - ALOGI(" running %s", t.toString(time).c_str()); - } - } + r.push_back(StringPrintf("started:%zu canceled:%zu accepted:%zu discarded:%zu expired:%zu", + counters_.started, + counters_.canceled, + counters_.accepted, + counters_.discarded, + counters_.expired)); + r.push_back(StringPrintf("extended:%zu drained:%zu error:%zu running:%zu maxRunning:%zu", + counters_.extended, + counters_.drained, + counters_.error, + running_.size(), + maxRunning_)); + r.push_back(StringPrintf("ticker:%zu ticking:%zu maxTicking:%zu", + ticker_->id(), + ticker_->running(), + ticker_->maxRunning())); + return r; } /** @@ -894,21 +919,26 @@ jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) { return toService(ptr)->discard(timerId); } -jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) { - if (!nativeSupportEnabled) return -1; - toService(ptr)->dump(verbose); - return 0; +jobjectArray anrTimerDump(JNIEnv *env, jclass, jlong ptr) { + if (!nativeSupportEnabled) return nullptr; + std::vector<std::string> stats = toService(ptr)->getDump(); + jclass sclass = env->FindClass("java/lang/String"); + jobjectArray r = env->NewObjectArray(stats.size(), sclass, nullptr); + for (size_t i = 0; i < stats.size(); i++) { + env->SetObjectArrayElement(r, i, env->NewStringUTF(stats[i].c_str())); + } + return r; } static const JNINativeMethod methods[] = { {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported}, - {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate}, - {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose}, - {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart}, - {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel}, - {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept}, - {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard}, - {"nativeAnrTimerDump", "(JZ)V", (void*) anrTimerDump}, + {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate}, + {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose}, + {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart}, + {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel}, + {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept}, + {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard}, + {"nativeAnrTimerDump", "(J)[Ljava/lang/String;", (void*) anrTimerDump}, }; } // anonymous namespace diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index ff757366ea23..85ab562902e2 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -5673,18 +5673,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private boolean resetPasswordInternal(String password, long tokenHandle, byte[] token, int flags, CallerIdentity caller) { - final boolean isPin = PasswordMetrics.isNumericOnly(password); - try (LockscreenCredential newCredential = - isPin ? LockscreenCredential.createPin(password) : - LockscreenCredential.createPasswordOrNone(password)) { - return resetPasswordInternal(newCredential, tokenHandle, token, flags, caller); - } - } - - private boolean resetPasswordInternal(LockscreenCredential newCredential, - long tokenHandle, byte[] token, int flags, CallerIdentity caller) { final int callingUid = caller.getUid(); final int userHandle = UserHandle.getUserId(callingUid); + final boolean isPin = PasswordMetrics.isNumericOnly(password); + final LockscreenCredential newCredential; + if (isPin) { + newCredential = LockscreenCredential.createPin(password); + } else { + newCredential = LockscreenCredential.createPasswordOrNone(password); + } synchronized (getLockObject()) { final PasswordMetrics minMetrics = getPasswordMinimumMetricsUnchecked(userHandle); final int complexity = getAggregatedPasswordComplexityLocked(userHandle); @@ -21613,7 +21610,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; } - if (Flags.headlessSingleUserFixes() && isSingleUserMode && !mInjector.isChangeEnabled( + if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode() + && isSingleUserMode && !mInjector.isChangeEnabled( PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) { throw new IllegalStateException("Device admin is not targeting Android V."); } @@ -21632,7 +21630,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { setLocale(provisioningParams.getLocale()); int deviceOwnerUserId = Flags.headlessDeviceOwnerSingleUserEnabled() - && isSingleUserMode + && isSingleUserMode && mInjector.userManagerIsHeadlessSystemUserMode() ? mUserManagerInternal.getMainUserId() : UserHandle.USER_SYSTEM; if (!removeNonRequiredAppsForManagedDevice( diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt new file mode 100644 index 000000000000..f0abcd228f14 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.mode + +import android.content.Context +import android.util.SparseArray +import android.view.Display +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.server.display.feature.DisplayManagerFlags +import com.android.server.display.mode.DisplayModeDirector.DisplayDeviceConfigProvider +import com.android.server.display.mode.RefreshRateVote.RenderVote +import com.android.server.testutils.TestHandler +import com.google.common.truth.Truth.assertThat +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(TestParameterInjector::class) +class AppRequestObserverTest { + + private lateinit var context: Context + private val mockInjector = mock<DisplayModeDirector.Injector>() + private val mockFlags = mock<DisplayManagerFlags>() + private val mockDisplayDeviceConfigProvider = mock<DisplayDeviceConfigProvider>() + private val testHandler = TestHandler(null) + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun `test app request votes`(@TestParameter testCase: AppRequestTestCase) { + whenever(mockFlags.ignoreAppPreferredRefreshRateRequest()) + .thenReturn(testCase.ignoreRefreshRateRequest) + val displayModeDirector = DisplayModeDirector( + context, testHandler, mockInjector, mockFlags, mockDisplayDeviceConfigProvider) + val modes = arrayOf( + Display.Mode(1, 1000, 1000, 60f), + Display.Mode(2, 1000, 1000, 90f), + Display.Mode(3, 1000, 1000, 120f) + ) + displayModeDirector.injectSupportedModesByDisplay(SparseArray<Array<Display.Mode>>().apply { + append(Display.DEFAULT_DISPLAY, modes) + }) + displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply { + append(Display.DEFAULT_DISPLAY, modes[0]) + }) + + displayModeDirector.appRequestObserver.setAppRequest(Display.DEFAULT_DISPLAY, + testCase.modeId, + testCase.requestedRefreshRates, + testCase.requestedMinRefreshRates, + testCase.requestedMaxRefreshRates) + + val baseModeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE) + assertThat(baseModeVote).isEqualTo(testCase.expectedBaseModeVote) + + val sizeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_SIZE) + assertThat(sizeVote).isEqualTo(testCase.expectedSizeVote) + + val renderRateVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE) + assertThat(renderRateVote).isEqualTo(testCase.expectedRenderRateVote) + } + + enum class AppRequestTestCase( + val ignoreRefreshRateRequest: Boolean, + val modeId: Int, + val requestedRefreshRates: Float, + val requestedMinRefreshRates: Float, + val requestedMaxRefreshRates: Float, + internal val expectedBaseModeVote: Vote?, + internal val expectedSizeVote: Vote?, + internal val expectedRenderRateVote: Vote?, + ) { + BASE_MODE_60(true, 1, 0f, 0f, 0f, + BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), null), + BASE_MODE_90(true, 2, 0f, 0f, 0f, + BaseModeRefreshRateVote(90f), SizeVote(1000, 1000, 1000, 1000), null), + MIN_REFRESH_RATE_60(true, 0, 0f, 60f, 0f, + null, null, RenderVote(60f, Float.POSITIVE_INFINITY)), + MIN_REFRESH_RATE_120(true, 0, 0f, 120f, 0f, + null, null, RenderVote(120f, Float.POSITIVE_INFINITY)), + MAX_REFRESH_RATE_60(true, 0, 0f, 0f, 60f, + null, null, RenderVote(0f, 60f)), + MAX_REFRESH_RATE_120(true, 0, 0f, 0f, 120f, + null, null, RenderVote(0f, 120f)), + INVALID_MIN_MAX_REFRESH_RATE(true, 0, 0f, 90f, 60f, + null, null, null), + BASE_MODE_MIN_MAX(true, 1, 0f, 60f, 90f, + BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), RenderVote(60f, 90f)), + PREFERRED_REFRESH_RATE(false, 0, 60f, 0f, 0f, + BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), null), + PREFERRED_REFRESH_RATE_IGNORED(true, 0, 60f, 0f, 0f, + null, null, null), + PREFERRED_REFRESH_RATE_INVALID(false, 0, 45f, 0f, 0f, + null, null, null), + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 52e157b7788f..cd1e9e85afb5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -31,7 +31,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; @@ -2262,162 +2261,6 @@ public class DisplayModeDirectorTest { } @Test - public void testAppRequestObserver_modeId() { - DisplayModeDirector director = createDirectorFromFpsRange(60, 90); - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0); - - Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class); - BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote; - assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(SizeVote.class); - SizeVote sizeVote = (SizeVote) vote; - assertThat(sizeVote.mHeight).isEqualTo(1000); - assertThat(sizeVote.mWidth).isEqualTo(1000); - assertThat(sizeVote.mMinHeight).isEqualTo(1000); - assertThat(sizeVote.mMinWidth).isEqualTo(1000); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNull(vote); - - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class); - baseModeVote = (BaseModeRefreshRateVote) vote; - assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(SizeVote.class); - sizeVote = (SizeVote) vote; - assertThat(sizeVote.mHeight).isEqualTo(1000); - assertThat(sizeVote.mWidth).isEqualTo(1000); - assertThat(sizeVote.mMinHeight).isEqualTo(1000); - assertThat(sizeVote.mMinWidth).isEqualTo(1000); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNull(vote); - } - - @Test - public void testAppRequestObserver_minRefreshRate() { - DisplayModeDirector director = createDirectorFromFpsRange(60, 90); - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60, 0); - Vote appRequestRefreshRate = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNull(appRequestRefreshRate); - - Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNull(appRequestSize); - - Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class); - RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote; - assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(60); - assertThat(renderVote.mMaxRefreshRate).isAtLeast(90); - - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 0); - appRequestRefreshRate = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNull(appRequestRefreshRate); - - appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNull(appRequestSize); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class); - renderVote = (RefreshRateVote.RenderVote) vote; - assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90); - assertThat(renderVote.mMaxRefreshRate).isAtLeast(90); - } - - @Test - public void testAppRequestObserver_maxRefreshRate() { - DisplayModeDirector director = createDirectorFromFpsRange(60, 90); - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 90); - Vote appRequestRefreshRate = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNull(appRequestRefreshRate); - - Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNull(appRequestSize); - - Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class); - RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote; - assertThat(renderVote.mMinRefreshRate).isZero(); - assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90); - - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 60); - appRequestRefreshRate = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNull(appRequestRefreshRate); - - appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNull(appRequestSize); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class); - renderVote = (RefreshRateVote.RenderVote) vote; - assertThat(renderVote.mMinRefreshRate).isZero(); - assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(60); - } - - @Test - public void testAppRequestObserver_invalidRefreshRateRange() { - DisplayModeDirector director = createDirectorFromFpsRange(60, 90); - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 60); - Vote appRequestRefreshRate = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNull(appRequestRefreshRate); - - Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNull(appRequestSize); - - Vote appRequestRefreshRateRange = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNull(appRequestRefreshRateRange); - } - - @Test - public void testAppRequestObserver_modeIdAndRefreshRateRange() { - DisplayModeDirector director = createDirectorFromFpsRange(60, 90); - director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90); - - Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class); - BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote; - assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(SizeVote.class); - SizeVote sizeVote = (SizeVote) vote; - assertThat(sizeVote.mHeight).isEqualTo(1000); - assertThat(sizeVote.mWidth).isEqualTo(1000); - assertThat(sizeVote.mMinHeight).isEqualTo(1000); - assertThat(sizeVote.mMinWidth).isEqualTo(1000); - - vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNotNull(vote); - assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class); - RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote; - assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90); - assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90); - } - - @Test public void testAppRequestsIsTheDefaultMode() { Display.Mode[] modes = new Display.Mode[2]; modes[0] = new Display.Mode( diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt index 3e0677c62f1c..4d910cefdb79 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt @@ -146,8 +146,8 @@ class SettingsObserverTest { val modes = arrayOf( Display.Mode(1, 1000, 1000, 60f), - Display.Mode(1, 1000, 1000, 90f), - Display.Mode(1, 1000, 1000, 120f) + Display.Mode(2, 1000, 1000, 90f), + Display.Mode(3, 1000, 1000, 120f) ) displayModeDirector.injectSupportedModesByDisplay(SparseArray<Array<Display.Mode>>().apply { append(Display.DEFAULT_DISPLAY, modes) diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java index 23314cdaf041..1322545c8d7e 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java @@ -20,12 +20,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.content.res.TypedArray; import android.os.Looper; import android.platform.test.annotations.EnableFlags; import android.service.dreams.DreamService; @@ -41,6 +46,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; @SmallTest @RunWith(AndroidJUnit4.class) @@ -83,6 +89,18 @@ public class DreamServiceTest { assertThat(metadata.dreamCategory).isEqualTo(DreamService.DREAM_CATEGORY_DEFAULT); } + @Test + public void testMetadataParsing_exceptionReading() { + final PackageManager packageManager = Mockito.mock(PackageManager.class); + final ServiceInfo serviceInfo = Mockito.mock(ServiceInfo.class); + final TypedArray rawMetadata = Mockito.mock(TypedArray.class); + when(packageManager.extractPackageItemInfoAttributes(eq(serviceInfo), any(), any(), any())) + .thenReturn(rawMetadata); + when(rawMetadata.getString(anyInt())).thenThrow(new RuntimeException("failure")); + + assertThat(DreamService.getDreamMetadata(packageManager, serviceInfo)).isNull(); + } + private DreamService.DreamMetadata getDreamMetadata(String dreamClassName) throws PackageManager.NameNotFoundException { final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java index a8b792e30485..80f7a066985b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -197,7 +197,7 @@ public class AsyncProcessStartTest { + Arrays.toString(invocation.getArguments())); if (!wedge) { if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) { - mRealAms.finishAttachApplication(0); + mRealAms.finishAttachApplication(0, 0); } } return null; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java index 67be93b3b49f..89c67d5e32b7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java @@ -200,7 +200,7 @@ public class ProcessObserverTest { Log.v(TAG, "Intercepting bindApplication() for " + Arrays.toString(invocation.getArguments())); if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) { - mRealAms.finishAttachApplication(0); + mRealAms.finishAttachApplication(0, 0); } return null; }).when(thread).bindApplication( diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java index e672928c5403..599b9cd06ee1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java @@ -24,6 +24,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import android.content.ContentResolver; import android.os.SystemProperties; import android.provider.Settings; +import android.provider.DeviceConfig.Properties; import android.text.TextUtils; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -42,6 +43,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; /** * Test SettingsToPropertiesMapper. @@ -61,6 +63,7 @@ public class SettingsToPropertiesMapperTest { private HashMap<String, String> mSystemSettingsMap; private HashMap<String, String> mGlobalSettingsMap; + private HashMap<String, String> mConfigSettingsMap; @Before public void setUp() throws Exception { @@ -71,9 +74,11 @@ public class SettingsToPropertiesMapperTest { .spyStatic(SystemProperties.class) .spyStatic(Settings.Global.class) .spyStatic(SettingsToPropertiesMapper.class) + .spyStatic(Settings.Config.class) .startMocking(); mSystemSettingsMap = new HashMap<>(); mGlobalSettingsMap = new HashMap<>(); + mConfigSettingsMap = new HashMap<>(); // Mock SystemProperties setter and various getters doAnswer((Answer<Void>) invocationOnMock -> { @@ -93,7 +98,7 @@ public class SettingsToPropertiesMapperTest { } ).when(() -> SystemProperties.get(anyString())); - // Mock Settings.Global methods + // Mock Settings.Global method doAnswer((Answer<String>) invocationOnMock -> { String key = invocationOnMock.getArgument(1); @@ -101,6 +106,21 @@ public class SettingsToPropertiesMapperTest { } ).when(() -> Settings.Global.getString(any(), anyString())); + // Mock Settings.Config getstrings method + doAnswer((Answer<Map<String, String>>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + List<String> flags = invocationOnMock.getArgument(1); + HashMap<String, String> values = new HashMap<>(); + for (String flag : flags) { + String value = mConfigSettingsMap.get(namespace + "/" + flag); + if (value != null) { + values.put(flag, value); + } + } + return values; + } + ).when(() -> Settings.Config.getStrings(anyString(), any())); + mTestMapper = new SettingsToPropertiesMapper( mMockContentResolver, TEST_MAPPING, new String[] {}, new String[] {}); } @@ -239,4 +259,43 @@ public class SettingsToPropertiesMapperTest { Assert.assertTrue(categories.contains("category2")); Assert.assertTrue(categories.contains("category3")); } + + @Test + public void testGetStagedFlagsWithValueChange() { + // mock up what is in the setting already + mConfigSettingsMap.put("namespace_1/flag_1", "true"); + mConfigSettingsMap.put("namespace_1/flag_2", "true"); + + // mock up input + String namespace = "staged"; + Map<String, String> keyValueMap = new HashMap<>(); + // case 1: existing prop, stage the same value + keyValueMap.put("namespace_1*flag_1", "true"); + // case 2: existing prop, stage a different value + keyValueMap.put("namespace_1*flag_2", "false"); + // case 3: new prop, stage the non default value + keyValueMap.put("namespace_2*flag_1", "true"); + // case 4: new prop, stage the default value + keyValueMap.put("namespace_2*flag_2", "false"); + Properties props = new Properties(namespace, keyValueMap); + + HashMap<String, HashMap<String, String>> toStageProps = + SettingsToPropertiesMapper.getStagedFlagsWithValueChange(props); + + HashMap<String, String> namespace_1_to_stage = toStageProps.get("namespace_1"); + HashMap<String, String> namespace_2_to_stage = toStageProps.get("namespace_2"); + Assert.assertTrue(namespace_1_to_stage != null); + Assert.assertTrue(namespace_2_to_stage != null); + + String namespace_1_flag_1 = namespace_1_to_stage.get("flag_1"); + String namespace_1_flag_2 = namespace_1_to_stage.get("flag_2"); + String namespace_2_flag_1 = namespace_2_to_stage.get("flag_1"); + String namespace_2_flag_2 = namespace_2_to_stage.get("flag_2"); + Assert.assertTrue(namespace_1_flag_1 == null); + Assert.assertTrue(namespace_1_flag_2 != null); + Assert.assertTrue(namespace_2_flag_1 != null); + Assert.assertTrue(namespace_2_flag_2 == null); + Assert.assertTrue(namespace_1_flag_2.equals("false")); + Assert.assertTrue(namespace_2_flag_1.equals("true")); + } } diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index a6f2196cf05b..9862663c37b2 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -16,8 +16,6 @@ package com.android.server.os; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; - import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -25,9 +23,9 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.app.admin.flags.Flags; -import android.app.role.RoleManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -61,6 +59,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.FileDescriptor; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -104,7 +104,7 @@ public class BugreportManagerServiceImplTest { ArraySet<String> mAllowlistedPackages = new ArraySet<>(); mAllowlistedPackages.add(mContext.getPackageName()); mInjector = new TestInjector(mContext, mAllowlistedPackages, mMappingFile, - mMockUserManager, mMockDevicePolicyManager); + mMockUserManager, mMockDevicePolicyManager, null); mService = new BugreportManagerServiceImpl(mInjector); mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile); when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid); @@ -114,24 +114,8 @@ public class BugreportManagerServiceImplTest { @After public void tearDown() throws Exception { - // Changes to RoleManager persist between tests, so we need to clear out any funny - // business we did in previous tests. + // Clean up the mapping file between tests since it would otherwise persist. mMappingFile.delete(); - RoleManager roleManager = mContext.getSystemService(RoleManager.class); - CallbackFuture future = new CallbackFuture(); - runWithShellPermissionIdentity( - () -> { - roleManager.setBypassingRoleQualification(false); - roleManager.removeRoleHolderAsUser( - "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION", - mContext.getPackageName(), - /* flags= */ 0, - Process.myUserHandle(), - mContext.getMainExecutor(), - future); - }); - - assertThat(future.get()).isEqualTo(true); } @Test @@ -267,7 +251,10 @@ public class BugreportManagerServiceImplTest { @Test public void testCancelBugreportWithoutRole() { - clearAllowlist(); + // Create a new service to clear the allowlist + mService = new BugreportManagerServiceImpl( + new TestInjector(mContext, new ArraySet<>(), mMappingFile, + mMockUserManager, mMockDevicePolicyManager, null)); assertThrows(SecurityException.class, () -> mService.cancelBugreport( Binder.getCallingUid(), mContext.getPackageName())); @@ -275,29 +262,13 @@ public class BugreportManagerServiceImplTest { @Test public void testCancelBugreportWithRole() throws Exception { - clearAllowlist(); - RoleManager roleManager = mContext.getSystemService(RoleManager.class); - CallbackFuture future = new CallbackFuture(); - runWithShellPermissionIdentity( - () -> { - roleManager.setBypassingRoleQualification(true); - roleManager.addRoleHolderAsUser( - "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION", - mContext.getPackageName(), - /* flags= */ 0, - Process.myUserHandle(), - mContext.getMainExecutor(), - future); - }); - - assertThat(future.get()).isEqualTo(true); - mService.cancelBugreport(Binder.getCallingUid(), mContext.getPackageName()); - } - - private void clearAllowlist() { + // Create a new service to clear the allowlist, but override the role manager mService = new BugreportManagerServiceImpl( new TestInjector(mContext, new ArraySet<>(), mMappingFile, - mMockUserManager, mMockDevicePolicyManager)); + mMockUserManager, mMockDevicePolicyManager, + "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION")); + + mService.cancelBugreport(Binder.getCallingUid(), mContext.getPackageName()); } private static class Listener implements IDumpstateListener { @@ -359,10 +330,22 @@ public class BugreportManagerServiceImplTest { private boolean mBugreportStarted = false; TestInjector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile, - UserManager um, DevicePolicyManager dpm) { + UserManager um, DevicePolicyManager dpm, String grantedRole) { super(context, allowlistedPackages, mappingFile); mUserManager = um; mDevicePolicyManager = dpm; + + if (grantedRole != null) { + mRoleManagerWrapper = + new BugreportManagerServiceImpl.Injector.RoleManagerWrapper() { + @Override + List<String> getRoleHolders(@NonNull String roleName) { + return roleName.equals(grantedRole) + ? Collections.singletonList(mContext.getPackageName()) + : Collections.emptyList(); + } + }; + } } @Override diff --git a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java index 669eedf39342..dabf5317f168 100644 --- a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java @@ -31,11 +31,13 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.os.Handler; import android.os.PowerManager; +import android.os.RemoteException; import android.os.test.TestLooper; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableResources; import android.view.Window; +import android.view.accessibility.AccessibilityManager; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -48,7 +50,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.List; @@ -72,9 +75,15 @@ public class SideFpsEventHandlerTest { private static final Integer AUTO_DISMISS_DIALOG = 500; @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Rule public TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext(), null); + private final AccessibilityManager mAccessibilityManager = + mContext.getSystemService(AccessibilityManager.class); + @Mock private PackageManager mPackageManager; @Mock @@ -89,9 +98,8 @@ public class SideFpsEventHandlerTest { private BiometricStateListener mBiometricStateListener; @Before - public void setup() { - MockitoAnnotations.initMocks(this); - + public void setup() throws RemoteException { + disableAccessibility(); mContext.addMockSystemService(PackageManager.class, mPackageManager); mContext.addMockSystemService(FingerprintManager.class, mFingerprintManager); TestableResources resources = mContext.getOrCreateTestableResources(); @@ -192,9 +200,8 @@ public class SideFpsEventHandlerTest { } @Test - public void dialogDismissesAfterTime() throws Exception { + public void dialogDismissesAfterTime_accessibilityDisabled() throws Exception { setupWithSensor(true /* hasSfps */, true /* initialized */); - setBiometricState(BiometricStateListener.STATE_ENROLLING); when(mDialog.isShowing()).thenReturn(true); assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue(); @@ -207,9 +214,23 @@ public class SideFpsEventHandlerTest { } @Test - public void dialogDoesNotDismissOnSensorTouch() throws Exception { + public void dialogDoesNotDismissAfterTime_accessibilityEnabled() throws Exception { + enableAccessibility(); setupWithSensor(true /* hasSfps */, true /* initialized */); + setBiometricState(BiometricStateListener.STATE_ENROLLING); + when(mDialog.isShowing()).thenReturn(true); + assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue(); + + mLooper.dispatchAll(); + verify(mDialog).show(); + mLooper.moveTimeForward(AUTO_DISMISS_DIALOG); + mLooper.dispatchAll(); + verify(mDialog, never()).dismiss(); + } + @Test + public void dialogDoesNotDismissOnSensorTouch_accessibilityDisabled() throws Exception { + setupWithSensor(true /* hasSfps */, true /* initialized */); setBiometricState(BiometricStateListener.STATE_ENROLLING); when(mDialog.isShowing()).thenReturn(true); assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue(); @@ -218,12 +239,26 @@ public class SideFpsEventHandlerTest { verify(mDialog).show(); mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH); - mLooper.moveTimeForward(AUTO_DISMISS_DIALOG - 1); mLooper.dispatchAll(); - verify(mDialog, never()).dismiss(); } + @Test + public void dialogDismissesOnSensorTouch_accessibilityEnabled() throws Exception { + enableAccessibility(); + setupWithSensor(true /* hasSfps */, true /* initialized */); + setBiometricState(BiometricStateListener.STATE_ENROLLING); + when(mDialog.isShowing()).thenReturn(true); + assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue(); + + mLooper.dispatchAll(); + verify(mDialog).show(); + + mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH); + mLooper.dispatchAll(); + verify(mDialog).dismiss(); + } + private void setBiometricState(@BiometricStateListener.State int newState) { if (mBiometricStateListener != null) { mBiometricStateListener.onStateChanged(newState); @@ -231,6 +266,20 @@ public class SideFpsEventHandlerTest { } } + private void enableAccessibility() throws RemoteException { + if (mAccessibilityManager != null) { + mAccessibilityManager.getClient().setState(1); + mLooper.dispatchAll(); + } + } + + private void disableAccessibility() throws RemoteException { + if (mAccessibilityManager != null) { + mAccessibilityManager.getClient().setState(0); + mLooper.dispatchAll(); + } + } + private void setupWithSensor(boolean hasSfps, boolean initialized) throws Exception { when(mPackageManager.hasSystemFeature(eq(PackageManager.FEATURE_FINGERPRINT))) .thenReturn(true); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index 0d6fdc9e312a..4af20a97a9aa 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -2638,7 +2638,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { @Test public void testSoundResetsRankingTime() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_UPDATE_RANKING_TIME); + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME); TestableFlagResolver flagResolver = new TestableFlagResolver(); initAttentionHelper(flagResolver); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 74d8433700d3..3a0eba11d488 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,7 +24,7 @@ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS; -import static android.app.Flags.FLAG_UPDATE_RANKING_TIME; +import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; @@ -7376,7 +7376,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_UPDATE_RANKING_TIME}) + @EnableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME}) public void testVisualDifference_userInitiatedJob() { Notification.Builder nb1 = new Notification.Builder(mContext, "") .setContentTitle("foo"); @@ -15283,7 +15283,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_UPDATE_RANKING_TIME) + @EnableFlags(FLAG_SORT_SECTION_BY_TIME) public void rankingTime_newNotification_noisy_matchesSbn() throws Exception { NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, mUserId); @@ -15297,7 +15297,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_UPDATE_RANKING_TIME) + @EnableFlags(FLAG_SORT_SECTION_BY_TIME) public void rankingTime_newNotification_silent_matchesSbn() throws Exception { NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); NotificationRecord nr = generateNotificationRecord(low, mUserId); @@ -15312,7 +15312,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_UPDATE_RANKING_TIME) + @EnableFlags(FLAG_SORT_SECTION_BY_TIME) public void rankingTime_updatedNotification_silentSameText_originalPostTime() throws Exception { NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); NotificationRecord nr = generateNotificationRecord(low, mUserId); @@ -15332,7 +15332,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_UPDATE_RANKING_TIME) + @EnableFlags(FLAG_SORT_SECTION_BY_TIME) public void rankingTime_updatedNotification_silentNewText_newPostTime() throws Exception { NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); NotificationRecord nr = generateNotificationRecord(low, 0, mUserId); @@ -15357,7 +15357,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_UPDATE_RANKING_TIME) + @EnableFlags(FLAG_SORT_SECTION_BY_TIME) public void rankingTime_updatedNotification_noisySameText_newPostTime() throws Exception { NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); NotificationRecord nr = generateNotificationRecord(low, mUserId); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 9a58594e818c..d1880d2f3528 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -1561,14 +1561,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, false, mClock); - loadByteArrayXml(baos.toByteArray(), false, USER_SYSTEM); + loadByteArrayXml(baos.toByteArray(), false, USER_ALL); // Trigger 2nd restore pass - when(mPm.getPackageUidAsUser(PKG_R, USER_SYSTEM)).thenReturn(UID_P); + when(mPm.getPackageUidAsUser(PKG_R, USER_SYSTEM)).thenReturn(UID_R); mXmlHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_R}, - new int[]{UID_P}); + new int[]{UID_R}); - NotificationChannel channel = mXmlHelper.getNotificationChannel(PKG_R, UID_P, id, + NotificationChannel channel = mXmlHelper.getNotificationChannel(PKG_R, UID_R, id, false); assertThat(channel.getImportance()).isEqualTo(2); assertThat(channel.canShowBadge()).isTrue(); @@ -1616,7 +1616,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, false, mClock); - loadByteArrayXml(xml.getBytes(), false, USER_SYSTEM); + loadByteArrayXml(xml.getBytes(), false, USER_ALL); // Trigger 2nd restore pass mXmlHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_R}, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index 527001df995f..9a6e81865947 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -378,7 +378,7 @@ public class RankingHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME, Flags.FLAG_UPDATE_RANKING_TIME}) + @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) public void testSort_oldWhenChildren_unspecifiedSummary() { NotificationRecord child1 = new NotificationRecord(mContext, new StatusBarNotification( @@ -430,7 +430,7 @@ public class RankingHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME, Flags.FLAG_UPDATE_RANKING_TIME}) + @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) public void testSort_oldChildren_unspecifiedSummary() { NotificationRecord child1 = new NotificationRecord(mContext, new StatusBarNotification( @@ -480,7 +480,7 @@ public class RankingHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_SORT_SECTION_BY_TIME, Flags.FLAG_UPDATE_RANKING_TIME}) + @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) public void testSort_oldChildren_oldSummary() { NotificationRecord child1 = new NotificationRecord(mContext, new StatusBarNotification( @@ -517,10 +517,10 @@ public class RankingHelperTest extends UiServiceTestCase { mUser, null, System.currentTimeMillis()), getLowChannel()); ArrayList<NotificationRecord> expected = new ArrayList<>(); + expected.add(unrelated); expected.add(summary); expected.add(child2); expected.add(child1); - expected.add(unrelated); ArrayList<NotificationRecord> actual = new ArrayList<>(); actual.addAll(expected); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 9559a256d326..5fdb3965be76 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -6095,6 +6095,67 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(readPolicy.allowConversations()).isFalse(); } + @Test + @EnableFlags(Flags.FLAG_MODES_API) + @DisableFlags(Flags.FLAG_MODES_UI) + public void setNotificationPolicy_updatesRulePolicies_ifRulePolicyIsDefaultOrGlobalPolicy() { + ZenPolicy defaultZenPolicy = mZenModeHelper.getDefaultZenPolicy(); + Policy previousManualPolicy = mZenModeHelper.mConfig.toNotificationPolicy(); + ZenPolicy previousManualZenPolicy = ZenAdapters.notificationPolicyToZenPolicy( + previousManualPolicy); + ZenPolicy customZenPolicy = new ZenPolicy.Builder(defaultZenPolicy).allowConversations( + CONVERSATION_SENDERS_ANYONE).build(); + + mZenModeHelper.mConfig.automaticRules.clear(); + addZenRule(mZenModeHelper.mConfig, "appWithDefault", "app.pkg", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, defaultZenPolicy); + addZenRule(mZenModeHelper.mConfig, "appWithSameAsManual", "app.pkg", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, previousManualZenPolicy); + addZenRule(mZenModeHelper.mConfig, "appWithCustom", "app.pkg", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, customZenPolicy); + addZenRule(mZenModeHelper.mConfig, "appWithOtherFilter", "app.pkg", + ZEN_MODE_ALARMS, null); + addZenRule(mZenModeHelper.mConfig, "systemWithDefault", "android", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, defaultZenPolicy); + addZenRule(mZenModeHelper.mConfig, "systemWithSameAsManual", "android", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, previousManualZenPolicy); + + Policy newManualPolicy = new Policy(PRIORITY_CATEGORY_EVENTS, 0, 0); + mZenModeHelper.setNotificationPolicy(newManualPolicy, UPDATE_ORIGIN_USER, 0); + ZenPolicy newManualZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(newManualPolicy); + + // Only app rules with default or same-as-manual policies were updated. + assertThat(mZenModeHelper.mConfig.automaticRules.get("appWithDefault").zenPolicy) + .isEqualTo(newManualZenPolicy); + assertThat(mZenModeHelper.mConfig.automaticRules.get("appWithSameAsManual").zenPolicy) + .isEqualTo(newManualZenPolicy); + + assertThat(mZenModeHelper.mConfig.automaticRules.get("appWithCustom").zenPolicy) + .isEqualTo(customZenPolicy); + assertThat(mZenModeHelper.mConfig.automaticRules.get("appWithOtherFilter").zenPolicy) + .isNull(); + assertThat(mZenModeHelper.mConfig.automaticRules.get("systemWithDefault").zenPolicy) + .isEqualTo(defaultZenPolicy); + assertThat(mZenModeHelper.mConfig.automaticRules.get("systemWithSameAsManual").zenPolicy) + .isEqualTo(previousManualZenPolicy); + } + + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, + @Nullable ZenPolicy zenPolicy) { + ZenRule rule = new ZenRule(); + rule.id = id; + rule.pkg = ownerPkg; + rule.enabled = true; + rule.zenMode = zenMode; + rule.zenPolicy = zenPolicy; + // Plus stuff so that isValidAutomaticRule() passes + rule.name = String.format("Rule %s from %s with mode=%s and policy=%s", id, ownerPkg, + zenMode, zenPolicy); + rule.conditionId = Uri.parse(rule.name); + + config.automaticRules.put(id, rule); + } + private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = Correspondence.transforming(zr -> { Parcel p = Parcel.obtain(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 30eb5efc16a7..1da5001dab42 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -116,6 +116,7 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; import android.app.ActivityOptions; import android.app.AppOpsManager; @@ -3507,6 +3508,23 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testIsCameraActive() { + final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final DisplayRotationCompatPolicy displayRotationCompatPolicy = mock( + DisplayRotationCompatPolicy.class); + when(mDisplayContent.getDisplayRotationCompatPolicy()).thenReturn( + displayRotationCompatPolicy); + + when(displayRotationCompatPolicy.isCameraActive(any(ActivityRecord.class), + anyBoolean())).thenReturn(false); + assertFalse(app.mActivityRecord.isCameraActive()); + + when(displayRotationCompatPolicy.isCameraActive(any(ActivityRecord.class), + anyBoolean())).thenReturn(true); + assertTrue(app.mActivityRecord.isCameraActive()); + } + + @Test public void testUpdateCameraCompatStateFromUser_clickedOnDismiss() throws RemoteException { final ActivityRecord activity = createActivityWithTask(); // Mock a flag being enabled. diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index 89ae802f474c..f92387cdf5c9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,30 +21,24 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; -import static com.android.server.wm.DesktopModeLaunchParamsModifier.ENFORCE_DEVICE_RESTRICTIONS_KEY; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; -import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import android.content.res.Resources; import android.graphics.Rect; -import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; -import com.android.internal.R; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result; import com.android.window.flags.Flags; @@ -52,7 +46,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; /** * Tests for desktop mode task bounds. @@ -80,6 +73,8 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { mCurrent.reset(); mResult = new LaunchParamsController.LaunchParams(); mResult.reset(); + + mTarget = new DesktopModeLaunchParamsModifier(mContext); } @Test @@ -101,12 +96,12 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void testReturnsDoneIfDesktopWindowingIsEnabledAndUnsupportedDeviceOverridden() { + public void testReturnsContinueIfDesktopWindowingIsEnabledAndUnsupportedDeviceOverridden() { setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true, /*enforceDeviceRestrictions=*/ false); final Task task = new TaskBuilder(mSupervisor).build(); - assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); } @Test @@ -139,22 +134,22 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void testReturnsDoneIfTaskUsingActivityTypeStandard() { + public void testReturnsContinueIfTaskUsingActivityTypeStandard() { setupDesktopModeLaunchParamsModifier(); final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_STANDARD).build(); - assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void testReturnsDoneIfTaskUsingActivityTypeUndefined() { + public void testReturnsContinueIfTaskUsingActivityTypeUndefined() { setupDesktopModeLaunchParamsModifier(); final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_UNDEFINED).build(); - assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); } @Test @@ -180,7 +175,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { task.getDisplayArea().setBounds(new Rect(0, 0, displayWidth, displayHeight)); final int desiredWidth = (int) (displayWidth * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); final int desiredHeight = (int) (displayHeight * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); assertEquals(desiredWidth, mResult.mBounds.width()); assertEquals(desiredHeight, mResult.mBounds.height()); } @@ -196,7 +191,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { mCurrent.mPreferredTaskDisplayArea = mockTaskDisplayArea; mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; - assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); assertEquals(mockTaskDisplayArea, mResult.mPreferredTaskDisplayArea); assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode); } @@ -208,15 +203,10 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported, boolean enforceDeviceRestrictions) { - Resources mockResources = Mockito.mock(Resources.class); - when(mockResources.getBoolean(eq(R.bool.config_isDesktopModeSupported))) - .thenReturn(isDesktopModeSupported); - doReturn(mockResources).when(mContext).getResources(); - - SystemProperties.set(ENFORCE_DEVICE_RESTRICTIONS_KEY, - String.valueOf(enforceDeviceRestrictions)); - - mTarget = new DesktopModeLaunchParamsModifier(mContext); + doReturn(isDesktopModeSupported) + .when(() -> DesktopModeLaunchParamsModifier.isDesktopModeSupported(any())); + doReturn(enforceDeviceRestrictions) + .when(DesktopModeLaunchParamsModifier::enforceDeviceRestrictions); } private class CalculateRequestBuilder { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index 507140d573ea..262ba8bb32f3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -38,6 +38,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -538,6 +539,42 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); } + @Test + public void testIsCameraActiveWhenCallbackInvokedNoMultiWindow_returnTrue() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertTrue( + mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + } + + @Test + public void testIsCameraActiveWhenCallbackNotInvokedNoMultiWindow_returnFalse() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + assertFalse( + mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + } + + @Test + public void testIsCameraActiveWhenCallbackNotInvokedMultiWindow_returnFalse() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.inMultiWindowMode()).thenReturn(true); + + assertFalse( + mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + } + + @Test + public void testIsCameraActiveWhenCallbackInvokedMultiWindow_returnFalse() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.inMultiWindowMode()).thenReturn(true); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertFalse( + mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + } + private void configureActivity(@ScreenOrientation int activityOrientation) { configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index b41db3170ef6..b74da1a888cd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -29,6 +29,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIE import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT; @@ -73,6 +74,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.annotation.Nullable; @@ -1086,6 +1089,39 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); } + @Test + public void testRecomputeConfigurationForCameraCompatIfNeeded() { + spyOn(mController); + doReturn(false).when(mController).isOverrideOrientationOnlyForCameraEnabled(); + doReturn(false).when(mController).isCameraCompatSplitScreenAspectRatioAllowed(); + doReturn(false).when(mController).shouldOverrideMinAspectRatioForCamera(); + clearInvocations(mActivity); + + mController.recomputeConfigurationForCameraCompatIfNeeded(); + + verify(mActivity, never()).recomputeConfiguration(); + + // isOverrideOrientationOnlyForCameraEnabled + doReturn(true).when(mController).isOverrideOrientationOnlyForCameraEnabled(); + clearInvocations(mActivity); + mController.recomputeConfigurationForCameraCompatIfNeeded(); + verify(mActivity).recomputeConfiguration(); + + // isCameraCompatSplitScreenAspectRatioAllowed + doReturn(false).when(mController).isOverrideOrientationOnlyForCameraEnabled(); + doReturn(true).when(mController).isCameraCompatSplitScreenAspectRatioAllowed(); + clearInvocations(mActivity); + mController.recomputeConfigurationForCameraCompatIfNeeded(); + verify(mActivity).recomputeConfiguration(); + + // shouldOverrideMinAspectRatioForCamera + doReturn(false).when(mController).isCameraCompatSplitScreenAspectRatioAllowed(); + doReturn(true).when(mController).shouldOverrideMinAspectRatioForCamera(); + clearInvocations(mActivity); + mController.recomputeConfigurationForCameraCompatIfNeeded(); + verify(mActivity).recomputeConfiguration(); + } + private void prepareActivityForShouldApplyUserMinAspectRatioOverride( boolean orientationRequest) { spyOn(mController); @@ -1280,6 +1316,78 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() { + doReturn(true).when(mActivity).isCameraActive(); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue() + throws Exception { + doReturn(true).when(mActivity).isCameraActive(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse() + throws Exception { + doReturn(false).when(mActivity).isCameraActive(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse() + throws Exception { + doReturn(true).when(mActivity).isCameraActive(); + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() { + doReturn(true).when(mActivity).isCameraActive(); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + doReturn(true).when(mActivity).isCameraActive(); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatioForCamera()); + } + + @Test @EnableCompatChanges({FORCE_RESIZE_APP}) public void testshouldOverrideForceResizeApp_overrideEnabled_returnsTrue() { mController = new LetterboxUiController(mWm, mActivity); diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 64adff80f442..87f26e5a8eae 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -200,6 +200,7 @@ public class SystemServicesTestRule implements TestRule { .mockStatic(DisplayControl.class, mockStubOnly) .mockStatic(LockGuard.class, mockStubOnly) .mockStatic(Watchdog.class, mockStubOnly) + .spyStatic(DesktopModeLaunchParamsModifier.class) .strictness(Strictness.LENIENT) .startMocking(); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index a35a35acb6c1..9d14290bdd8a 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -1241,7 +1241,7 @@ public class UsageStatsService extends SystemService implements break; case Event.SHORTCUT_INVOCATION: case Event.CHOOSER_ACTION: - case Event.STANDBY_BUCKET_CHANGED: + // case Event.STANDBY_BUCKET_CHANGED: case Event.FOREGROUND_SERVICE_START: case Event.FOREGROUND_SERVICE_STOP: logAppUsageEventReportedAtomLocked(event.mEventType, uid, event.mPackage); diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 461dfeca09e3..9a5e88becf1e 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -16,6 +16,7 @@ package com.android.server.wm.flicker.helpers +import android.graphics.Rect import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper @@ -32,6 +33,14 @@ import androidx.test.uiautomator.Until */ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : IStandardAppHelper by innerHelper { + + enum class Corners { + LEFT_TOP, + RIGHT_TOP, + LEFT_BOTTOM, + RIGHT_BOTTOM + } + private val TIMEOUT_MS = 3_000L private val CAPTION = "desktop_mode_caption" private val CAPTION_HANDLE = "caption_handle" @@ -121,4 +130,39 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds) } } + + /** Resize a desktop app from its corners. */ + fun cornerResize( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + corner: Corners, + horizontalChange: Int, + verticalChange: Int + ) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val (startX, startY) = getStartCoordinatesForCornerResize(windowRect, corner) + + // The position we want to drag to + val endY = startY + verticalChange + val endX = startX + horizontalChange + + // drag the specified corner of the window to the end coordinate. + device.drag(startX, startY, endX, endY, 100) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() + } + + private fun getStartCoordinatesForCornerResize( + windowRect: Rect, + corner: Corners + ): Pair<Int, Int> { + return when (corner) { + Corners.LEFT_TOP -> Pair(windowRect.left, windowRect.top) + Corners.RIGHT_TOP -> Pair(windowRect.right, windowRect.top) + Corners.LEFT_BOTTOM -> Pair(windowRect.left, windowRect.bottom) + Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom) + } + } } diff --git a/tests/Input/src/com/android/server/input/InputShellCommandTest.java b/tests/Input/src/com/android/server/input/InputShellCommandTest.java index f4845a518b20..11f46335f017 100644 --- a/tests/Input/src/com/android/server/input/InputShellCommandTest.java +++ b/tests/Input/src/com/android/server/input/InputShellCommandTest.java @@ -125,6 +125,14 @@ public class InputShellCommandTest { assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); } + @Test + public void testInvalidKeyEventCommandArgsCombination() { + // --duration and --longpress must not be sent together + runCommand("keyevent --duration 1000 --longpress KEYCODE_A"); + + assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); + } + private InputEvent getSingleInjectedInputEvent() { assertThat(mInputEventInjector.mInjectedEvents).hasSize(1); return mInputEventInjector.mInjectedEvents.get(0); diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING index eca258c5a74d..f6885e1e74ba 100644 --- a/tools/hoststubgen/TEST_MAPPING +++ b/tools/hoststubgen/TEST_MAPPING @@ -1,13 +1,63 @@ +// Keep the following two TEST_MAPPINGs in sync: +// frameworks/base/ravenwood/TEST_MAPPING +// frameworks/base/tools/hoststubgen/TEST_MAPPING { "presubmit": [ { "name": "tiny-framework-dump-test" }, { "name": "hoststubgentest" }, - { "name": "hoststubgen-invoke-test" } + { "name": "hoststubgen-invoke-test" }, + { + "name": "RavenwoodMockitoTest_device" + }, + { + "name": "RavenwoodBivalentTest_device" + }, + // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING + { + "name": "SystemUIGoogleTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ], + "presubmit-large": [ + { + "name": "SystemUITests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } ], "ravenwood-presubmit": [ { + "name": "RavenwoodMinimumTest", + "host": true + }, + { + "name": "RavenwoodMockitoTest", + "host": true + }, + { "name": "CtsUtilTestCasesRavenwood", "host": true + }, + { + "name": "RavenwoodCoreTest", + "host": true + }, + { + "name": "RavenwoodBivalentTest", + "host": true } ] } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 2f432cc7ac96..7212beb6ae4b 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -16,6 +16,7 @@ package com.android.hoststubgen import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.dumper.ApiDumper import com.android.hoststubgen.filters.AnnotationBasedFilter import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter import com.android.hoststubgen.filters.ConstantFilter @@ -89,7 +90,11 @@ class HostStubGen(val options: HostStubGenOptions) { log.i("Dump file created at $it") } options.apiListFile.ifSet { - PrintWriter(it).use { pw -> stats.dumpApis(pw) } + PrintWriter(it).use { pw -> + // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed + // framework-minus-apex.jar so that we can dump inherited methods from it. + ApiDumper(pw, allClasses, null, filter).dump() + } log.i("API list file created at $it") } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt index da6146911a21..9045db210495 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt @@ -15,7 +15,8 @@ */ package com.android.hoststubgen -import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.asm.getOuterClassNameFromFullClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName import com.android.hoststubgen.filters.FilterPolicyWithReason import org.objectweb.asm.Opcodes import java.io.PrintWriter @@ -55,8 +56,8 @@ open class HostStubGenStats { // Ignore methods where policy isn't relevant if (policy.isIgnoredForStats) return - val packageName = resolvePackageName(fullClassName) - val className = resolveOuterClassName(fullClassName) + val packageName = getPackageNameFromFullClassName(fullClassName) + val className = getOuterClassNameFromFullClassName(fullClassName) // Ignore methods for certain generated code if (className.endsWith("Proto") @@ -88,42 +89,4 @@ open class HostStubGenStats { } } } - - fun dumpApis(pw: PrintWriter) { - pw.printf("PackageName,ClassName,MethodName,MethodDesc\n") - apis.sortedWith(compareBy({ it.fullClassName }, { it.methodName }, { it.methodDesc })) - .forEach { api -> - pw.printf( - "%s,%s,%s,%s\n", - csvEscape(resolvePackageName(api.fullClassName)), - csvEscape(resolveClassName(api.fullClassName)), - csvEscape(api.methodName), - csvEscape(api.methodDesc), - ) - } - } - - private fun resolvePackageName(fullClassName: String): String { - val start = fullClassName.lastIndexOf('/') - return fullClassName.substring(0, start).toHumanReadableClassName() - } - - private fun resolveOuterClassName(fullClassName: String): String { - val start = fullClassName.lastIndexOf('/') - val end = fullClassName.indexOf('$') - if (end == -1) { - return fullClassName.substring(start + 1) - } else { - return fullClassName.substring(start + 1, end) - } - } - - private fun resolveClassName(fullClassName: String): String { - val pos = fullClassName.lastIndexOf('/') - if (pos == -1) { - return fullClassName - } else { - return fullClassName.substring(pos + 1) - } - } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index 83e122feeeb2..b8d18001f37b 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -76,17 +76,45 @@ fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): Strin return null } -private val removeLastElement = """[./][^./]*$""".toRegex() +val periodOrSlash = charArrayOf('.', '/') + +fun getPackageNameFromFullClassName(fullClassName: String): String { + val pos = fullClassName.lastIndexOfAny(periodOrSlash) + if (pos == -1) { + return "" + } else { + return fullClassName.substring(0, pos) + } +} + +fun getClassNameFromFullClassName(fullClassName: String): String { + val pos = fullClassName.lastIndexOfAny(periodOrSlash) + if (pos == -1) { + return fullClassName + } else { + return fullClassName.substring(pos + 1) + } +} -fun getPackageNameFromClassName(className: String): String { - return className.replace(removeLastElement, "") +fun getOuterClassNameFromFullClassName(fullClassName: String): String { + val start = fullClassName.lastIndexOfAny(periodOrSlash) + val end = fullClassName.indexOf('$') + if (end == -1) { + return fullClassName.substring(start + 1) + } else { + return fullClassName.substring(start + 1, end) + } } -fun resolveClassName(className: String, packageName: String): String { +/** + * If [className] is a fully qualified name, just return it. + * Otherwise, prepend [defaultPackageName]. + */ +fun resolveClassNameWithDefaultPackage(className: String, defaultPackageName: String): String { if (className.contains('.') || className.contains('/')) { return className } - return "$packageName.$className" + return "$defaultPackageName.$className" } fun String.toJvmClassName(): String { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt new file mode 100644 index 000000000000..aaefee4f71e8 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.hoststubgen.dumper + +import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME +import com.android.hoststubgen.asm.CTOR_NAME +import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.getClassNameFromFullClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.csvEscape +import com.android.hoststubgen.filters.FilterPolicy +import com.android.hoststubgen.filters.FilterPolicyWithReason +import com.android.hoststubgen.filters.OutputFilter +import com.android.hoststubgen.log +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import java.io.PrintWriter + +/** + * Dump all the API methods in [classes], with inherited methods, with their policies. + */ +class ApiDumper( + val pw: PrintWriter, + val classes: ClassNodes, + val frameworkClasses: ClassNodes?, + val filter: OutputFilter, +) { + private data class MethodKey( + val name: String, + val descriptor: String, + ) + + val javaStandardApiPolicy = FilterPolicy.Stub.withReason("Java standard API") + + private val shownMethods = mutableSetOf<MethodKey>() + + /** + * Do the dump. + */ + fun dump() { + pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" + + ",Supported,Policy,Reason\n") + + classes.forEach { classNode -> + shownMethods.clear() + dump(classNode, classNode) + } + } + + private fun dumpMethod( + classPackage: String, + className: String, + isSuperClass: Boolean, + methodClassName: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + pw.printf( + "%s,%s,%d,%s,%s,%s,%d,%s,%s\n", + csvEscape(classPackage), + csvEscape(className), + if (isSuperClass) { 1 } else { 0 }, + csvEscape(methodClassName), + csvEscape(methodName), + csvEscape(methodDesc), + if (policy.policy.isSupported) { 1 } else { 0 }, + policy.policy, + csvEscape(policy.reason), + ) + } + + private fun isDuplicate(methodName: String, methodDesc: String): Boolean { + val methodKey = MethodKey(methodName, methodDesc) + + if (shownMethods.contains(methodKey)) { + return true + } + shownMethods.add(methodKey) + return false + } + + private fun dump( + dumpClass: ClassNode, + methodClass: ClassNode, + ) { + val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + + val isSuperClass = dumpClass != methodClass + + methodClass.methods?.sortedWith(compareBy({ it.name }, { it.desc }))?.forEach { method -> + + // Don't print ctor's from super classes. + if (isSuperClass) { + if (CTOR_NAME == method.name || CLASS_INITIALIZER_NAME == method.name) { + return@forEach + } + } + // If we already printed the method from a subclass, don't print it. + if (isDuplicate(method.name, method.desc)) { + return@forEach + } + + val policy = filter.getPolicyForMethod(methodClass.name, method.name, method.desc) + + // Let's skip "Remove" APIs. Ideally we want to print it, just to make the CSV + // complete, we still need to hide methods substituted (== @RavenwoodReplace) methods + // and for now we don't have an easy way to detect it. + if (policy.policy == FilterPolicy.Remove) { + return@forEach + } + + val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc) + + dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(), + renameTo ?: method.name, method.desc, policy) + } + + // Dump super class methods. + dumpSuper(dumpClass, methodClass.superName) + + // Dump interface methods (which may have default methods). + methodClass.interfaces?.sorted()?.forEach { interfaceName -> + dumpSuper(dumpClass, interfaceName) + } + } + + /** + * Dump a given super class / interface. + */ + private fun dumpSuper( + dumpClass: ClassNode, + methodClassName: String, + ) { + classes.findClass(methodClassName)?.let { methodClass -> + dump(dumpClass, methodClass) + return + } + frameworkClasses?.findClass(methodClassName)?.let { methodClass -> + dump(dumpClass, methodClass) + return + } + if (methodClassName.startsWith("java/") || + methodClassName.startsWith("javax/") + ) { + dumpStandardClass(dumpClass, methodClassName) + return + } + log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.") + } + + /** + * Dump methods from Java standard classes. + */ + private fun dumpStandardClass( + dumpClass: ClassNode, + methodClassName: String, + ) { + val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + + val methodClassName = methodClassName.toHumanReadableClassName() + + try { + val clazz = Class.forName(methodClassName) + + // Method.getMethods() returns only public methods, but with inherited ones. + // Method.getDeclaredMethods() returns private methods too, but no inherited methods. + // + // Since we're only interested in public ones, just use getMethods(). + clazz.methods.forEach { method -> + val methodName = method.name + val methodDesc = Type.getMethodDescriptor(method) + + // If we already printed the method from a subclass, don't print it. + if (isDuplicate(methodName, methodDesc)) { + return@forEach + } + + dumpMethod(pkg, cls, true, methodClassName, + methodName, methodDesc, javaStandardApiPolicy) + } + } catch (e: ClassNotFoundException) { + log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.") + } + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index 45e140c8e3ff..6643492a1394 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -20,8 +20,8 @@ import com.android.hoststubgen.HostStubGenStats import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor -import com.android.hoststubgen.asm.getPackageNameFromClassName -import com.android.hoststubgen.asm.resolveClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.filters.FilterPolicyWithReason @@ -89,7 +89,7 @@ abstract class BaseAdapter ( ) { super.visit(version, access, name, signature, superName, interfaces) currentClassName = name - currentPackageName = getPackageNameFromClassName(name) + currentPackageName = getPackageNameFromFullClassName(name) classPolicy = filter.getPolicyForClass(currentClassName) log.d("[%s] visit: %s (package: %s)", this.javaClass.simpleName, name, currentPackageName) @@ -98,7 +98,8 @@ abstract class BaseAdapter ( log.indent() filter.getNativeSubstitutionClass(currentClassName)?.let { className -> - val fullClassName = resolveClassName(className, currentPackageName).toJvmClassName() + val fullClassName = resolveClassNameWithDefaultPackage(className, currentPackageName) + .toJvmClassName() log.d(" NativeSubstitutionClass: $fullClassName") if (classes.findClass(fullClassName) == null) { log.w("Native substitution class $fullClassName not found. Class must be " + |