diff options
216 files changed, 6244 insertions, 1426 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 3bddfe175c0d..20b9b0e2331d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -25477,34 +25477,34 @@ package android.media.audiofx { field public short preset; } - public class Virtualizer extends android.media.audiofx.AudioEffect { - ctor public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; - method public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean getStrengthSupported(); - method public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener); - method public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - field public static final int PARAM_STRENGTH = 1; // 0x1 - field public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0 - field public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1 - field public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2 - field public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0 - field public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3 - } - - public static interface Virtualizer.OnParameterChangeListener { - method public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short); - } - - public static class Virtualizer.Settings { - ctor public Virtualizer.Settings(); - ctor public Virtualizer.Settings(String); - field public short strength; + @Deprecated public class Virtualizer extends android.media.audiofx.AudioEffect { + ctor @Deprecated public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean getStrengthSupported(); + method @Deprecated public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener); + method @Deprecated public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + field @Deprecated public static final int PARAM_STRENGTH = 1; // 0x1 + field @Deprecated public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0 + field @Deprecated public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1 + field @Deprecated public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2 + field @Deprecated public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0 + field @Deprecated public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3 + } + + @Deprecated public static interface Virtualizer.OnParameterChangeListener { + method @Deprecated public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short); + } + + @Deprecated public static class Virtualizer.Settings { + ctor @Deprecated public Virtualizer.Settings(); + ctor @Deprecated public Virtualizer.Settings(String); + field @Deprecated public short strength; } public class Visualizer { @@ -44953,7 +44953,7 @@ package android.telephony { method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid); method public boolean canManageSubscription(android.telephony.SubscriptionInfo); - method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles(); + method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>); method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context); method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index debf1bfdfc8c..fe32bad40e1f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3959,6 +3959,7 @@ package android.content.pm { method public void setInstallAsInstantApp(boolean); method public void setInstallAsVirtualPreload(); method public void setRequestDowngrade(boolean); + method @FlaggedApi("android.content.pm.recoverability_detection") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackImpactLevel(int); method @FlaggedApi("android.content.pm.rollback_lifetime") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackLifetimeMillis(long); method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setStaged(); } @@ -4127,6 +4128,9 @@ package android.content.pm { field public static final int ROLLBACK_DATA_POLICY_RESTORE = 0; // 0x0 field public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; // 0x2 field public static final int ROLLBACK_DATA_POLICY_WIPE = 1; // 0x1 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_HIGH = 1; // 0x1 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_LOW = 0; // 0x0 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; // 0x2 field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_HIDDEN = 0; // 0x0 field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_VISIBLE = 1; // 0x1 field public static final int SYSTEM_APP_STATE_INSTALLED = 2; // 0x2 @@ -14712,7 +14716,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst(); - method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public android.telephony.CellIdentity getLastKnownCellIdentity(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity(); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Map<java.lang.Integer,java.lang.Integer> getLogicalToPhysicalSlotMapping(); method public int getMaxNumberOfSimultaneouslyActiveSims(); method public static long getMaxNumberVerificationTimeoutMillis(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 5d271cc10a69..b8b98a3cb3ba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -377,6 +377,7 @@ package android.app { public class NotificationManager { method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean); method public void cleanUpCallersAfter(long); + method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy(); method public android.content.ComponentName getEffectsSuppressor(); method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String); method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean); @@ -1210,6 +1211,10 @@ package android.content.res { package android.content.rollback { + public final class RollbackInfo implements android.os.Parcelable { + method @FlaggedApi("android.content.pm.recoverability_detection") public int getRollbackImpactLevel(); + } + public final class RollbackManager { method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void blockRollbackManager(long); method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void expireRollbackForPackage(@NonNull String); @@ -3023,6 +3028,10 @@ package android.service.notification { method @Deprecated public boolean isBound(); } + public final class ZenPolicy implements android.os.Parcelable { + method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy); + } + public static final class ZenPolicy.Builder { ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy); } diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index afa513dbaaef..c6712c044539 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -413,7 +413,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ public void setIntent(Intent startIntent) { - mStartIntent = startIntent; + if (startIntent != null) { + mStartIntent = startIntent.maybeStripForHistory(); + } } /** @@ -548,6 +550,8 @@ public final class ApplicationStartInfo implements Parcelable { /** * The intent used to launch the application. * + * <p class="note"> Note: Intent is stripped and does not include extras.</p> + * * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p> */ @SuppressLint("IntentBuilderName") @@ -662,6 +666,7 @@ public final class ApplicationStartInfo implements Parcelable { private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP = "timestamp"; private static final String PROTO_SERIALIZER_ATTRIBUTE_KEY = "key"; private static final String PROTO_SERIALIZER_ATTRIBUTE_TS = "ts"; + private static final String PROTO_SERIALIZER_ATTRIBUTE_INTENT = "intent"; /** * Write to a protocol buffer output stream. Protocol buffer message definition at {@link @@ -702,10 +707,17 @@ public final class ApplicationStartInfo implements Parcelable { } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { - Parcel parcel = Parcel.obtain(); - mStartIntent.writeToParcel(parcel, 0); - proto.write(ApplicationStartInfoProto.START_INTENT, parcel.marshall()); - parcel.recycle(); + ByteArrayOutputStream intentBytes = new ByteArrayOutputStream(); + ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes); + TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut); + serializer.startDocument(null, true); + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent.saveToXml(serializer); + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + serializer.endDocument(); + proto.write(ApplicationStartInfoProto.START_INTENT, + intentBytes.toByteArray()); + intentOut.close(); } proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode); proto.end(token); @@ -772,15 +784,17 @@ public final class ApplicationStartInfo implements Parcelable { mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); break; case (int) ApplicationStartInfoProto.START_INTENT: - byte[] startIntentBytes = proto.readBytes( - ApplicationStartInfoProto.START_INTENT); - if (startIntentBytes.length > 0) { - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(startIntentBytes, 0, startIntentBytes.length); - parcel.setDataPosition(0); - mStartIntent = Intent.CREATOR.createFromParcel(parcel); - parcel.recycle(); + ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes( + ApplicationStartInfoProto.START_INTENT)); + ObjectInputStream intentIn = new ObjectInputStream(intentBytes); + try { + TypedXmlPullParser parser = Xml.resolvePullParser(intentIn); + XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent = Intent.restoreFromXml(parser); + } catch (XmlPullParserException e) { + // Intent lost } + intentIn.close(); break; case (int) ApplicationStartInfoProto.LAUNCH_MODE: mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 7370fc36c23e..5b044f616487 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -119,7 +119,7 @@ interface IActivityClientController { oneway void setShowWhenLocked(in IBinder token, boolean showWhenLocked); oneway void setInheritShowWhenLocked(in IBinder token, boolean setInheritShownWhenLocked); - oneway void setTurnScreenOn(in IBinder token, boolean turnScreenOn); + void setTurnScreenOn(in IBinder token, boolean turnScreenOn); oneway void setAllowCrossUidActivitySwitchFromBelow(in IBinder token, boolean allowed); oneway void reportActivityFullyDrawn(in IBinder token, boolean restoredFromBundle); oneway void overrideActivityTransition(IBinder token, boolean open, int enterAnim, int exitAnim, diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index c3adbc30641f..578105f9f99e 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -38,6 +38,7 @@ import android.service.notification.IConditionProvider; import android.service.notification.INotificationListener; import android.service.notification.NotificationListenerFilter; import android.service.notification.StatusBarNotification; +import android.service.notification.ZenPolicy; import android.app.AutomaticZenRule; import android.service.notification.ZenModeConfig; @@ -213,6 +214,7 @@ interface INotificationManager boolean isNotificationPolicyAccessGrantedForPackage(String pkg); void setNotificationPolicyAccessGranted(String pkg, boolean granted); void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted); + ZenPolicy getDefaultZenPolicy(); AutomaticZenRule getAutomaticZenRule(String id); Map<String, AutomaticZenRule> getAutomaticZenRules(); // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 0b6e24cf5545..366b45badcfd 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1750,6 +1750,20 @@ public class NotificationManager { @NonNull ComponentName listener, boolean granted) { setNotificationListenerAccessGranted(listener, granted, true); } + /** + * Gets the device-default notification policy as a ZenPolicy. + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public @NonNull ZenPolicy getDefaultZenPolicy() { + INotificationManager service = getService(); + try { + return service.getDefaultZenPolicy(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, the diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 63f37f150d33..36b03c1b1f48 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -261,6 +261,13 @@ public class WallpaperManager { public static final String COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep"; /** + * Command for {@link #sendWallpaperCommand}: reported when a physical display switch event + * happens, e.g. fold and unfold. + * @hide + */ + public static final String COMMAND_DISPLAY_SWITCH = "android.wallpaper.displayswitch"; + + /** * Command for {@link #sendWallpaperCommand}: reported when the wallpaper that was already * set is re-applied by the user. * @hide diff --git a/core/java/android/content/pm/ArchivedActivityInfo.java b/core/java/android/content/pm/ArchivedActivityInfo.java index 1faa4373d88f..166d26503625 100644 --- a/core/java/android/content/pm/ArchivedActivityInfo.java +++ b/core/java/android/content/pm/ArchivedActivityInfo.java @@ -91,26 +91,31 @@ public final class ArchivedActivityInfo { * @hide */ public static Bitmap drawableToBitmap(Drawable drawable, int iconSize) { - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); - - } - Bitmap bitmap; - if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { - // Needed for drawables that are just a single color. - bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + if (drawable instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable) drawable).getBitmap(); } else { - bitmap = + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + // Needed for drawables that are just a single color. + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } else { + bitmap = Bitmap.createBitmap( - drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); + drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); } - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - if (iconSize > 0 && bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) { + if (iconSize <= 0) { + return bitmap; + } + + if (bitmap.getWidth() < iconSize || bitmap.getHeight() < iconSize + || bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) { var scaledBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true); if (scaledBitmap != bitmap) { bitmap.recycle(); @@ -235,7 +240,7 @@ public final class ArchivedActivityInfo { } @DataClass.Generated( - time = 1698789991876L, + time = 1705615445673L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/ArchivedActivityInfo.java", inputSignatures = "private @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.content.ComponentName mComponentName\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mIcon\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mMonochromeIcon\n @android.annotation.NonNull android.content.pm.ArchivedActivityParcel getParcel()\npublic static android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable)\npublic static android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable,int)\npublic static byte[] bytesFromBitmap(android.graphics.Bitmap)\nprivate static android.graphics.drawable.Drawable drawableFromCompressedBitmap(byte[])\nclass ArchivedActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genBuilder=false, genConstructor=false, genSetters=true)") diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index f0efed97d8ed..22926febfb2c 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2693,6 +2693,8 @@ public class PackageInstaller { /** @hide */ public long rollbackLifetimeMillis = 0; /** {@hide} */ + public int rollbackImpactLevel = PackageManager.ROLLBACK_USER_IMPACT_LOW; + /** {@hide} */ public boolean forceQueryableOverride; /** {@hide} */ public int requireUserAction = USER_ACTION_UNSPECIFIED; @@ -2749,6 +2751,7 @@ public class PackageInstaller { } rollbackDataPolicy = source.readInt(); rollbackLifetimeMillis = source.readLong(); + rollbackImpactLevel = source.readInt(); requireUserAction = source.readInt(); packageSource = source.readInt(); applicationEnabledSettingPersistent = source.readBoolean(); @@ -2783,6 +2786,7 @@ public class PackageInstaller { ret.dataLoaderParams = dataLoaderParams; ret.rollbackDataPolicy = rollbackDataPolicy; ret.rollbackLifetimeMillis = rollbackLifetimeMillis; + ret.rollbackImpactLevel = rollbackImpactLevel; ret.requireUserAction = requireUserAction; ret.packageSource = packageSource; ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; @@ -3121,6 +3125,28 @@ public class PackageInstaller { } /** + * rollbackImpactLevel is a measure of impact a rollback has on the user. This can take one + * of 3 values: + * <ul> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_LOW} (default)</li> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_HIGH} (1)</li> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_ONLY_MANUAL} (2)</li> + * </ul> + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) + @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION) + public void setRollbackImpactLevel(@PackageManager.RollbackImpactLevel int impactLevel) { + if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) { + throw new IllegalArgumentException( + "Can't set rollbackImpactLevel when rollback is not enabled"); + } + rollbackImpactLevel = impactLevel; + } + + /** * @deprecated use {@link #setRequestDowngrade(boolean)}. * {@hide} */ @@ -3493,6 +3519,7 @@ public class PackageInstaller { pw.printPair("dataLoaderParams", dataLoaderParams); pw.printPair("rollbackDataPolicy", rollbackDataPolicy); pw.printPair("rollbackLifetimeMillis", rollbackLifetimeMillis); + pw.printPair("rollbackImpactLevel", rollbackImpactLevel); pw.printPair("applicationEnabledSettingPersistent", applicationEnabledSettingPersistent); pw.printHexPair("developmentInstallFlags", developmentInstallFlags); @@ -3536,6 +3563,7 @@ public class PackageInstaller { } dest.writeInt(rollbackDataPolicy); dest.writeLong(rollbackLifetimeMillis); + dest.writeInt(rollbackImpactLevel); dest.writeInt(requireUserAction); dest.writeInt(packageSource); dest.writeBoolean(applicationEnabledSettingPersistent); @@ -3734,6 +3762,9 @@ public class PackageInstaller { public long rollbackLifetimeMillis; /** {@hide} */ + public int rollbackImpactLevel; + + /** {@hide} */ public int requireUserAction; /** {@hide} */ @@ -3801,6 +3832,7 @@ public class PackageInstaller { isPreapprovalRequested = source.readBoolean(); rollbackDataPolicy = source.readInt(); rollbackLifetimeMillis = source.readLong(); + rollbackImpactLevel = source.readInt(); createdMillis = source.readLong(); requireUserAction = source.readInt(); installerUid = source.readInt(); @@ -4438,6 +4470,7 @@ public class PackageInstaller { dest.writeBoolean(isPreapprovalRequested); dest.writeInt(rollbackDataPolicy); dest.writeLong(rollbackLifetimeMillis); + dest.writeInt(rollbackImpactLevel); dest.writeLong(createdMillis); dest.writeInt(requireUserAction); dest.writeInt(installerUid); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index aabbe698703c..4724e866f094 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1501,6 +1501,44 @@ public abstract class PackageManager { public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; /** @hide */ + @IntDef(prefix = {"ROLLBACK_USER_IMPACT_"}, value = { + ROLLBACK_USER_IMPACT_LOW, + ROLLBACK_USER_IMPACT_HIGH, + ROLLBACK_USER_IMPACT_ONLY_MANUAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RollbackImpactLevel {} + + /** + * Rollback will be performed automatically in response to native crashes on startup or + * persistent service crashes. More suitable for apps that do not store any user data. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_LOW = 0; + + /** + * Rollback will be performed automatically only when the device is found to be unrecoverable. + * More suitable for apps that store user data and have higher impact on user. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_HIGH = 1; + + /** + * Rollback will not be performed automatically. It can be triggered externally. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; + + /** @hide */ @IntDef(flag = true, prefix = { "INSTALL_" }, value = { INSTALL_REPLACE_EXISTING, INSTALL_ALLOW_TEST, diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index a2cd3e153b3e..e4e9fbaf2c55 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -146,3 +146,10 @@ flag { bug: "281848623" } +flag { + name: "recoverability_detection" + namespace: "package_manager_service" + description: "Feature flag to enable recoverability detection feature. It includes GMS core rollback and improvements to rescue party." + bug: "291135724" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index a4db733af013..bd74b0b9293c 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,6 +49,13 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); + public Builder() {} + + public Builder(@NonNull OverlayPaths base) { + mPaths.mResourceDirs.addAll(base.getResourceDirs()); + mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); + } + /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java index a363718a8b1d..d128055fec6d 100644 --- a/core/java/android/content/rollback/RollbackInfo.java +++ b/core/java/android/content/rollback/RollbackInfo.java @@ -16,8 +16,12 @@ package android.content.rollback; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.content.pm.Flags; +import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.os.Parcel; import android.os.Parcelable; @@ -25,17 +29,14 @@ import android.os.Parcelable; import java.util.List; /** - * Information about a set of packages that can be, or already have been - * rolled back together. + * Information about a set of packages that can be, or already have been rolled back together. * * @hide */ @SystemApi public final class RollbackInfo implements Parcelable { - /** - * A unique identifier for the rollback. - */ + /** A unique identifier for the rollback. */ private final int mRollbackId; private final List<PackageRollbackInfo> mPackages; @@ -44,15 +45,39 @@ public final class RollbackInfo implements Parcelable { private final boolean mIsStaged; private int mCommittedSessionId; + private int mRollbackImpactLevel; /** @hide */ - public RollbackInfo(int rollbackId, List<PackageRollbackInfo> packages, boolean isStaged, - List<VersionedPackage> causePackages, int committedSessionId) { + public RollbackInfo( + int rollbackId, + List<PackageRollbackInfo> packages, + boolean isStaged, + List<VersionedPackage> causePackages, + int committedSessionId, + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { this.mRollbackId = rollbackId; this.mPackages = packages; this.mIsStaged = isStaged; this.mCausePackages = causePackages; this.mCommittedSessionId = committedSessionId; + this.mRollbackImpactLevel = rollbackImpactLevel; + } + + /** @hide */ + public RollbackInfo( + int rollbackId, + List<PackageRollbackInfo> packages, + boolean isStaged, + List<VersionedPackage> causePackages, + int committedSessionId) { + // If impact level is not set default to 0 + this( + rollbackId, + packages, + isStaged, + causePackages, + committedSessionId, + PackageManager.ROLLBACK_USER_IMPACT_LOW); } private RollbackInfo(Parcel in) { @@ -61,34 +86,28 @@ public final class RollbackInfo implements Parcelable { mIsStaged = in.readBoolean(); mCausePackages = in.createTypedArrayList(VersionedPackage.CREATOR); mCommittedSessionId = in.readInt(); + mRollbackImpactLevel = in.readInt(); } - /** - * Returns a unique identifier for this rollback. - */ + /** Returns a unique identifier for this rollback. */ public int getRollbackId() { return mRollbackId; } - /** - * Returns the list of package that are rolled back. - */ + /** Returns the list of package that are rolled back. */ @NonNull public List<PackageRollbackInfo> getPackages() { return mPackages; } - /** - * Returns true if this rollback requires reboot to take effect after - * being committed. - */ + /** Returns true if this rollback requires reboot to take effect after being committed. */ public boolean isStaged() { return mIsStaged; } /** - * Returns the session ID for the committed rollback for staged rollbacks. - * Only applicable for rollbacks that have been committed. + * Returns the session ID for the committed rollback for staged rollbacks. Only applicable for + * rollbacks that have been committed. */ public int getCommittedSessionId() { return mCommittedSessionId; @@ -96,6 +115,7 @@ public final class RollbackInfo implements Parcelable { /** * Sets the session ID for the committed rollback for staged rollbacks. + * * @hide */ public void setCommittedSessionId(int sessionId) { @@ -103,15 +123,40 @@ public final class RollbackInfo implements Parcelable { } /** - * Gets the list of package versions that motivated this rollback. - * As provided to {@link #commitRollback} when the rollback was committed. - * This is only applicable for rollbacks that have been committed. + * Gets the list of package versions that motivated this rollback. As provided to {@link + * #commitRollback} when the rollback was committed. This is only applicable for rollbacks that + * have been committed. */ @NonNull public List<VersionedPackage> getCausePackages() { return mCausePackages; } + /** + * Get rollback impact level. Refer {@link + * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info + * on impact level. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION) + public @PackageManager.RollbackImpactLevel int getRollbackImpactLevel() { + return mRollbackImpactLevel; + } + + /** + * Set rollback impact level. Refer {@link + * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info + * on impact level. + * + * @hide + */ + public void setRollbackImpactLevel( + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { + mRollbackImpactLevel = rollbackImpactLevel; + } + @Override public int describeContents() { return 0; @@ -124,16 +169,17 @@ public final class RollbackInfo implements Parcelable { out.writeBoolean(mIsStaged); out.writeTypedList(mCausePackages); out.writeInt(mCommittedSessionId); + out.writeInt(mRollbackImpactLevel); } public static final @android.annotation.NonNull Parcelable.Creator<RollbackInfo> CREATOR = new Parcelable.Creator<RollbackInfo>() { - public RollbackInfo createFromParcel(Parcel in) { - return new RollbackInfo(in); - } - - public RollbackInfo[] newArray(int size) { - return new RollbackInfo[size]; - } - }; + public RollbackInfo createFromParcel(Parcel in) { + return new RollbackInfo(in); + } + + public RollbackInfo[] newArray(int size) { + return new RollbackInfo[size]; + } + }; } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 8a4f678b52f2..35ae3c9e23c7 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -40,12 +40,15 @@ import android.os.Looper; import android.os.OperationCanceledException; import android.os.SystemProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Printer; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.NeverCompile; @@ -103,8 +106,14 @@ public final class SQLiteDatabase extends SQLiteClosable { // Stores reference to all databases opened in the current process. // (The referent Object is not used at this time.) // INVARIANT: Guarded by sActiveDatabases. + @GuardedBy("sActiveDatabases") private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>(); + // Tracks which database files are currently open. If a database file is opened more than + // once at any given moment, the associated databases are marked as "concurrent". + @GuardedBy("sActiveDatabases") + private static final OpenTracker sOpenTracker = new OpenTracker(); + // Thread-local for database sessions that belong to this database. // Each thread has its own database session. // INVARIANT: Immutable. @@ -510,6 +519,7 @@ public final class SQLiteDatabase extends SQLiteClosable { private void dispose(boolean finalized) { final SQLiteConnectionPool pool; + final String path; synchronized (mLock) { if (mCloseGuardLocked != null) { if (finalized) { @@ -520,10 +530,12 @@ public final class SQLiteDatabase extends SQLiteClosable { pool = mConnectionPoolLocked; mConnectionPoolLocked = null; + path = isInMemoryDatabase() ? null : getPath(); } if (!finalized) { synchronized (sActiveDatabases) { + sOpenTracker.close(path); sActiveDatabases.remove(this); } @@ -1132,6 +1144,74 @@ public final class SQLiteDatabase extends SQLiteClosable { } } + /** + * Track the number of times a database file has been opened. There is a primary connection + * associated with every open database, and these can contend with each other, leading to + * unexpected SQLiteDatabaseLockedException exceptions. The tracking here is only advisory: + * multiply-opened databases are logged but no other action is taken. + * + * This class is not thread-safe. + */ + private static class OpenTracker { + // The list of currently-open databases. This maps the database file to the number of + // currently-active opens. + private final ArrayMap<String, Integer> mOpens = new ArrayMap<>(); + + // The maximum number of concurrently open database paths that will be stored. Once this + // many paths have been recorded, further paths are logged but not saved. + private static final int MAX_RECORDED_PATHS = 20; + + // The list of databases that were ever concurrently opened. + private final ArraySet<String> mConcurrent = new ArraySet<>(); + + /** Return the canonical path. On error, just return the input path. */ + private static String normalize(String path) { + try { + return new File(path).toPath().toRealPath().toString(); + } catch (Exception e) { + // If there is an IO or security exception, just continue, using the input path. + return path; + } + } + + /** Return true if the path is currently open in another SQLiteDatabase instance. */ + void open(@Nullable String path) { + if (path == null) return; + path = normalize(path); + + Integer count = mOpens.get(path); + if (count == null || count == 0) { + mOpens.put(path, 1); + return; + } else { + mOpens.put(path, count + 1); + if (mConcurrent.size() < MAX_RECORDED_PATHS) { + mConcurrent.add(path); + } + Log.w(TAG, "multiple primary connections on " + path); + return; + } + } + + void close(@Nullable String path) { + if (path == null) return; + path = normalize(path); + Integer count = mOpens.get(path); + if (count == null || count <= 0) { + Log.e(TAG, "open database counting failure on " + path); + } else if (count == 1) { + // Implicitly set the count to zero, and make mOpens smaller. + mOpens.remove(path); + } else { + mOpens.put(path, count - 1); + } + } + + ArraySet<String> getConcurrentDatabasePaths() { + return new ArraySet<>(mConcurrent); + } + } + private void open() { try { try { @@ -1153,14 +1233,17 @@ public final class SQLiteDatabase extends SQLiteClosable { } private void openInner() { + final String path; synchronized (mLock) { assert mConnectionPoolLocked == null; mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); mCloseGuardLocked.open("close"); + path = isInMemoryDatabase() ? null : getPath(); } synchronized (sActiveDatabases) { sActiveDatabases.put(this, null); + sOpenTracker.open(path); } } @@ -2345,6 +2428,17 @@ public final class SQLiteDatabase extends SQLiteClosable { } /** + * Return list of databases that have been concurrently opened. + * @hide + */ + @VisibleForTesting + public static ArraySet<String> getConcurrentDatabasePaths() { + synchronized (sActiveDatabases) { + return sOpenTracker.getConcurrentDatabasePaths(); + } + } + + /** * Returns true if the new version code is greater than the current database version. * * @param newVersion The new version code. @@ -2766,6 +2860,19 @@ public final class SQLiteDatabase extends SQLiteClosable { dumpDatabaseDirectory(printer, new File(dir), isSystem); } } + + // Dump concurrently-opened database files, if any + final ArraySet<String> concurrent; + synchronized (sActiveDatabases) { + concurrent = sOpenTracker.getConcurrentDatabasePaths(); + } + if (concurrent.size() > 0) { + printer.println(""); + printer.println("Concurrently opened database files"); + for (String f : concurrent) { + printer.println(" " + f); + } + } } private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) { diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 39b6aeb814f6..8c7050176506 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -86,3 +86,11 @@ flag { description: "This flag is used to enabled the Wallet Role for all users on the device" bug: "283989236" } + +flag { + name: "signature_permission_allowlist_enabled" + is_fixed_read_only: true + namespace: "permissions" + description: "Enable signature permission allowlist" + bug: "308573169" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ab98c94cba73..d946430f18af 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3243,9 +3243,17 @@ public final class Settings { private static final String NAME_EQ_PLACEHOLDER = "name=?"; + // Cached values of queried settings. + // Key is the setting's name, value is the setting's value. // Must synchronize on 'this' to access mValues and mValuesVersion. private final ArrayMap<String, String> mValues = new ArrayMap<>(); + // Cached values for queried prefixes. + // Key is the prefix, value is all of the settings under the prefix, mapped from a setting's + // name to a setting's value. The name string doesn't include the prefix. + // Must synchronize on 'this' to access. + private final ArrayMap<String, ArrayMap<String, String>> mPrefixToValues = new ArrayMap<>(); + private final Uri mUri; @UnsupportedAppUsage private final ContentProviderHolder mProviderHolder; @@ -3592,15 +3600,13 @@ public final class Settings { || applicationInfo.isSignedWithPlatformKey(); } - private ArrayMap<String, String> getStringsForPrefixStripPrefix( - ContentResolver cr, String prefix, String[] names) { + private Map<String, String> getStringsForPrefixStripPrefix( + ContentResolver cr, String prefix, List<String> names) { String namespace = prefix.substring(0, prefix.length() - 1); ArrayMap<String, String> keyValues = new ArrayMap<>(); int substringLength = prefix.length(); - int currentGeneration = -1; boolean needsGenerationTracker = false; - synchronized (NameValueCache.this) { final GenerationTracker generationTracker = mGenerationTrackers.get(prefix); if (generationTracker != null) { @@ -3614,40 +3620,24 @@ public final class Settings { // generation tracker and request a new one generationTracker.destroy(); mGenerationTrackers.remove(prefix); - for (int i = mValues.size() - 1; i >= 0; i--) { - String key = mValues.keyAt(i); - if (key.startsWith(prefix)) { - mValues.remove(key); - } - } + mPrefixToValues.remove(prefix); needsGenerationTracker = true; } else { - boolean prefixCached = mValues.containsKey(prefix); - if (prefixCached) { - if (DEBUG) { - Log.i(TAG, "Cache hit for prefix:" + prefix); - } - if (names.length > 0) { + final ArrayMap<String, String> cachedSettings = mPrefixToValues.get(prefix); + if (cachedSettings != null) { + if (!names.isEmpty()) { for (String name : names) { - // mValues can contain "null" values, need to use containsKey. - if (mValues.containsKey(name)) { + // The cache can contain "null" values, need to use containsKey. + if (cachedSettings.containsKey(name)) { keyValues.put( - name.substring(substringLength), - mValues.get(name)); + name, + cachedSettings.get(name)); } } } else { - for (int i = 0; i < mValues.size(); ++i) { - String key = mValues.keyAt(i); - // Explicitly exclude the prefix as it is only there to - // signal that the prefix has been cached. - if (key.startsWith(prefix) && !key.equals(prefix)) { - String value = mValues.valueAt(i); - keyValues.put( - key.substring(substringLength), - value); - } - } + keyValues.putAll(cachedSettings); + // Remove the hack added for the legacy behavior. + keyValues.remove(""); } return keyValues; } @@ -3657,7 +3647,6 @@ public final class Settings { needsGenerationTracker = true; } } - if (mCallListCommand == null) { // No list command specified, return empty map return keyValues; @@ -3702,20 +3691,23 @@ public final class Settings { } // All flags for the namespace - Map<String, String> flagsToValues = + HashMap<String, String> flagsToValues = (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class); + if (flagsToValues == null) { + return keyValues; + } // Only the flags requested by the caller - if (names.length > 0) { + if (!names.isEmpty()) { for (String name : names) { // flagsToValues can contain "null" values, need to use containsKey. - if (flagsToValues.containsKey(name)) { + final String key = Config.createCompositeName(namespace, name); + if (flagsToValues.containsKey(key)) { keyValues.put( - name.substring(substringLength), - flagsToValues.get(name)); + name, + flagsToValues.get(key)); } } } else { - keyValues.ensureCapacity(keyValues.size() + flagsToValues.size()); for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { keyValues.put( flag.getKey().substring(substringLength), @@ -3751,10 +3743,18 @@ public final class Settings { if (DEBUG) { Log.i(TAG, "Updating cache for prefix:" + prefix); } - // cache the complete list of flags for the namespace - mValues.putAll(flagsToValues); - // Adding the prefix as a signal that the prefix is cached. - mValues.put(prefix, null); + // Cache the complete list of flags for the namespace for bulk queries. + // In this cached list, the setting's name doesn't include the prefix. + ArrayMap<String, String> namesToValues = + new ArrayMap<>(flagsToValues.size() + 1); + for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { + namesToValues.put( + flag.getKey().substring(substringLength), + flag.getValue()); + } + // Legacy behavior, we return <"", null> when queried with name = "" + namesToValues.put("", null); + mPrefixToValues.put(prefix, namesToValues); } } return keyValues; @@ -19945,16 +19945,9 @@ public final class Settings { @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public static Map<String, String> getStrings(@NonNull ContentResolver resolver, @NonNull String namespace, @NonNull List<String> names) { - String[] compositeNames = new String[names.size()]; - for (int i = 0, size = names.size(); i < size; ++i) { - compositeNames[i] = createCompositeName(namespace, names.get(i)); - } - String prefix = createPrefix(namespace); - ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix( - resolver, prefix, compositeNames); - return keyValues; + return sNameValueCache.getStringsForPrefixStripPrefix(resolver, prefix, names); } /** @@ -20276,7 +20269,7 @@ public final class Settings { } } - private static String createCompositeName(@NonNull String namespace, @NonNull String name) { + static String createCompositeName(@NonNull String namespace, @NonNull String name) { Preconditions.checkNotNull(namespace); Preconditions.checkNotNull(name); var sb = new StringBuilder(namespace.length() + 1 + name.length()); diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index c479877fe98e..9895551a8672 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -185,7 +185,13 @@ public class ZenModeConfig implements Parcelable { SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT; - public static final int XML_VERSION = 8; + // ZenModeConfig XML versions distinguishing key changes. + public static final int XML_VERSION_ZEN_UPGRADE = 8; + public static final int XML_VERSION_MODES_API = 11; + + // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when + // modes_api is inlined. + private static final int XML_VERSION = 10; public static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; @@ -586,6 +592,10 @@ public class ZenModeConfig implements Parcelable { } } + public static int getCurrentXmlVersion() { + return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION; + } + public static ZenModeConfig readXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -593,7 +603,7 @@ public class ZenModeConfig implements Parcelable { String tag = parser.getName(); if (!ZEN_TAG.equals(tag)) return null; final ZenModeConfig rt = new ZenModeConfig(); - rt.version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION); + rt.version = safeInt(parser, ZEN_ATT_VERSION, getCurrentXmlVersion()); rt.user = safeInt(parser, ZEN_ATT_USER, rt.user); boolean readSuppressedEffects = false; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { @@ -707,14 +717,16 @@ public class ZenModeConfig implements Parcelable { /** * Writes XML of current ZenModeConfig * @param out serializer - * @param version uses XML_VERSION if version is null + * @param version uses the current XML version if version is null * @throws IOException */ + public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup) throws IOException { + int xmlVersion = getCurrentXmlVersion(); out.startTag(null, ZEN_TAG); out.attribute(null, ZEN_ATT_VERSION, version == null - ? Integer.toString(XML_VERSION) : Integer.toString(version)); + ? Integer.toString(xmlVersion) : Integer.toString(version)); out.attributeInt(null, ZEN_ATT_USER, user); out.startTag(null, ALLOW_TAG); out.attributeBoolean(null, ALLOW_ATT_CALLS, allowCalls); diff --git a/core/java/android/service/notification/ZenPolicy.aidl b/core/java/android/service/notification/ZenPolicy.aidl new file mode 100644 index 000000000000..b56f5c6989bf --- /dev/null +++ b/core/java/android/service/notification/ZenPolicy.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +parcelable ZenPolicy;
\ No newline at end of file diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index fb491d010f54..d8318a6bee7c 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -1422,6 +1422,54 @@ public final class ZenPolicy implements Parcelable { } /** + * Overwrites any policy values in this ZenPolicy with set values from newPolicy and + * returns a copy of the resulting ZenPolicy. + * Unlike apply(), values set in newPolicy will always be kept over pre-existing + * fields. Any values in newPolicy that are not set keep their currently set values. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) { + ZenPolicy result = this.copy(); + + if (newPolicy == null) { + return result; + } + + // set priority categories + for (int category = 0; category < mPriorityCategories.size(); category++) { + @State int newState = newPolicy.mPriorityCategories.get(category); + if (newState != STATE_UNSET) { + result.mPriorityCategories.set(category, newState); + + if (category == PRIORITY_CATEGORY_MESSAGES) { + result.mPriorityMessages = newPolicy.mPriorityMessages; + } else if (category == PRIORITY_CATEGORY_CALLS) { + result.mPriorityCalls = newPolicy.mPriorityCalls; + } else if (category == PRIORITY_CATEGORY_CONVERSATIONS) { + result.mConversationSenders = newPolicy.mConversationSenders; + } + } + } + + // set visual effects + for (int visualEffect = 0; visualEffect < mVisualEffects.size(); visualEffect++) { + if (newPolicy.mVisualEffects.get(visualEffect) != STATE_UNSET) { + result.mVisualEffects.set(visualEffect, newPolicy.mVisualEffects.get(visualEffect)); + } + } + + // set allowed channels + if (newPolicy.mAllowChannels != CHANNEL_POLICY_UNSET) { + result.mAllowChannels = newPolicy.mAllowChannels; + } + + return result; + } + + /** * @hide */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 1a2be15b2e8d..76e0c259b38f 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -16,6 +16,7 @@ package android.service.wallpaper; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; @@ -153,6 +154,7 @@ public abstract class WallpaperService extends Service { static final boolean DEBUG = false; static final float MIN_PAGE_ALLOWED_MARGIN = .05f; private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64; + private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000; private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); @@ -165,6 +167,7 @@ public abstract class WallpaperService extends Service { private static final int MSG_UPDATE_SURFACE = 10000; private static final int MSG_VISIBILITY_CHANGED = 10010; + private static final int MSG_REFRESH_VISIBILITY = 10011; private static final int MSG_WALLPAPER_OFFSETS = 10020; private static final int MSG_WALLPAPER_COMMAND = 10025; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -248,6 +251,11 @@ public abstract class WallpaperService extends Service { */ private boolean mIsScreenTurningOn; boolean mReportedVisible; + /** + * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility + * changes if the display may be toggled in a short time, e.g. display switch. + */ + boolean mPreserveVisible; boolean mDestroyed; // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once @@ -1084,6 +1092,9 @@ public abstract class WallpaperService extends Service { if (pendingCount != 0) { out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount); } + if (mPreserveVisible) { + out.print(prefix); out.print("mPreserveVisible=true"); + } synchronized (mLock) { out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset); out.print(" mPendingXOffset="); out.println(mPendingXOffset); @@ -1643,7 +1654,8 @@ public abstract class WallpaperService extends Service { ? false : mIWallpaperEngine.mInfo.supportsAmbientMode(); // Report visibility only if display is fully on or wallpaper supports ambient mode. - boolean visible = mVisible && (displayFullyOn || supportsAmbientMode); + final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode)) + || mPreserveVisible; if (DEBUG) { Log.v( TAG, @@ -2080,6 +2092,9 @@ public abstract class WallpaperService extends Service { if (!mDestroyed) { if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) { updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action)); + } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) { + handleDisplaySwitch(cmd.z == 1 /* startToSwitch */); + return; } result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z, cmd.extras, cmd.sync); @@ -2095,6 +2110,23 @@ public abstract class WallpaperService extends Service { } } + private void handleDisplaySwitch(boolean startToSwitch) { + if (startToSwitch && mReportedVisible) { + // The display may be off/on in a short time when the display is switching. + // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so + // the rendering thread can be active to redraw in time when receiving size change. + mPreserveVisible = true; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY), + PRESERVE_VISIBLE_TIMEOUT_MS); + } else if (!startToSwitch && mPreserveVisible) { + // The switch is finished, so restore to actual visibility. + mPreserveVisible = false; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + reportVisibility(false /* forceReport */); + } + } + private void updateFrozenState(boolean frozenRequested) { if (mIWallpaperEngine.mInfo == null // Procees the unfreeze command in case the wallaper became static while @@ -2638,6 +2670,10 @@ public abstract class WallpaperService extends Service { + ": " + message.arg1); mEngine.doVisibilityChanged(message.arg1 != 0); break; + case MSG_REFRESH_VISIBILITY: + mEngine.mPreserveVisible = false; + mEngine.reportVisibility(false /* forceReport */); + break; case MSG_UPDATE_SCREEN_TURNING_ON: if (DEBUG) { Log.v(TAG, diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 8935ab38451a..0a813a3edb1f 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -38,6 +38,7 @@ import android.telephony.Annotation.PreciseDisconnectCauses; import android.telephony.Annotation.RadioPowerState; import android.telephony.Annotation.SimActivationState; import android.telephony.Annotation.SrvccState; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager.CarrierPrivilegesCallback; import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsCallSession; @@ -116,14 +117,15 @@ public class TelephonyRegistryManager { } /** - * Register for changes to the list of active {@link SubscriptionInfo} records or to the - * individual records themselves. When a change occurs the onSubscriptionsChanged method of - * the listener will be invoked immediately if there has been a notification. The - * onSubscriptionChanged method will also be triggered once initially when calling this - * function. + * Register for changes to the list of {@link SubscriptionInfo} records or to the + * individual records (active or inactive) themselves. When a change occurs, the + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of + * the listener will be invoked immediately. The + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked + * once initially when calling this method. * - * @param listener an instance of {@link SubscriptionManager.OnSubscriptionsChangedListener} - * with onSubscriptionsChanged overridden. + * @param listener an instance of {@link OnSubscriptionsChangedListener} with + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden. * @param executor the executor that will execute callbacks. * @hide */ diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e0bda9181380..3c36227eda0a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,7 +26,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; -import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; @@ -1012,10 +1011,8 @@ public final class ViewRootImpl implements ViewParent, // Used to check if there were any view invalidations in // the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME). private boolean mHasInvalidation = false; - // Used to check if it is in the frame rate boosting period. + // Used to check if it is in the touch boosting period. private boolean mIsFrameRateBoosting = false; - // Used to check if it is in touch boosting period. - private boolean mIsTouchBoosting = false; // Used to check if there is a message in the message queue // for idleness handling. private boolean mHasIdledMessage = false; @@ -6424,12 +6421,11 @@ public final class ViewRootImpl implements ViewParent, * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). */ mIsFrameRateBoosting = false; - mIsTouchBoosting = false; setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory, mLastPreferredFrameRateCategory)); break; case MSG_CHECK_INVALIDATION_IDLE: - if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) { + if (!mHasInvalidation && !mIsFrameRateBoosting) { mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; setPreferredFrameRateCategory(mPreferredFrameRateCategory); mHasIdledMessage = false; @@ -7452,7 +7448,7 @@ public final class ViewRootImpl implements ViewParent, // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { // set the frame rate to the maximum value. - mIsTouchBoosting = true; + mIsFrameRateBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); } /** @@ -12204,16 +12200,8 @@ public final class ViewRootImpl implements ViewParent, return; } - int frameRateCategory = mIsTouchBoosting - ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory; - - // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT - // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction. - // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction - // (e.g., Window Initialization). - if (mIsFrameRateBoosting || mInsetsAnimationRunning) { - frameRateCategory = FRAME_RATE_CATEGORY_HIGH; - } + int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning + ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; try { if (mLastPreferredFrameRateCategory != frameRateCategory) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index efae57c9946c..a11ac7cb48ad 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -94,6 +94,13 @@ flag { } flag { + name: "skip_accessibility_warning_dialog_for_trusted_services" + namespace: "accessibility" + description: "Skips showing the accessibility warning dialog for trusted services." + bug: "303511250" +} + +flag { namespace: "accessibility" name: "update_always_on_a11y_service" description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut." diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index c20b278f7eaa..7f5331b936e9 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -167,6 +167,11 @@ public class WindowTokenClient extends Binder { + ", reported config=" + currentConfig + ", updated config=" + newConfig); } + // Update display first. In case callers want to obtain display information( + // ex: DisplayMetrics) in #onConfigurationChanged callback. + if (displayChanged) { + context.updateDisplay(newDisplayId); + } if (shouldUpdateResources) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); @@ -195,9 +200,6 @@ public class WindowTokenClient extends Binder { } } } - if (displayChanged) { - context.updateDisplay(newDisplayId); - } } /** diff --git a/core/res/res/drawable/autofill_half_sheet_divider.xml b/core/res/res/drawable/autofill_half_sheet_divider.xml new file mode 100644 index 000000000000..1a96c7dc263e --- /dev/null +++ b/core/res/res/drawable/autofill_half_sheet_divider.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copied from //frameworks/base/core/res/res/drawable/list_divider_material.xml. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="@color/foreground_material_light"> + <solid android:color="#1f000000" /> + <size + android:height="1dp" + android:width="1dp"/> +</shape>
\ No newline at end of file diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 27f8138ac5e3..ddedca2865c4 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -31,11 +31,11 @@ android:gravity="center_horizontal" android:orientation="vertical"> <ScrollView + android:id="@+id/autofill_sheet_scroll_view" android:layout_width="fill_parent" android:layout_height="0dp" android:fillViewport="true" - android:layout_weight="1" - android:layout_marginBottom="8dp"> + android:layout_weight="1"> <LinearLayout android:layout_marginStart="@dimen/autofill_save_outer_margin" android:layout_marginEnd="@dimen/autofill_save_outer_margin" @@ -66,16 +66,25 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:visibility="gone"/> - + <View + android:id="@+id/autofill_sheet_scroll_view_space" + android:layout_width="match_parent" + android:layout_height="16dp"/> </LinearLayout> </ScrollView> + <View + android:id="@+id/autofill_sheet_divider" + android:layout_width="match_parent" + android:layout_height="1dp" + style="@style/AutofillHalfSheetDivider" /> + <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="end" android:clipToPadding="false" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton" android:orientation="horizontal" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9e1400641084..5e2aacdb24af 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4497,6 +4497,16 @@ <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> + <!-- Array of component names, each flattened to a string, for accessibility services that + can be enabled by the user without showing a warning prompt. These services must be + preinstalled. --> + <string-array translatable="false" name="config_trustedAccessibilityServices"> + <!-- + <item>com.example.package.first/com.example.class.FirstService</item> + <item>com.example.package.second/com.example.class.SecondService</item> + --> + </string-array> + <!-- Warning: This API can be dangerous when not implemented properly. In particular, escrow token must NOT be retrievable from device storage. In other words, either escrow token is not stored on device or its ciphertext is stored on device while @@ -6891,4 +6901,8 @@ <!-- Defines suitability of the built-in speaker route. Refer to {@link MediaRoute2Info} to see supported values. --> <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer> + + <!-- Whether to show a percentage text next to the progressbar while preparing to update the + device --> + <bool name="config_showPercentageTextDuringRebootToUpdate">true</bool> </resources> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 619ec31e37bc..22d028cb079e 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1515,6 +1515,11 @@ please see styles_device_defaults.xml. <item name="background">@drawable/btn_outlined</item> </style> + <!-- @hide Divider for Autofill half screen dialog --> + <style name="AutofillHalfSheetDivider"> + <item name="android:background">@drawable/autofill_half_sheet_divider</item> + </style> + <!-- @hide Autofill background for popup window (not for fullscreen) --> <style name="AutofillDatasetPicker"> <item name="elevation">4dp</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef12d8f4ac0f..33ea02ac8172 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3630,6 +3630,7 @@ <java-symbol type="string" name="config_defaultAccessibilityService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> + <java-symbol type="array" name="config_trustedAccessibilityServices" /> <java-symbol type="string" name="accessibility_select_shortcut_menu_title" /> <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" /> @@ -3703,6 +3704,10 @@ <java-symbol type="id" name="autofill_dataset_list"/> <java-symbol type="id" name="autofill_dataset_picker"/> <java-symbol type="id" name="autofill_dataset_title" /> + <java-symbol type="id" name="autofill_sheet_divider"/> + <java-symbol type="id" name="autofill_sheet_scroll_view"/> + <java-symbol type="id" name="autofill_sheet_scroll_view_space"/> + <java-symbol type="id" name="autofill_save_custom_subtitle" /> <java-symbol type="id" name="autofill_save_icon" /> <java-symbol type="id" name="autofill_save_no" /> @@ -5312,4 +5317,7 @@ <!-- Android MediaRouter framework configs. --> <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" /> + + <!-- Shutdown thread config flags --> + <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" /> </resources> diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index e118c98dd4da..3ee565f8e025 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -403,4 +403,41 @@ public class SQLiteDatabaseTest { } assertFalse(allowed); } + + /** Return true if the path is in the list of strings. */ + private boolean isConcurrent(String path) throws Exception { + path = new File(path).toPath().toRealPath().toString(); + return SQLiteDatabase.getConcurrentDatabasePaths().contains(path); + } + + @Test + public void testDuplicateDatabases() throws Exception { + // The two database paths in this test are assumed not to have been opened earlier in this + // process. + + // A database path that will be opened twice. + final String dbName = "never-used-db.db"; + final File dbFile = mContext.getDatabasePath(dbName); + final String dbPath = dbFile.getPath(); + + // A database path that will be opened only once. + final String okName = "never-used-ok.db"; + final File okFile = mContext.getDatabasePath(okName); + final String okPath = okFile.getPath(); + + SQLiteDatabase db1 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + assertFalse(isConcurrent(dbPath)); + SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + assertTrue(isConcurrent(dbPath)); + db1.close(); + assertTrue(isConcurrent(dbPath)); + db2.close(); + assertTrue(isConcurrent(dbPath)); + + SQLiteDatabase db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); + db3.close(); + db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); + assertFalse(isConcurrent(okPath)); + db3.close(); + } } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index cfbda84f24e1..cf3eb12498ca 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -19,7 +19,6 @@ package android.view; import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; -import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; @@ -576,13 +575,8 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); - viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH_HINT); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); - viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW); 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 d4a92894a397..a5f7880cfd41 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 @@ -137,7 +137,7 @@ import java.util.function.IntConsumer; * The controller manages addition, removal, and visible state of bubbles on screen. */ public class BubbleController implements ConfigurationChangeListener, - RemoteCallable<BubbleController> { + RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -706,6 +706,7 @@ public class BubbleController implements ConfigurationChangeListener, return mBubbleIconFactory; } + @Override public Bubbles.SysuiProxy getSysuiProxy() { return mSysuiProxy; } @@ -732,8 +733,7 @@ public class BubbleController implements ConfigurationChangeListener, if (mStackView == null) { mStackView = new BubbleStackView( mContext, this, mBubbleData, mSurfaceSynchronizer, - mFloatingContentCoordinator, - mMainExecutor); + mFloatingContentCoordinator, this, mMainExecutor); mStackView.onOrientationChanged(); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index c25d41275f2b..a619401301aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -206,6 +206,7 @@ public class BubbleStackView extends FrameLayout }; private final BubbleController mBubbleController; private final BubbleData mBubbleData; + private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider; private StackViewState mStackViewState = new StackViewState(); private final ValueAnimator mDismissBubbleAnimator; @@ -875,12 +876,14 @@ public class BubbleStackView extends FrameLayout public BubbleStackView(Context context, BubbleController bubbleController, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, + Bubbles.SysuiProxy.Provider sysuiProxyProvider, ShellExecutor mainExecutor) { super(context); mMainExecutor = mainExecutor; mBubbleController = bubbleController; mBubbleData = data; + mSysuiProxyProvider = sysuiProxyProvider; Resources res = getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); @@ -2090,7 +2093,7 @@ public class BubbleStackView extends FrameLayout hideCurrentInputMethod(); - mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); + mSysuiProxyProvider.getSysuiProxy().onStackExpandChanged(shouldExpand); if (wasExpanded) { stopMonitoringSwipeUpGesture(); @@ -3034,7 +3037,7 @@ public class BubbleStackView extends FrameLayout if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { mManageMenu.setVisibility(View.INVISIBLE); mManageMenuScrim.setVisibility(INVISIBLE); - mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */); + mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */); return; } if (show) { @@ -3048,7 +3051,7 @@ public class BubbleStackView extends FrameLayout } }; - mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show); + mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(show); mManageMenuScrim.animate() .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) .alpha(show ? BUBBLE_EXPANDED_SCRIM_ALPHA : 0f) 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 759246eb285d..28af0ca6ac6c 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 @@ -321,6 +321,13 @@ public interface Bubbles { /** Callback to tell SysUi components execute some methods. */ interface SysuiProxy { + + /** Provider interface for {@link SysuiProxy}. */ + interface Provider { + /** Returns {@link SysuiProxy}. */ + SysuiProxy getSysuiProxy(); + } + void isNotificationPanelExpand(Consumer<Boolean> callback); void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index f58332198696..4878df806f82 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -125,7 +125,7 @@ class BubbleViewInfoTest : ShellTestCase() { mock<BubbleProperties>()) bubbleStackView = BubbleStackView(context, bubbleController, bubbleData, - surfaceSynchronizer, FloatingContentCoordinator(), mainExecutor) + surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor) bubbleBarLayerView = BubbleBarLayerView(context, bubbleController) } diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp index 4d020c567972..5f5ffe97e953 100644 --- a/libs/hwui/AutoBackendTextureRelease.cpp +++ b/libs/hwui/AutoBackendTextureRelease.cpp @@ -21,6 +21,7 @@ #include <include/gpu/GrDirectContext.h> #include <include/gpu/GrBackendSurface.h> #include <include/gpu/MutableTextureState.h> +#include <include/gpu/vk/VulkanMutableTextureState.h> #include "renderthread/RenderThread.h" #include "utils/Color.h" #include "utils/PaintUtils.h" @@ -142,8 +143,9 @@ void AutoBackendTextureRelease::releaseQueueOwnership(GrDirectContext* context) LOG_ALWAYS_FATAL_IF(Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan); if (mBackendTexture.isValid()) { // Passing in VK_IMAGE_LAYOUT_UNDEFINED means we keep the old layout. - skgpu::MutableTextureState newState(VK_IMAGE_LAYOUT_UNDEFINED, - VK_QUEUE_FAMILY_FOREIGN_EXT); + skgpu::MutableTextureState newState = skgpu::MutableTextureStates::MakeVulkan( + VK_IMAGE_LAYOUT_UNDEFINED, + VK_QUEUE_FAMILY_FOREIGN_EXT); // The unref for this ref happens in the releaseProc passed into setBackendTextureState. The // releaseProc callback will be made when the work to set the new state has finished on the diff --git a/libs/hwui/jni/PathMeasure.cpp b/libs/hwui/jni/PathMeasure.cpp index acf893e9544c..79acb6cc35e5 100644 --- a/libs/hwui/jni/PathMeasure.cpp +++ b/libs/hwui/jni/PathMeasure.cpp @@ -17,7 +17,11 @@ #include "GraphicsJNI.h" +#include "SkMatrix.h" +#include "SkPath.h" #include "SkPathMeasure.h" +#include "SkPoint.h" +#include "SkScalar.h" /* We declare an explicit pair, so that we don't have to rely on the java client to be sure not to edit the path while we have an active measure diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 17f25255fd4b..687feef6c58a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -3109,9 +3109,8 @@ public final class MediaRouter2 { mStub, mDiscoveryPreference); } - if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { - unregisterRouterStubLocked(); - } + unregisterRouterStubIfNeededLocked(); + } catch (RemoteException ex) { Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); } @@ -3319,13 +3318,12 @@ public final class MediaRouter2 { obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller)); } - if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { - try { - unregisterRouterStubLocked(); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } + try { + unregisterRouterStubIfNeededLocked(); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); } + } } @@ -3339,8 +3337,10 @@ public final class MediaRouter2 { } @GuardedBy("mLock") - private void unregisterRouterStubLocked() throws RemoteException { - if (mStub != null) { + private void unregisterRouterStubIfNeededLocked() throws RemoteException { + if (mStub != null + && mRouteCallbackRecords.isEmpty() + && mNonSystemRoutingControllers.isEmpty()) { mMediaRouterService.unregisterRouter2(mStub); mStub = null; } diff --git a/media/java/android/media/audiofx/Virtualizer.java b/media/java/android/media/audiofx/Virtualizer.java index 74b6fc13ade4..71147f4becb5 100644 --- a/media/java/android/media/audiofx/Virtualizer.java +++ b/media/java/android/media/audiofx/Virtualizer.java @@ -46,6 +46,11 @@ import java.util.StringTokenizer; * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling * audio effects. + * + * @deprecated use the {@link android.media.Spatializer} class to query the capabilities of the + * platform with regards to spatialization, a different name for audio channel virtualization, + * and the {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)} to + * characterize how you want your content to be played when spatialization is supported. */ public class Virtualizer extends AudioEffect { diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java index cbd8c1fd53cd..694756c3f1a3 100644 --- a/media/java/android/media/tv/BroadcastInfoRequest.java +++ b/media/java/android/media/tv/BroadcastInfoRequest.java @@ -32,7 +32,8 @@ import java.lang.annotation.RetentionPolicy; public abstract class BroadcastInfoRequest implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE}) + @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE, + REQUEST_OPTION_ONEWAY, REQUEST_OPTION_ONESHOT}) public @interface RequestOption {} /** @@ -47,6 +48,18 @@ public abstract class BroadcastInfoRequest implements Parcelable { * first time, new values are detected. */ public static final int REQUEST_OPTION_AUTO_UPDATE = 1; + /** + * Request option: one-way + * <p> With this option, no response is expected after sending the request. + * @hide + */ + public static final int REQUEST_OPTION_ONEWAY = 2; + /** + * Request option: one-shot + * <p> With this option, only one response will be given per request. + * @hide + */ + public static final int REQUEST_OPTION_ONESHOT = 3; public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR = new Parcelable.Creator<BroadcastInfoRequest>() { diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl index 0f8a00a3c7d9..897827750d4e 100644 --- a/media/java/android/media/tv/ITvInputClient.aidl +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -44,6 +44,7 @@ oneway interface ITvInputClient { void onTrackSelected(int type, in String trackId, int seq); void onVideoAvailable(int seq); void onVideoUnavailable(int reason, int seq); + void onVideoFreezeUpdated(boolean isFrozen, int seq); void onContentAllowed(int seq); void onContentBlocked(in String rating, int seq); void onLayoutSurface(int left, int top, int right, int bottom, int seq); diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl index a52e9a572dca..8e2702a50662 100644 --- a/media/java/android/media/tv/ITvInputSessionCallback.aidl +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -41,6 +41,7 @@ oneway interface ITvInputSessionCallback { void onTrackSelected(int type, in String trackId); void onVideoAvailable(); void onVideoUnavailable(int reason); + void onVideoFreezeUpdated(boolean isFrozen); void onContentAllowed(); void onContentBlocked(in String rating); void onLayoutSurface(int left, int top, int right, int bottom); diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 51b2542688a6..caddd8ad674f 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -740,6 +740,15 @@ public final class TvInputManager { } /** + * This is called when the video freeze state has been updated. + * If {@code true}, the video is frozen on the last frame while audio playback continues. + * @param session A {@link TvInputManager.Session} associated with this callback. + * @param isFrozen Whether the video is frozen + */ + public void onVideoFreezeUpdated(Session session, boolean isFrozen) { + } + + /** * This is called when the current program content turns out to be allowed to watch since * its content rating is not blocked by parental controls. * @@ -1030,6 +1039,19 @@ public final class TvInputManager { }); } + void postVideoFreezeUpdated(boolean isFrozen) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onVideoFreezeUpdated(mSession, isFrozen); + if (mSession.mIAppNotificationEnabled + && mSession.getInteractiveAppSession() != null) { + mSession.getInteractiveAppSession().notifyVideoFreezeUpdated(isFrozen); + } + } + }); + } + void postContentAllowed() { mHandler.post(new Runnable() { @Override @@ -1546,6 +1568,18 @@ public final class TvInputManager { } @Override + public void onVideoFreezeUpdated(boolean isFrozen, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postVideoFreezeUpdated(isFrozen); + } + } + + @Override public void onContentAllowed(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 76d8e50f94be..6301a27bac4d 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -763,6 +763,34 @@ public abstract class TvInputService extends Service { } /** + * Informs the application that the video freeze state has been updated. + * + * When {@code true}, the video is frozen on the last frame but audio playback remains + * active. + * + * @param isFrozen Whether or not the video is frozen + * @hide + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated"); + } + if (mSessionCallback != null) { + mSessionCallback.onVideoFreezeUpdated(isFrozen); + } + } catch (RemoteException e) { + Log.e(TAG, "error in notifyVideoFreezeUpdated", e); + } + } + }); + } + + /** * Sends an updated list of all audio presentations available from a Next Generation Audio * service. This is used by the framework to maintain the audio presentation information for * a given track of {@link TvTrackInfo#TYPE_AUDIO}, which in turn is used by diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index a747e4995869..c4806fba3585 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,6 +16,7 @@ package android.media.tv.ad; +import android.graphics.Rect; import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManagerCallback; import android.media.tv.ad.TvAdServiceInfo; @@ -37,4 +38,9 @@ interface ITvAdManager { void registerCallback(in ITvAdManagerCallback callback, int userId); void unregisterCallback(in ITvAdManagerCallback callback, int userId); + + void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, + int userId); + void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId); + void removeMediaView(in IBinder sessionToken, int userId); } diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index 751257ce4d4e..3ca0198ddf19 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,6 +16,7 @@ package android.media.tv.ad; +import android.graphics.Rect; import android.view.Surface; /** @@ -27,4 +28,8 @@ oneway interface ITvAdSession { void startAdService(); void setSurface(in Surface surface); void dispatchSurfaceChanged(int format, int width, int height); + + void createMediaView(in IBinder windowToken, in Rect frame); + void relayoutMediaView(in Rect frame); + void removeMediaView(); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java index 4df2783f6511..3d5bc89e2b17 100644 --- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -17,6 +17,8 @@ package android.media.tv.ad; import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -43,6 +45,9 @@ public class ITvAdSessionWrapper private static final int DO_RELEASE = 1; private static final int DO_SET_SURFACE = 2; private static final int DO_DISPATCH_SURFACE_CHANGED = 3; + private static final int DO_CREATE_MEDIA_VIEW = 4; + private static final int DO_RELAYOUT_MEDIA_VIEW = 5; + private static final int DO_REMOVE_MEDIA_VIEW = 6; private final HandlerCaller mCaller; private TvAdService.Session mSessionImpl; @@ -61,6 +66,7 @@ public class ITvAdSessionWrapper @Override public void release() { + mSessionImpl.scheduleMediaViewCleanup(); mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); } @@ -97,6 +103,20 @@ public class ITvAdSessionWrapper args.recycle(); break; } + case DO_CREATE_MEDIA_VIEW: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.createMediaView((IBinder) args.arg1, (Rect) args.arg2); + args.recycle(); + break; + } + case DO_RELAYOUT_MEDIA_VIEW: { + mSessionImpl.relayoutMediaView((Rect) msg.obj); + break; + } + case DO_REMOVE_MEDIA_VIEW: { + mSessionImpl.removeMediaView(true); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -129,6 +149,22 @@ public class ITvAdSessionWrapper mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); } + @Override + public void createMediaView(IBinder windowToken, Rect frame) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_CREATE_MEDIA_VIEW, windowToken, frame)); + } + + @Override + public void relayoutMediaView(Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_MEDIA_VIEW, frame)); + } + + @Override + public void removeMediaView() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW)); + } + private final class TvAdEventReceiver extends InputEventReceiver { TvAdEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 9c7505197dec..b2ea00dd3593 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.graphics.Rect; import android.media.tv.TvInputManager; import android.media.tv.flags.Flags; import android.os.Handler; @@ -35,6 +36,7 @@ import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.Surface; +import android.view.View; import com.android.internal.util.Preconditions; @@ -286,6 +288,67 @@ public class TvAdManager { } /** + * Creates a media view. Once the media view is created, {@link #relayoutMediaView} + * should be called whenever the layout of its containing view is changed. + * {@link #removeMediaView()} should be called to remove the media view. + * Since a session can have only one media view, this method should be called only once + * or it can be called again after calling {@link #removeMediaView()}. + * + * @param view A view for AD service. + * @param frame A position of the media view. + * @throws IllegalStateException if {@code view} is not attached to a window. + */ + void createMediaView(@NonNull View view, @NonNull Rect frame) { + Preconditions.checkNotNull(view); + Preconditions.checkNotNull(frame); + if (view.getWindowToken() == null) { + throw new IllegalStateException("view must be attached to a window"); + } + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.createMediaView(mToken, view.getWindowToken(), frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(@NonNull Rect frame) { + Preconditions.checkNotNull(frame); + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.relayoutMediaView(mToken, frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes the current media view. + */ + void removeMediaView() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.removeMediaView(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Notifies of any structural changes (format or size) of the surface passed in * {@link #setSurface}. * diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 699570397e34..6f7f67d365f7 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -20,19 +20,25 @@ import android.annotation.CallSuper; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; import android.annotation.SdkConstant; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; +import android.view.Gravity; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; @@ -42,6 +48,7 @@ import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.WindowManager; +import android.widget.FrameLayout; import com.android.internal.os.SomeArgs; @@ -56,6 +63,8 @@ public abstract class TvAdService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvAdService"; + private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000; + /** * Name under which a TvAdService component publishes information about itself. This meta-data * must reference an XML resource containing an @@ -151,7 +160,14 @@ public abstract class TvAdService extends Service { private final Context mContext; final Handler mHandler; private final WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; private Surface mSurface; + private FrameLayout mMediaViewContainer; + private View mMediaView; + private MediaViewCleanUpTask mMediaViewCleanUpTask; + private boolean mMediaViewEnabled; + private IBinder mWindowToken; + private Rect mMediaFrame; /** @@ -166,6 +182,48 @@ public abstract class TvAdService extends Service { } /** + * Enables or disables the media view. + * + * <p>By default, the media view is disabled. Must be called explicitly after the + * session is created to enable the media view. + * + * <p>The TV AD service can disable its media view when needed. + * + * @param enable {@code true} if you want to enable the media view. {@code false} + * otherwise. + * @hide + */ + @CallSuper + public void setMediaViewEnabled(final boolean enable) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (enable == mMediaViewEnabled) { + return; + } + mMediaViewEnabled = enable; + if (enable) { + if (mWindowToken != null) { + createMediaView(mWindowToken, mMediaFrame); + } + } else { + removeMediaView(false); + } + } + }); + } + + /** + * Returns {@code true} if media view is enabled, {@code false} otherwise. + * + * @see #setMediaViewEnabled(boolean) + * @hide + */ + public boolean isMediaViewEnabled() { + return mMediaViewEnabled; + } + + /** * Releases TvAdService session. */ public abstract void onRelease(); @@ -180,6 +238,9 @@ public abstract class TvAdService extends Service { mSessionCallback = null; mPendingActions.clear(); } + // Removes the media view lastly so that any hanging on the main thread can be handled + // in {@link #scheduleMediaViewCleanup}. + removeMediaView(true); } /** @@ -307,6 +368,33 @@ public abstract class TvAdService extends Service { } /** + * Called when the size of the media view is changed by the application. + * + * <p>This is always called at least once when the session is created regardless of whether + * the media view is enabled or not. The media view container size is the same as the + * containing {@link TvAdView}. Note that the size of the underlying surface can + * be different if the surface was changed by calling {@link #layoutSurface}. + * + * @param width The width of the media view, in pixels. + * @param height The height of the media view, in pixels. + * @hide + */ + public void onMediaViewSizeChanged(@Px int width, @Px int height) { + } + + /** + * Called when the application requests to create a media view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the media window. {@code null} if no media view is created. + * @hide + */ + @Nullable + public View onCreateMediaView() { + return null; + } + + /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { @@ -388,6 +476,137 @@ public abstract class TvAdService extends Service { } } } + + /** + * Creates a media view. This calls {@link #onCreateMediaView} to get a view to attach + * to the media window. + * + * @param windowToken A window token of the application. + * @param frame A position of the media view. + */ + void createMediaView(IBinder windowToken, Rect frame) { + if (mMediaViewContainer != null) { + removeMediaView(false); + } + if (DEBUG) Log.d(TAG, "create media view(" + frame + ")"); + mWindowToken = windowToken; + mMediaFrame = frame; + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + if (!mMediaViewEnabled) { + return; + } + mMediaView = onCreateMediaView(); + if (mMediaView == null) { + return; + } + if (mMediaViewCleanUpTask != null) { + mMediaViewCleanUpTask.cancel(true); + mMediaViewCleanUpTask = null; + } + // Creates a container view to check hanging on the media view detaching. + // Adding/removing the media view to/from the container make the view attach/detach + // logic run on the main thread. + mMediaViewContainer = new FrameLayout(mContext.getApplicationContext()); + mMediaViewContainer.addView(mMediaView); + + int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + // We make the overlay view non-focusable and non-touchable so that + // the application that owns the window token can decide whether to consume or + // dispatch the input events. + int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + if (ActivityManager.isHighEndGfx()) { + flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + mWindowParams = new WindowManager.LayoutParams( + frame.right - frame.left, frame.bottom - frame.top, + frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT); + mWindowParams.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowParams.gravity = Gravity.START | Gravity.TOP; + mWindowParams.token = windowToken; + mWindowManager.addView(mMediaViewContainer, mWindowParams); + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(Rect frame) { + if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")"); + if (mMediaFrame == null || mMediaFrame.width() != frame.width() + || mMediaFrame.height() != frame.height()) { + // Note: relayoutMediaView is called whenever TvAdView's layout is + // changed regardless of setMediaViewEnabled. + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + } + mMediaFrame = frame; + if (!mMediaViewEnabled || mMediaViewContainer == null) { + return; + } + mWindowParams.x = frame.left; + mWindowParams.y = frame.top; + mWindowParams.width = frame.right - frame.left; + mWindowParams.height = frame.bottom - frame.top; + mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams); + } + + /** + * Removes the current media view. + */ + void removeMediaView(boolean clearWindowToken) { + if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")"); + if (clearWindowToken) { + mWindowToken = null; + mMediaFrame = null; + } + if (mMediaViewContainer != null) { + // Removes the media view from the view hierarchy in advance so that it can be + // cleaned up in the {@link MediaViewCleanUpTask} if the remove process is + // hanging. + mMediaViewContainer.removeView(mMediaView); + mMediaView = null; + mWindowManager.removeView(mMediaViewContainer); + mMediaViewContainer = null; + mWindowParams = null; + } + } + + /** + * Schedules a task which checks whether the media view is detached and kills the process + * if it is not. Note that this method is expected to be called in a non-main thread. + */ + void scheduleMediaViewCleanup() { + View mediaViewParent = mMediaViewContainer; + if (mediaViewParent != null) { + mMediaViewCleanUpTask = new MediaViewCleanUpTask(); + mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + mediaViewParent); + } + } + } + + private static final class MediaViewCleanUpTask extends AsyncTask<View, Void, Void> { + @Override + protected Void doInBackground(View... views) { + View mediaViewParent = views[0]; + try { + Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS); + } catch (InterruptedException e) { + return null; + } + if (isCancelled()) { + return null; + } + if (mediaViewParent.isAttachedToWindow()) { + Log.e(TAG, "Time out on releasing media view. Killing " + + mediaViewParent.getContext().getPackageName()); + android.os.Process.killProcess(Process.myPid()); + } + return null; + } } diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 5e67fe9f697b..5e4a70b03511 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -22,6 +22,8 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; @@ -64,6 +66,9 @@ public class TvAdView extends ViewGroup { private int mSurfaceViewTop; private int mSurfaceViewBottom; + private boolean mMediaViewCreated; + private Rect mMediaViewFrame; + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { @@ -121,6 +126,20 @@ public class TvAdView extends ViewGroup { mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); } + /** @hide */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionMediaView(); + } + + /** @hide */ + @Override + public void onDetachedFromWindow() { + removeSessionMediaView(); + super.onDetachedFromWindow(); + } + @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { if (DEBUG) { @@ -150,6 +169,11 @@ public class TvAdView extends ViewGroup { public void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); mSurfaceView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + createSessionMediaView(); + } else { + removeSessionMediaView(); + } } private void resetSurfaceView() { @@ -162,6 +186,7 @@ public class TvAdView extends ViewGroup { @Override protected void updateSurface() { super.updateSurface(); + relayoutSessionMediaView(); }}; // The surface view's content should be treated as secure all the time. mSurfaceView.setSecure(true); @@ -174,6 +199,69 @@ public class TvAdView extends ViewGroup { addView(mSurfaceView); } + /** + * Resets this TvAdView to release its resources. + * + * <p>It can be reused by call {@link #prepareAdService(String, String)}. + * @hide + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + resetInternal(); + } + + private void resetInternal() { + mSessionCallback = null; + if (mSession != null) { + setSessionSurface(null); + removeSessionMediaView(); + mUseRequestedSurfaceLayout = false; + mSession.release(); + mSession = null; + resetSurfaceView(); + } + } + + private void createSessionMediaView() { + // TODO: handle z-order + if (mSession == null || !isAttachedToWindow() || mMediaViewCreated) { + return; + } + mMediaViewFrame = getViewFrameOnScreen(); + mSession.createMediaView(this, mMediaViewFrame); + mMediaViewCreated = true; + } + + private void removeSessionMediaView() { + if (mSession == null || !mMediaViewCreated) { + return; + } + mSession.removeMediaView(); + mMediaViewCreated = false; + mMediaViewFrame = null; + } + + private void relayoutSessionMediaView() { + if (mSession == null || !isAttachedToWindow() || !mMediaViewCreated) { + return; + } + Rect viewFrame = getViewFrameOnScreen(); + if (viewFrame.equals(mMediaViewFrame)) { + return; + } + mSession.relayoutMediaView(viewFrame); + mMediaViewFrame = viewFrame; + } + + private Rect getViewFrameOnScreen() { + Rect frame = new Rect(); + getGlobalVisibleRect(frame); + RectF frameF = new RectF(frame); + getMatrix().mapRect(frameF); + frameF.round(frame); + return frame; + } + private void setSessionSurface(Surface surface) { if (mSession == null) { return; @@ -185,7 +273,7 @@ public class TvAdView extends ViewGroup { if (mSession == null) { return; } - //mSession.dispatchSurfaceChanged(format, width, height); + mSession.dispatchSurfaceChanged(format, width, height); } /** @@ -246,6 +334,7 @@ public class TvAdView extends ViewGroup { dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); } } + createSessionMediaView(); } else { // Failed to create // Todo: forward error to Tv App @@ -262,6 +351,8 @@ public class TvAdView extends ViewGroup { Log.w(TAG, "onSessionReleased - session not created"); return; } + mMediaViewCreated = false; + mMediaViewFrame = null; mSessionCallback = null; mSession = null; } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 4316d053a275..0f58b29247bb 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -88,6 +88,7 @@ interface ITvInteractiveAppManager { void notifyTracksChanged(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId); void notifyVideoAvailable(in IBinder sessionToken, int userId); void notifyVideoUnavailable(in IBinder sessionToken, int reason, int userId); + void notifyVideoFreezeUpdated(in IBinder sessionToken, boolean isFrozen, int userId); void notifyContentAllowed(in IBinder sessionToken, int userId); void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId); void notifySignalStrength(in IBinder sessionToken, int stength, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index ba7cf13a7a1d..06808c9ff915 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -67,6 +67,7 @@ oneway interface ITvInteractiveAppSession { void notifyTracksChanged(in List<TvTrackInfo> tracks); void notifyVideoAvailable(); void notifyVideoUnavailable(int reason); + void notifyVideoFreezeUpdated(boolean isFrozen); void notifyContentAllowed(); void notifyContentBlocked(in String rating); void notifySignalStrength(int strength); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 518b08a93f95..77730aa46d0a 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -103,6 +103,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_SEND_TIME_SHIFT_MODE = 46; private static final int DO_SEND_AVAILABLE_SPEEDS = 47; private static final int DO_SEND_SELECTED_TRACK_INFO = 48; + private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -364,6 +365,10 @@ public class ITvInteractiveAppSessionWrapper args.recycle(); break; } + case DO_NOTIFY_VIDEO_FREEZE_UPDATED: { + mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -552,6 +557,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void notifyVideoFreezeUpdated(boolean isFrozen) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_VIDEO_FREEZE_UPDATED, + isFrozen)); + } + + @Override public void notifyContentAllowed() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_CONTENT_ALLOWED)); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index bf4379f470d8..8a340f6862bb 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -1731,6 +1731,22 @@ public final class TvInteractiveAppManager { } /** + * Notifies Interactive app session when the video freeze state is updated + * @param isFrozen Whether or not the video is frozen + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.notifyVideoFreezeUpdated(mToken, isFrozen, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Notifies Interactive APP session when content is allowed. */ public void notifyContentAllowed() { diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 79364034ac2a..5247a0ebe6e0 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -882,6 +882,15 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Called when video becomes frozen or unfrozen. Audio playback will continue while + * video will be frozen to the last frame if {@code true}. + * @param isFrozen Whether or not the video is frozen. + * @hide + */ + public void onVideoFreezeUpdated(boolean isFrozen) { + } + + /** * Called when content is allowed. */ public void onContentAllowed() { @@ -1770,6 +1779,13 @@ public abstract class TvInteractiveAppService extends Service { onVideoUnavailable(reason); } + void notifyVideoFreezeUpdated(boolean isFrozen) { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated (isFrozen=" + isFrozen + ")"); + } + onVideoFreezeUpdated(isFrozen); + } + void notifyContentAllowed() { if (DEBUG) { Log.d(TAG, "notifyContentAllowed"); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 40a12e4db4cc..5bb61c261ae2 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -719,6 +719,22 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Alerts the TV Interactive app that the video freeze state has been updated. + * If {@code true}, the video is frozen on the last frame while audio playback continues. + * + * @param isFrozen Whether the video is frozen. + * @hide + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated"); + } + if (mSession != null) { + mSession.notifyVideoFreezeUpdated(isFrozen); + } + } + + /** * Sends signing result to related TV interactive app. * * <p>This is used when the corresponding server of the broadcast-independent interactive diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index d6f1eab442fa..15f33d2cff42 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -229,6 +229,17 @@ public class PhoneMediaDevice extends MediaDevice { @SuppressWarnings("NewApi") @Override public String getId() { + if (com.android.media.flags.Flags.enableAudioPoliciesDeviceAndBluetoothController()) { + // Note: be careful when removing this flag. Instead of just removing it, you might want + // to replace it with SDK_INT >= 35. Explanation: The presence of SDK checks in settings + // lib suggests that a mainline component may depend on this code. Which means removing + // this "if" (and using always the route info id) could mean a regression on mainline + // code running on a device that's running API 34 or older. Unfortunately, we cannot + // check the API level at the moment of writing this code because the API level has not + // been bumped, yet. + return mRouteInfo.getId(); + } + String id; switch (mRouteInfo.getType()) { case TYPE_WIRED_HEADSET: diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index 1746befbfa4d..ceba9be70487 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -31,10 +31,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.media.MediaRoute2Info; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import com.android.media.flags.Flags; import com.android.settingslib.R; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -45,6 +50,8 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class PhoneMediaDeviceTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private MediaRoute2Info mInfo; @@ -110,8 +117,18 @@ public class PhoneMediaDeviceTest { .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name)); } + @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) + @Test + public void getId_whenAdvancedWiredRoutingEnabled_returnCorrectId() { + String fakeId = "foo"; + when(mInfo.getId()).thenReturn(fakeId); + + assertThat(mPhoneMediaDevice.getId()).isEqualTo(fakeId); + } + + @DisableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) @Test - public void getId_returnCorrectId() { + public void getId_whenAdvancedWiredRoutingDisabled_returnCorrectId() { when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); assertThat(mPhoneMediaDevice.getId()) diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 2d442f4c0e6e..3a46f4e96ccb 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -406,7 +406,7 @@ public class SettingsProvider extends ContentProvider { Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); - mSettingsRegistry = new SettingsRegistry(); + mSettingsRegistry = new SettingsRegistry(mHandlerThread.getLooper()); } SettingsState.cacheSystemPackageNamesAndSystemSignature(getContext()); synchronized (mLock) { @@ -2896,8 +2896,8 @@ public class SettingsProvider extends ContentProvider { private String mSettingsCreationBuildId; - public SettingsRegistry() { - mHandler = new MyHandler(getContext().getMainLooper()); + SettingsRegistry(Looper looper) { + mHandler = new MyHandler(looper); mGenerationRegistry = new GenerationRegistry(UserManager.getMaxSupportedUsers()); mBackupManager = new BackupManager(getContext()); } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 323613077a70..2c35c777ab12 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -89,6 +89,13 @@ flag { } flag { + name: "notification_avalanche_suppression" + namespace: "systemui" + description: "After notification avalanche floodgate event, suppress HUNs completely." + bug: "321089634" +} + +flag { name: "notification_background_tint_optimization" namespace: "systemui" description: "Re-enable the codepath that removed tinting of notifications when the" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index f0b941a8f774..86279ef24ca7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget +import android.content.pm.UserInfo import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,6 +47,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -57,8 +60,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations /** * This class of test cases assume that communal is enabled. For disabled cases, see @@ -68,6 +73,9 @@ import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class CommunalInteractorTest : SysuiTestCase() { + @Mock private lateinit var mainUser: UserInfo + @Mock private lateinit var secondaryUser: UserInfo + private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -76,6 +84,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter @@ -84,15 +93,22 @@ class CommunalInteractorTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + tutorialRepository = kosmos.fakeCommunalTutorialRepository communalRepository = kosmos.fakeCommunalRepository mediaRepository = kosmos.fakeCommunalMediaRepository widgetRepository = kosmos.fakeCommunalWidgetRepository smartspaceRepository = kosmos.fakeSmartspaceRepository + userRepository = kosmos.fakeUserRepository keyguardRepository = kosmos.fakeKeyguardRepository editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter communalPrefsRepository = kosmos.fakeCommunalPrefsRepository + whenever(mainUser.isMain).thenReturn(true) + whenever(secondaryUser.isMain).thenReturn(false) + userRepository.setUserInfos(listOf(mainUser, secondaryUser)) + underTest = kosmos.communalInteractor } @@ -101,36 +117,52 @@ class CommunalInteractorTest : SysuiTestCase() { testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() } @Test - fun isCommunalAvailable_trueWhenStorageUnlock() = + fun isCommunalAvailable_storageUnlockedAndMainUser_true() = testScope.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) runCurrent() assertThat(isAvailable).isTrue() } @Test - fun isCommunalAvailable_whenStorageUnlock_true() = + fun isCommunalAvailable_storageLockedAndMainUser_false() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(true) + userRepository.setSelectedUserInfo(mainUser) + runCurrent() + + assertThat(isAvailable).isFalse() + } + + @Test + fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() = testScope.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(secondaryUser) runCurrent() - assertThat(isAvailable).isTrue() + assertThat(isAvailable).isFalse() } @Test - fun updateAppWidgetHostActive_uponStorageUnlock_true() = + fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = testScope.runTest { collectLastValue(underTest.isCommunalAvailable) assertThat(widgetRepository.isHostActive()).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) runCurrent() assertThat(widgetRepository.isHostActive()).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 161356d1bed2..352463f5a198 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.pm.UserInfo import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED @@ -30,9 +31,9 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope -import com.android.systemui.settings.UserTracker import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.user.data.repository.FakeUserRepository +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.test.runTest @@ -45,15 +46,17 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTutorialInteractorTest : SysuiTestCase() { + @Mock lateinit var user: UserInfo + private val kosmos = testKosmos() private val testScope = kosmos.testScope - @Mock private lateinit var userTracker: UserTracker - private lateinit var underTest: CommunalTutorialInteractor private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository private lateinit var communalRepository: FakeCommunalRepository + private lateinit var communalInteractor: CommunalInteractor + private lateinit var userRepository: FakeUserRepository @Before fun setUp() { @@ -62,16 +65,17 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { keyguardRepository = kosmos.fakeKeyguardRepository communalTutorialRepository = kosmos.fakeCommunalTutorialRepository communalRepository = kosmos.fakeCommunalRepository + communalInteractor = kosmos.communalInteractor + userRepository = kosmos.fakeUserRepository underTest = kosmos.communalTutorialInteractor - - whenever(userTracker.userHandle).thenReturn(mock()) } @Test fun tutorialUnavailable_whenKeyguardNotVisible() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) keyguardRepository.setKeyguardShowing(false) assertThat(isTutorialAvailable).isFalse() @@ -81,6 +85,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { fun tutorialUnavailable_whenTutorialIsCompleted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(false) @@ -89,9 +94,20 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test + fun tutorialUnavailable_whenCommunalNotAvailable() = + testScope.runTest { + val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(false) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + keyguardRepository.setKeyguardShowing(true) + assertThat(isTutorialAvailable).isFalse() + } + + @Test fun tutorialAvailable_whenTutorialNotStarted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(false) @@ -103,6 +119,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { fun tutorialAvailable_whenTutorialIsStarted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(true) @@ -183,4 +200,16 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) } + + private suspend fun setCommunalAvailable(available: Boolean) { + if (available) { + communalRepository.setIsCommunalEnabled(true) + keyguardRepository.setIsEncryptedOrLockdown(false) + whenever(user.isMain).thenReturn(true) + userRepository.setUserInfos(listOf(user)) + userRepository.setSelectedUserInfo(user) + } else { + keyguardRepository.setIsEncryptedOrLockdown(true) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 867a48dc3925..a2dec5ff8830 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -24,19 +24,21 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger 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.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +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.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -59,8 +61,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var communalRepository: FakeCommunalRepository private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository @@ -72,17 +72,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create(testScope) - keyguardRepository = withDeps.keyguardRepository - communalRepository = withDeps.communalRepository - tutorialRepository = withDeps.tutorialRepository - widgetRepository = withDeps.widgetRepository - smartspaceRepository = withDeps.smartspaceRepository - mediaRepository = withDeps.mediaRepository + tutorialRepository = kosmos.fakeCommunalTutorialRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + smartspaceRepository = kosmos.fakeSmartspaceRepository + mediaRepository = kosmos.fakeCommunalMediaRepository underTest = CommunalEditModeViewModel( - withDeps.communalInteractor, + kosmos.communalInteractor, mediaHost, uiEventLogger, ) 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 9ac21dd69aab..c814f3f1db6a 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 @@ -17,31 +17,39 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget +import android.content.pm.UserInfo import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository -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.CommunalInteractorFactory +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +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.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before @@ -57,16 +65,17 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class CommunalViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var user: UserInfo - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var communalRepository: FakeCommunalRepository private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository - private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository + private lateinit var userRepository: FakeUserRepository private lateinit var underTest: CommunalViewModel @@ -74,22 +83,18 @@ class CommunalViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - testScope = TestScope() - - val withDeps = CommunalInteractorFactory.create() - keyguardRepository = withDeps.keyguardRepository - communalRepository = withDeps.communalRepository - tutorialRepository = withDeps.tutorialRepository - widgetRepository = withDeps.widgetRepository - smartspaceRepository = withDeps.smartspaceRepository - mediaRepository = withDeps.mediaRepository - communalPrefsRepository = withDeps.communalPrefsRepository + keyguardRepository = kosmos.fakeKeyguardRepository + tutorialRepository = kosmos.fakeCommunalTutorialRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + smartspaceRepository = kosmos.fakeSmartspaceRepository + mediaRepository = kosmos.fakeCommunalMediaRepository + userRepository = kosmos.fakeUserRepository underTest = CommunalViewModel( testScope, - withDeps.communalInteractor, - withDeps.tutorialInteractor, + kosmos.communalInteractor, + kosmos.communalTutorialInteractor, mediaHost, ) } @@ -104,9 +109,11 @@ class CommunalViewModelTest : SysuiTestCase() { @Test fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = testScope.runTest { - // Keyguard showing, and tutorial not started. + // Keyguard showing, storage unlocked, main user, and tutorial not started. keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) + keyguardRepository.setIsEncryptedOrLockdown(false) + setIsMainUser(true) tutorialRepository.setTutorialSettingState( Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED ) @@ -202,4 +209,10 @@ class CommunalViewModelTest : SysuiTestCase() { underTest.onHidePopupAfterDismissCta() assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) } + + private suspend fun setIsMainUser(isMainUser: Boolean) { + whenever(user.isMain).thenReturn(isMainUser) + userRepository.setUserInfos(listOf(user)) + userRepository.setSelectedUserInfo(user) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 6f62afc560fe..dc8b97abbfe8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -139,6 +139,18 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun topClippingBounds() = + testScope.runTest { + assertThat(underTest.topClippingBounds.value).isNull() + + underTest.topClippingBounds.value = 50 + assertThat(underTest.topClippingBounds.value).isEqualTo(50) + + underTest.topClippingBounds.value = 500 + assertThat(underTest.topClippingBounds.value).isEqualTo(500) + } + + @Test fun clockPosition() = testScope.runTest { assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0)) 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 1eaa0602b258..6cc680baf938 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 @@ -25,9 +25,14 @@ import androidx.test.filters.SmallTest 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.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -42,6 +47,7 @@ import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value 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.Before @@ -54,13 +60,14 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardInteractor = kosmos.keyguardInteractor + 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 underTest by lazy { - kosmos.keyguardRootViewModel - } + private val underTest by lazy { kosmos.keyguardRootViewModel } @Before fun setUp() { @@ -207,7 +214,38 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubOpen_isZero() = + fun topClippingBounds() = + testScope.runTest { + val topClippingBounds by collectLastValue(underTest.topClippingBounds) + assertThat(topClippingBounds).isNull() + + keyguardRepository.topClippingBounds.value = 50 + assertThat(topClippingBounds).isEqualTo(50) + + keyguardRepository.topClippingBounds.value = 1000 + assertThat(topClippingBounds).isEqualTo(1000) + } + + @Test + fun alpha_idleOnHub_isZero() = + testScope.runTest { + val alpha by collectLastValue(underTest.alpha) + + // Hub transition state is idle with hub open. + communalRepository.setTransitionState( + flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)) + ) + runCurrent() + + // Set keyguard alpha to 1.0f. + keyguardInteractor.setAlpha(1.0f) + + // Alpha property remains 0 regardless. + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun alpha_transitionToHub_isZero() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) @@ -221,7 +259,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubClosed_isOne() = + fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c630a7ff09b6..e8201ecba623 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1728,6 +1728,10 @@ <dimen name="communal_grid_height">630dp</dimen> <!-- Number of columns for each communal card --> <integer name="communal_grid_columns_per_card">6</integer> + <!-- Width of area on right edge of screen in which swipes will open the communal hub --> + <dimen name="communal_right_edge_swipe_region_width">16dp</dimen> + <!-- Height of area at top of communal hub where swipes should open the notification shade --> + <dimen name="communal_top_edge_swipe_region_height">32dp</dimen> <dimen name="drag_and_drop_icon_size">70dp</dimen> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index 7088829bd2a3..d191a3c3f28b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -20,7 +20,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; import android.view.MotionEvent; -import android.view.SurfaceControl; import com.android.systemui.shared.recents.ISystemUiProxy; // Next ID: 29 @@ -99,11 +98,6 @@ oneway interface IOverviewProxy { void enterStageSplitFromRunningApp(boolean leftOrTop) = 25; /** - * Sent when the surface for navigation bar is created or changed - */ - void onNavigationBarSurface(in SurfaceControl surface) = 26; - - /** * Sent when the task bar stash state is toggled. */ void onTaskbarToggled() = 27; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 033f93b260ab..ad303171d22e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -477,9 +477,13 @@ public class KeyguardClockSwitch extends RelativeLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame = " + mSmallClockFrame); - pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); + if (mSmallClockFrame != null) { + pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); + } pw.println(" mLargeClockFrame = " + mLargeClockFrame); - pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); + if (mLargeClockFrame != null) { + pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); + } pw.println(" mStatusArea = " + mStatusArea); pw.println(" mDisplayedClockSize = " + mDisplayedClockSize); } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 92d01dbe3c3e..44b0383e12c6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -36,6 +36,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository +import com.android.systemui.user.data.repository.UserRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,6 +63,7 @@ constructor( private val communalPrefsRepository: CommunalPrefsRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, + userRepository: UserRepository, keyguardInteractor: KeyguardInteractor, private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter @@ -75,7 +77,14 @@ constructor( val isCommunalAvailable: StateFlow<Boolean> = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> - if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false) + if (enabled) + combine( + keyguardInteractor.isEncryptedOrLockdown, + userRepository.selectedUserInfo, + ) { isEncryptedOrLockdown, selectedUserInfo -> + !isEncryptedOrLockdown && selectedUserInfo.isMain + } + else flowOf(false) } .distinctUntilChanged() .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } @@ -263,6 +272,12 @@ constructor( companion object { /** + * The user activity timeout which should be used when the communal hub is opened. A value + * of -1 means that the user's chosen screen timeout will be used instead. + */ + const val AWAKE_INTERVAL_MS = -1 + + /** * Calculates the content size dynamically based on the total number of contents of that * type. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 5ca89f28f1fc..4e5be9b8aa5e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -45,14 +45,17 @@ constructor( private val communalTutorialRepository: CommunalTutorialRepository, keyguardInteractor: KeyguardInteractor, private val communalRepository: CommunalRepository, + communalInteractor: CommunalInteractor, ) { /** An observable for whether the tutorial is available. */ val isTutorialAvailable: Flow<Boolean> = combine( + communalInteractor.isCommunalAvailable, keyguardInteractor.isKeyguardVisible, communalTutorialRepository.tutorialSettingState, - ) { isKeyguardVisible, tutorialSettingState -> - isKeyguardVisible && + ) { isCommunalAvailable, isKeyguardVisible, tutorialSettingState -> + isCommunalAvailable && + isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } .distinctUntilChanged() 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 2bb9d0e52757..a909383f4a2c 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 @@ -74,6 +74,7 @@ constructor( // before the MediaHierarchyManager attempts to move the UMO to the hub. with(mediaHost) { expansion = MediaHostState.EXPANDED + expandedMatchesParentHeight = true showsOnlyActiveMedia = false falsingProtectionNeeded = false init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index d012d24b767e..14371949c9c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -132,6 +132,9 @@ interface KeyguardRepository { */ val isDozing: StateFlow<Boolean> + /** Keyguard can be clipped at the top as the shade is dragged */ + val topClippingBounds: MutableStateFlow<Int?> + /** * Observable for whether the device is dreaming. * @@ -326,6 +329,8 @@ constructor( private val _clockShouldBeCentered = MutableStateFlow(true) override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow() + override val topClippingBounds = MutableStateFlow<Int?>(null) + override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3292ea85639c..4df5d50a7a96 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -95,6 +95,7 @@ constructor( companion object { const val TAG = "FromGlanceableHubTransitionInteractor" - val DEFAULT_DURATION = 500.milliseconds + val DEFAULT_DURATION = 400.milliseconds + val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 6170356d4a90..91747e0f69a0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -171,6 +171,14 @@ constructor( /** Whether the keyguard is going away. */ val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway + /** Keyguard can be clipped at the top as the shade is dragged */ + val topClippingBounds: Flow<Int?> = + combine(configurationInteractor.onAnyConfigurationChange, repository.topClippingBounds) { + _, + topClippingBounds -> + topClippingBounds + } + /** Last point that [KeyguardRootView] view was tapped */ val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow() @@ -328,6 +336,10 @@ constructor( repository.keyguardDoneAnimationsFinished() } + fun setTopClippingBounds(top: Int?) { + repository.topClippingBounds.value = top + } + companion object { private const val TAG = "KeyguardInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 2aebd99e3664..48092c6374e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.annotation.DrawableRes import android.annotation.SuppressLint import android.graphics.Point +import android.graphics.Rect import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener @@ -158,6 +159,23 @@ object KeyguardRootViewBinder { } launch { + val clipBounds = Rect() + viewModel.topClippingBounds.collect { clipTop -> + if (clipTop == null) { + view.setClipBounds(null) + } else { + clipBounds.apply { + top = clipTop + left = view.getLeft() + right = view.getRight() + bottom = view.getBottom() + } + view.setClipBounds(clipBounds) + } + } + } + + launch { viewModel.lockscreenStateAlpha.collect { alpha -> childViews[statusViewId]?.alpha = alpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bc518210a067..6aa2ecabae75 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject @@ -37,7 +37,7 @@ constructor( ) { private val transitionAnimation = animationFlow.setup( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, ) @@ -45,10 +45,20 @@ constructor( // TODO(b/315205222): implement full animation spec instead of just a simple fade. val keyguardAlpha: Flow<Float> = transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, onStep = { it }, onFinish = { 1f }, onCancel = { 0f }, name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, + onStep = { it }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha", + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 5d36da9b9df1..709e184e6e52 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -21,6 +21,7 @@ import android.graphics.Point import android.view.View.VISIBLE import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -56,6 +57,7 @@ constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val dozeParameters: DozeParameters, private val keyguardInteractor: KeyguardInteractor, + communalInteractor: CommunalInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, @@ -80,13 +82,31 @@ constructor( val notificationBounds: StateFlow<NotificationContainerBounds> = keyguardInteractor.notificationContainerBounds + /** + * The keyguard root view can be clipped as the shade is pulled down, typically only for + * non-split shade cases. + */ + val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds + /** An observable for the alpha level for the entire keyguard root view. */ val alpha: Flow<Float> = - merge( - aodAlphaViewModel.alpha, - lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, - glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, - ) + combine( + communalInteractor.isIdleOnCommunal, + merge( + aodAlphaViewModel.alpha, + lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, + glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, + ) + ) { isIdleOnCommunal, alpha -> + if (isIdleOnCommunal) { + // Keyguard should not show while the communal hub is fully visible. This check + // is added since at the moment, closing the notification shade will cause the + // keyguard alpha to be set back to 1. + 0f + } else { + alpha + } + } .distinctUntilChanged() /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 3ea83ae21085..3afa49e50167 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -51,4 +51,14 @@ constructor( onCancel = { 1f }, name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + onStep = { 1f - it }, + onFinish = { 0f }, + onCancel = { 1f }, + name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha", + ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt index 631a0b8471b8..437218f9f440 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt @@ -228,6 +228,14 @@ constructor( } } + override var expandedMatchesParentHeight: Boolean = false + set(value) { + if (value != field) { + field = value + changedListener?.invoke() + } + } + override var squishFraction: Float = 1.0f set(value) { if (!value.equals(field)) { @@ -282,6 +290,7 @@ constructor( override fun copy(): MediaHostState { val mediaHostState = MediaHostStateHolder() mediaHostState.expansion = expansion + mediaHostState.expandedMatchesParentHeight = expandedMatchesParentHeight mediaHostState.squishFraction = squishFraction mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.measurementInput = measurementInput?.copy() @@ -360,6 +369,12 @@ interface MediaHostState { */ var expansion: Float + /** + * If true, the [EXPANDED] layout should stretch to match the height of its parent container, + * rather than having a fixed height. + */ + var expandedMatchesParentHeight: Boolean + /** Fraction of the height animation. */ var squishFraction: Float diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index be9393655c5d..962764c028fc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.Configuration import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import com.android.app.tracing.traceSection import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder @@ -152,18 +153,11 @@ constructor( lastOrientation = newOrientation // Update the height of media controls for the expanded layout. it is needed // for large screen devices. - val backgroundIds = - if (type == TYPE.PLAYER) { - MediaViewHolder.backgroundIds - } else { - setOf(RecommendationViewHolder.backgroundId) - } - backgroundIds.forEach { id -> - expandedLayout.getConstraint(id).layout.mHeight = - context.resources.getDimensionPixelSize( - R.dimen.qs_media_session_height_expanded - ) - } + setBackgroundHeights( + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + ) } if (this@MediaViewController::configurationChangeListener.isInitialized) { configurationChangeListener.invoke() @@ -276,6 +270,17 @@ constructor( private fun constraintSetForExpansion(expansion: Float): ConstraintSet = if (expansion > 0) expandedLayout else collapsedLayout + /** Set the height of UMO background constraints. */ + private fun setBackgroundHeights(height: Int) { + val backgroundIds = + if (type == TYPE.PLAYER) { + MediaViewHolder.backgroundIds + } else { + setOf(RecommendationViewHolder.backgroundId) + } + backgroundIds.forEach { id -> expandedLayout.getConstraint(id).layout.mHeight = height } + } + /** * Set the views to be showing/hidden based on the [isGutsVisible] for a given * [TransitionViewState]. @@ -454,6 +459,18 @@ constructor( } // Let's create a new measurement if (state.expansion == 0.0f || state.expansion == 1.0f) { + if (state.expansion == 1.0f) { + val height = + if (state.expandedMatchesParentHeight) { + MATCH_CONSTRAINT + } else { + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } + setBackgroundHeights(height) + } + result = transitionLayout!!.calculateViewState( state.measurementInput!!, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 0a72a2f15e95..068e5fd61ea4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -504,24 +504,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; - private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback = - new SurfaceChangedCallback() { - @Override - public void surfaceCreated(Transaction t) { - notifyNavigationBarSurface(); - } - - @Override - public void surfaceDestroyed() { - notifyNavigationBarSurface(); - } - - @Override - public void surfaceReplaced(Transaction t) { - notifyNavigationBarSurface(); - } - }; - private boolean mScreenPinningActive = false; private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override @@ -787,8 +769,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.getViewTreeObserver().addOnComputeInternalInsetsListener( mOnComputeInternalInsetsListener); - mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback); - notifyNavigationBarSurface(); mPipOptional.ifPresent(mView::addPipExclusionBoundsChangeListener); mBackAnimation.ifPresent(mView::registerBackAnimation); @@ -860,13 +840,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mHandler.removeCallbacks(mEnableLayoutTransitions); mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener); - ViewRootImpl viewRoot = mView.getViewRootImpl(); - if (viewRoot != null) { - viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback); - } mFrame = null; mOrientationHandle = null; - notifyNavigationBarSurface(); } // TODO: Remove this when we update nav bar recreation @@ -1026,17 +1001,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } } - private void notifyNavigationBarSurface() { - ViewRootImpl viewRoot = mView.getViewRootImpl(); - SurfaceControl surface = mView.getParent() != null - && viewRoot != null - && viewRoot.getSurfaceControl() != null - && viewRoot.getSurfaceControl().isValid() - ? viewRoot.getSurfaceControl() - : null; - mOverviewProxyService.onNavigationBarSurfaceChanged(surface); - } - private int deltaRotation(int oldRotation, int newRotation) { int delta = newRotation - oldRotation; if (delta < 0) delta += 4; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 456520051f58..c5eeb2f87d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -249,6 +249,10 @@ open class QSTileViewImpl @JvmOverloads constructor( height = iconSize marginEnd = endMargin } + + background = createTileBackground() + setColor(backgroundColor) + setOverlayColor(backgroundOverlayColor) } private fun createAndAddLabels() { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index fd53423e7f22..cc53aabfcd25 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -169,7 +169,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final DisplayTracker mDisplayTracker; private Region mActiveNavBarRegion; - private SurfaceControl mNavigationBarSurface; private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; @@ -488,7 +487,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis Log.e(TAG_OPS, "Failed to call onInitialize()", e); } dispatchNavButtonBounds(); - dispatchNavigationBarSurface(); // Force-update the systemui state flags updateSystemUiStateFlags(); @@ -679,28 +677,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis .commitUpdate(mContext.getDisplayId()); } - /** - * Called when the navigation bar surface is created or changed - */ - public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) { - mNavigationBarSurface = navbarSurface; - dispatchNavigationBarSurface(); - } - - private void dispatchNavigationBarSurface() { - try { - if (mOverviewProxy != null) { - // Catch all for cases where the surface is no longer valid - if (mNavigationBarSurface != null && !mNavigationBarSurface.isValid()) { - mNavigationBarSurface = null; - } - mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface); - } - } catch (RemoteException e) { - Log.e(TAG_OPS, "Failed to notify back action", e); - } - } - private void updateEnabledAndBinding() { updateEnabledState(); startConnectionToCurrentUser(); @@ -1075,7 +1051,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis pw.print(" mInputFocusTransferStartY="); pw.println(mInputFocusTransferStartY); pw.print(" mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis); pw.print(" mActiveNavBarRegion="); pw.println(mActiveNavBarRegion); - pw.print(" mNavigationBarSurface="); pw.println(mNavigationBarSurface); pw.print(" mNavBarMode="); pw.println(mNavBarMode); mSysUiState.dump(pw, args); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 782d6519468c..3362ebc8ebb4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -55,7 +55,15 @@ constructor( * The width of the area in which a right edge swipe can open the hub, in pixels. Read from * resources when [initView] is called. */ - private var edgeSwipeRegionWidth: Int = 0 + // TODO(b/320786721): support RTL layouts + private var rightEdgeSwipeRegionWidth: Int = 0 + + /** + * The height of the area in which a top edge swipe while the hub is open will not intercept + * touches, in pixels. This allows the top edge swipe to instead open the notification shade. + * Read from resources when [initView] is called. + */ + private var topEdgeSwipeRegionWidth: Int = 0 /** * True if we are currently tracking a gesture for opening the hub that started in the edge @@ -63,6 +71,9 @@ constructor( */ private var isTrackingOpenGesture = false + /** True if we are currently tracking a touch on the hub while it's open. */ + private var isTrackingHubTouch = false + /** * True if the hub UI is fully open, meaning it should receive touch input. * @@ -113,8 +124,14 @@ constructor( communalContainerView = containerView - edgeSwipeRegionWidth = - communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size) + rightEdgeSwipeRegionWidth = + communalContainerView.resources.getDimensionPixelSize( + R.dimen.communal_right_edge_swipe_region_width + ) + topEdgeSwipeRegionWidth = + communalContainerView.resources.getDimensionPixelSize( + R.dimen.communal_top_edge_swipe_region_height + ) collectFlow( communalContainerView, @@ -157,17 +174,38 @@ constructor( // fully showing state val hubOccluded = anyBouncerShowing || shadeShowing - // If the hub is fully visible, send all touch events to it. - val communalVisible = hubShowing && !hubOccluded - if (communalVisible) { + // If the hub is fully visible, send all touch events to it, other than top and bottom edge + // swipes. + if (hubShowing && isDown) { + val y = ev.rawY + val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth + + // TODO(b/315207481): allow bottom edge swipes to open the bouncer + if (topSwipe) { + // Don't intercept touches at the top edge so that swipes can open the notification + // shade. + return false + } + + if (!hubOccluded) { + isTrackingHubTouch = true + dispatchTouchEvent(ev) + // Return true regardless of dispatch result as some touches at the start of a + // gesture may return false from dispatchTouchEvent. + return true + } + } else if (isTrackingHubTouch) { + if (isUp || isCancel) { + isTrackingHubTouch = false + } dispatchTouchEvent(ev) // Return true regardless of dispatch result as some touches at the start of a gesture // may return false from dispatchTouchEvent. return true } - if (edgeSwipeRegionWidth == 0) { - // If the edge region width has not been read yet or whatever reason, don't bother + if (rightEdgeSwipeRegionWidth == 0) { + // If the edge region width has not been read yet for whatever reason, don't bother // intercepting touches to open the hub. return false } @@ -175,7 +213,7 @@ constructor( if (!isTrackingOpenGesture && isDown) { val x = ev.rawX val inOpeningSwipeRegion: Boolean = - x >= communalContainerView.width - edgeSwipeRegionWidth + x >= communalContainerView.width - rightEdgeSwipeRegionWidth if (inOpeningSwipeRegion && !hubOccluded) { isTrackingOpenGesture = true dispatchTouchEvent(ev) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 60feb82bf4aa..00534743588c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -50,6 +50,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -118,6 +119,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final Lazy<SelectedUserInteractor> mUserInteractor; private final Lazy<ShadeInteractor> mShadeInteractorLazy; private final SceneContainerFlags mSceneContainerFlags; + private final Lazy<CommunalInteractor> mCommunalInteractor; private ViewGroup mWindowRootView; private LayoutParams mLp; private boolean mHasTopUi; @@ -165,7 +167,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW ShadeWindowLogger logger, Lazy<SelectedUserInteractor> userInteractor, UserTracker userTracker, - SceneContainerFlags sceneContainerFlags) { + SceneContainerFlags sceneContainerFlags, + Lazy<CommunalInteractor> communalInteractor) { mContext = context; mWindowRootViewComponentFactory = windowRootViewComponentFactory; mWindowManager = windowManager; @@ -184,6 +187,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mAuthController = authController; mUserInteractor = userInteractor; mSceneContainerFlags = sceneContainerFlags; + mCommunalInteractor = communalInteractor; mLastKeyguardRotationAllowed = mKeyguardStateController.isKeyguardScreenRotationAllowed(); mLockScreenDisplayTimeout = context.getResources() .getInteger(R.integer.config_lockScreenDisplayTimeout); @@ -325,6 +329,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mShadeInteractorLazy.get().isQsExpanded(), this::onQsExpansionChanged ); + collectFlow( + mWindowRootView, + mCommunalInteractor.get().isCommunalShowing(), + this::onCommunalShowingChanged + ); } @Override @@ -501,14 +510,21 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyUserActivityTimeout(NotificationShadeWindowState state) { - if (state.isKeyguardShowingAndNotOccluded() + final Boolean communalShowing = state.isCommunalShowingAndNotOccluded(); + final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded(); + long timeout = -1; + if ((communalShowing || keyguardShowing) && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { - mLpChanged.userActivityTimeout = state.bouncerShowing - ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout; - } else { - mLpChanged.userActivityTimeout = -1; + if (state.bouncerShowing) { + timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS; + } else if (communalShowing) { + timeout = CommunalInteractor.AWAKE_INTERVAL_MS; + } else if (keyguardShowing) { + timeout = mLockScreenDisplayTimeout; + } } + mLpChanged.userActivityTimeout = timeout; } private void applyInputFeatures(NotificationShadeWindowState state) { @@ -607,7 +623,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.forcePluginOpen, state.dozing, state.scrimsVisibility, - state.backgroundBlurRadius + state.backgroundBlurRadius, + state.communalShowing ); } @@ -731,6 +748,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW apply(mCurrentState); } + @VisibleForTesting + void onCommunalShowingChanged(Boolean showing) { + mCurrentState.communalShowing = showing; + apply(mCurrentState); + } + @Override public void setForceUserActivity(boolean forceUserActivity) { mCurrentState.forceUserActivity = forceUserActivity; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index 0b20170834d8..f9c9d83e03aa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -58,12 +58,17 @@ class NotificationShadeWindowState( @JvmField var dreaming: Boolean = false, @JvmField var scrimsVisibility: Int = 0, @JvmField var backgroundBlurRadius: Int = 0, + @JvmField var communalShowing: Boolean = false, ) { fun isKeyguardShowingAndNotOccluded(): Boolean { return keyguardShowing && !keyguardOccluded } + fun isCommunalShowingAndNotOccluded(): Boolean { + return communalShowing && !keyguardOccluded + } + /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ val asStringList: List<String> by lazy { listOf( @@ -93,7 +98,8 @@ class NotificationShadeWindowState( forcePluginOpen.toString(), dozing.toString(), scrimsVisibility.toString(), - backgroundBlurRadius.toString() + backgroundBlurRadius.toString(), + communalShowing.toString(), ) } @@ -134,6 +140,7 @@ class NotificationShadeWindowState( dozing: Boolean, scrimsVisibility: Int, backgroundBlurRadius: Int, + communalShowing: Boolean, ) { buffer.advance().apply { this.keyguardShowing = keyguardShowing @@ -165,6 +172,7 @@ class NotificationShadeWindowState( this.dozing = dozing this.scrimsVisibility = scrimsVisibility this.backgroundBlurRadius = backgroundBlurRadius + this.communalShowing = communalShowing } } @@ -209,7 +217,8 @@ class NotificationShadeWindowState( "forcePluginOpen", "dozing", "scrimsVisibility", - "backgroundBlurRadius" + "backgroundBlurRadius", + "communalShowing" ) } } 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 46806e66cb8c..54b6ad71e734 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 @@ -790,7 +790,7 @@ private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R { /** Mutates the HeadsUp state of notifications. */ private interface HunMutator { - fun updateNotification(key: String, alert: Boolean) + fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) fun removeNotification(key: String, releaseImmediately: Boolean) } @@ -801,8 +801,8 @@ private interface HunMutator { private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator { private val deferred = mutableListOf<Pair<String, Boolean>>() - override fun updateNotification(key: String, alert: Boolean) { - headsUpManager.updateNotification(key, alert) + override fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) { + headsUpManager.updateNotification(key, shouldHeadsUpAgain) } override fun removeNotification(key: String, releaseImmediately: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index 380cdadd1361..ae4ba27775b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.screenshareNotificationHiding import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import dagger.Binds import dagger.Module @@ -55,6 +57,8 @@ class SensitiveContentCoordinatorImpl @Inject constructor( private val statusBarStateController: StatusBarStateController, private val keyguardStateController: KeyguardStateController, private val selectedUserInteractor: SelectedUserInteractor, + private val sensitiveNotificationProtectionController: + SensitiveNotificationProtectionController, ) : Invalidator("SensitiveContentInvalidator"), SensitiveContentCoordinator, DynamicPrivacyController.Listener, @@ -82,10 +86,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( return } + val isSensitiveContentProtectionActive = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.isSensitiveStateActive val currentUserId = lockscreenUserManager.currentUserId val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId) - val deviceSensitive = devicePublic && - !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId) + val deviceSensitive = (devicePublic && + !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) || + isSensitiveContentProtectionActive val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) { val notifUserId = entry.sbn.user.identifier @@ -105,9 +112,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) } } + + val shouldProtectNotification = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.shouldProtectNotification(entry) + val needsRedaction = lockscreenUserManager.needsRedaction(entry) val isSensitive = userPublic && needsRedaction - entry.setSensitive(isSensitive, deviceSensitive) + entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt index 0bc8e682c891..f375ebce2de3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -187,7 +187,7 @@ object NotificationIconContainerViewBinder { configuration.getDimensionPixelSize(RInternal.dimen.status_bar_icon_size_sp) val iconHorizontalPaddingFlow: Flow<Int> = configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin) - val layoutParams: Flow<FrameLayout.LayoutParams> = + val layoutParams: StateFlow<FrameLayout.LayoutParams> = combine(iconSizeFlow, iconHorizontalPaddingFlow, systemBarUtilsState.statusBarHeight) { iconSize, iconHPadding, @@ -206,7 +206,7 @@ object NotificationIconContainerViewBinder { private suspend fun Flow<NotificationIconsViewData>.bindIcons( view: NotificationIconContainer, - layoutParams: Flow<FrameLayout.LayoutParams>, + layoutParams: StateFlow<FrameLayout.LayoutParams>, notifyBindingFailures: (Collection<String>) -> Unit, viewStore: IconViewStore, bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit, @@ -259,7 +259,7 @@ object NotificationIconContainerViewBinder { // added again. removeTransientView(sbiv) } - view.addView(sbiv) + view.addView(sbiv, layoutParams.value) boundViewsByNotifKey.remove(notifKey)?.second?.cancel() boundViewsByNotifKey[notifKey] = Pair( @@ -267,7 +267,9 @@ object NotificationIconContainerViewBinder { launch { launch { layoutParams.collectTracingEach("SBIV#bindLayoutParams") { - sbiv.layoutParams = it + if (it != sbiv.layoutParams) { + sbiv.layoutParams = it + } } } bindIcon(notifKey, sbiv) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt new file mode 100644 index 000000000000..a21dd9bb9579 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt @@ -0,0 +1,53 @@ +/* + * 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.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notification avalanche suppression flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationAvalancheSuppression { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationAvalancheSuppression() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 6a66bb74f16d..1143481863f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -22,6 +22,7 @@ import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_N import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; +import static com.android.systemui.Flags.screenshareNotificationHiding; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; @@ -135,6 +136,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; @@ -218,6 +220,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final SecureSettings mSecureSettings; private final NotificationDismissibilityProvider mDismissibilityProvider; private final ActivityStarter mActivityStarter; + private final SensitiveNotificationProtectionController + mSensitiveNotificationProtectionController; private View mLongPressedView; @@ -295,6 +299,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; + private final Runnable mSensitiveStateChangedListener = new Runnable() { + @Override + public void run() { + // Animate false to protect against screen recording capturing content + // during the animation + updateSensitivenessWithAnimation(false); + } + }; + private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> { if (mView.isExpanded()) { // The bottom might change because we're using the final actual height of the view @@ -342,6 +355,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { private float mMaxAlphaForExpansion = 1.0f; private float mMaxAlphaForUnhide = 1.0f; + /** + * Maximum alpha when to and from or sitting idle on the glanceable hub. Will be 1.0f when the + * hub is not visible or transitioning. + */ + private float mMaxAlphaForGlanceableHub = 1.0f; + private final NotificationListViewBinder mViewBinder; private void updateResources() { @@ -399,7 +418,20 @@ public class NotificationStackScrollLayoutController implements Dumpable { } private void updateSensitivenessWithAnimation(boolean animate) { - mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + Trace.beginSection("NSSLC.updateSensitivenessWithAnimation"); + if (screenshareNotificationHiding()) { + boolean isAnyProfilePublic = mLockscreenUserManager.isAnyProfilePublicMode(); + boolean isSensitiveContentProtectionActive = + mSensitiveNotificationProtectionController.isSensitiveStateActive(); + boolean isSensitive = isAnyProfilePublic || isSensitiveContentProtectionActive; + + // Only animate if in a non-sensitive state (not screen sharing) + boolean shouldAnimate = animate && !isSensitiveContentProtectionActive; + mView.updateSensitiveness(shouldAnimate, isSensitive); + } else { + mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + } + Trace.endSection(); } /** @@ -708,7 +740,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { SecureSettings secureSettings, NotificationDismissibilityProvider dismissibilityProvider, ActivityStarter activityStarter, - SplitShadeStateController splitShadeStateController) { + SplitShadeStateController splitShadeStateController, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController) { mView = view; mKeyguardTransitionRepo = keyguardTransitionRepo; mViewBinder = viewBinder; @@ -756,6 +789,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mSecureSettings = secureSettings; mDismissibilityProvider = dismissibilityProvider; mActivityStarter = activityStarter; + mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController; mView.passSplitShadeStateController(splitShadeStateController); mDumpManager.registerDumpable(this); updateResources(); @@ -860,6 +894,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mDeviceProvisionedListener.onDeviceProvisionedChanged(); + if (screenshareNotificationHiding()) { + mSensitiveNotificationProtectionController + .registerSensitiveStateListener(mSensitiveStateChangedListener); + } + if (mView.isAttachedToWindow()) { mOnAttachStateChangeListener.onViewAttachedToWindow(mView); } @@ -1252,9 +1291,19 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Sets the max alpha value for notifications when idle on the glanceable hub or when + * transitioning to/from the glanceable hub. + */ + public void setMaxAlphaForGlanceableHub(float alpha) { + mMaxAlphaForGlanceableHub = alpha; + updateAlpha(); + } + private void updateAlpha() { if (mView != null) { - mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide)); + mView.setAlpha(Math.min(mMaxAlphaForExpansion, + Math.min(mMaxAlphaForUnhide, mMaxAlphaForGlanceableHub))); } } @@ -1746,6 +1795,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion); pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide); + pw.println("mMaxAlphaForGlanceableHub=" + mMaxAlphaForGlanceableHub); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 12927b87630e..f842e304ffdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main @@ -125,7 +124,14 @@ object SharedNotificationContainerBinder { launch { viewModel.translationY.collect { controller.setTranslationY(it) } } - launch { viewModel.alpha.collect { controller.setMaxAlphaForExpansion(it) } } + launch { + viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) } + } + launch { + viewModel.glanceableHubAlpha.collect { + controller.setMaxAlphaForGlanceableHub(it) + } + } } } 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 a48fb45861d2..99cd89b84c14 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 @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -27,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -61,8 +64,11 @@ constructor( private val keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, + communalInteractor: CommunalInteractor, occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel ) { private val statesForConstrainedNotifications = setOf( @@ -87,6 +93,20 @@ constructor( .distinctUntilChanged() .onStart { emit(false) } + private val lockscreenToGlanceableHubRunning = + keyguardTransitionInteractor + .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + + private val glanceableHubToLockscreenRunning = + keyguardTransitionInteractor + .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + val shadeCollapseFadeInComplete = MutableStateFlow(false) val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = @@ -144,6 +164,24 @@ constructor( initialValue = false, ) + /** Are we purely on the glanceable hub without the shade/qs? */ + internal val isOnGlanceableHubWithoutShade: Flow<Boolean> = + combine( + communalInteractor.isIdleOnCommunal, + // Shade with notifications + shadeInteractor.shadeExpansion.map { it > 0f }, + // Shade without notifications, quick settings only (pull down from very top on + // lockscreen) + shadeInteractor.qsExpansion.map { it > 0f }, + ) { isIdleOnCommunal, isShadeVisible, qsExpansion -> + isIdleOnCommunal && !(isShadeVisible || qsExpansion) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** Fade in only for use after the shade collapses */ val shadeCollpaseFadeIn: Flow<Boolean> = flow { @@ -201,7 +239,7 @@ constructor( initialValue = NotificationContainerBounds(), ) - val alpha: Flow<Float> = + val expansionAlpha: Flow<Float> = // Due to issues with the legacy shade, some shade expansion events are sent incorrectly, // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while @@ -235,6 +273,43 @@ constructor( } /** + * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition + * or idle on the glanceable hub. + * + * Must return 1.0f when not controlling the alpha since notifications does a min of all the + * alpha sources. + */ + val glanceableHubAlpha: Flow<Float> = + isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade -> + combineTransform( + lockscreenToGlanceableHubRunning, + glanceableHubToLockscreenRunning, + merge( + lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, + glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + ) + .onStart { + // Transition flows don't emit a value on start, kick things off so the + // combine starts. + emit(1f) + } + ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> + if (isOnGlanceableHubWithoutShade) { + // Notifications should not be visible on the glanceable hub. + // TODO(b/321075734): implement a way to actually set the notifications to gone + // while on the hub instead of just adjusting alpha + emit(0f) + } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) { + emit(alpha) + } else { + // Not on the hub and no transitions running, return full visibility so we don't + // block the notifications from showing. + emit(1f) + } + } + } + + /** * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be * translated as the keyguard fades out. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index ae04eaf49b65..459b368b5ac9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -60,6 +60,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.ScrimAlpha; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -217,6 +218,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private final KeyguardInteractor mKeyguardInteractor; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -311,6 +313,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, AlternateBouncerToGoneTransitionViewModel alternateBouncerToGoneTransitionViewModel, KeyguardTransitionInteractor keyguardTransitionInteractor, + KeyguardInteractor keyguardInteractor, WallpaperRepository wallpaperRepository, @Main CoroutineDispatcher mainDispatcher, LargeScreenShadeInterpolator largeScreenShadeInterpolator) { @@ -357,6 +360,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel; mAlternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mKeyguardInteractor = keyguardInteractor; mWallpaperRepository = wallpaperRepository; mMainDispatcher = mainDispatcher; } @@ -759,7 +763,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump // see: b/186644628 mNotificationsScrim.setDrawableBounds(left - 1, top, right + 1, bottom); mScrimBehind.setBottomEdgePosition((int) top); + mKeyguardInteractor.setTopClippingBounds((int) top); } else { + mKeyguardInteractor.setTopClippingBounds(null); mNotificationsScrim.setDrawableBounds(left, top, right, bottom); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 1528c9befda7..1414150c5511 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -159,7 +159,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { public void showNotification(@NonNull NotificationEntry entry) { mLogger.logShowNotification(entry); addEntry(entry); - updateNotification(entry.getKey(), true /* show */); + updateNotification(entry.getKey(), true /* shouldHeadsUpAgain */); entry.setInterruption(); } @@ -190,12 +190,12 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { /** * Called when the notification state has been updated. * @param key the key of the entry that was updated - * @param show whether the notification should show again and force reevaluation of - * removal time + * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation + * of removal time */ - public void updateNotification(@NonNull String key, boolean show) { + public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); - mLogger.logUpdateNotification(key, show, headsUpEntry != null); + mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null); if (headsUpEntry == null) { // the entry was released before this update (i.e by a listener) This can happen // with the groupmanager @@ -204,7 +204,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - if (show) { + if (shouldHeadsUpAgain) { headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); if (headsUpEntry != null) { setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt index b8c7e202ce7c..a7352be8d80a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt @@ -182,7 +182,7 @@ interface HeadsUpManager : Dumpable { */ fun unpinAll(userUnPinned: Boolean) - fun updateNotification(key: String, alert: Boolean) + fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) } /** Sets the animation state of the HeadsUpManager. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java new file mode 100644 index 000000000000..970cc75bbb6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * A controller which provides the current sensitive notification protections status as well as + * to assist in feature usage and exemptions + */ +public interface SensitiveNotificationProtectionController { + /** + * Register a runnable that triggers on changes to protection state + * + * <p> onSensitiveStateChanged not invoked on registration + */ + void registerSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Unregister a previously registered onSensitiveStateChanged runnable */ + void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Return {@code true} if device in state in which notifications should be protected */ + boolean isSensitiveStateActive(); + + /** Return {@code true} when notification should be protected */ + boolean shouldProtectNotification(NotificationEntry entry); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java new file mode 100644 index 000000000000..3c4ca4465874 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import static com.android.systemui.Flags.screenshareNotificationHiding; + +import android.media.projection.MediaProjectionInfo; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.Trace; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.ListenerSet; + +import javax.inject.Inject; + +/** Implementation of SensitiveNotificationProtectionController. **/ +@SysUISingleton +public class SensitiveNotificationProtectionControllerImpl + implements SensitiveNotificationProtectionController { + private final MediaProjectionManager mMediaProjectionManager; + private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); + private volatile MediaProjectionInfo mProjection; + + @VisibleForTesting + final MediaProjectionManager.Callback mMediaProjectionCallback = + new MediaProjectionManager.Callback() { + @Override + public void onStart(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStart"); + mProjection = info; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + + @Override + public void onStop(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStop"); + mProjection = null; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + }; + + @Inject + public SensitiveNotificationProtectionControllerImpl( + MediaProjectionManager mediaProjectionManager, + @Main Handler mainHandler) { + mMediaProjectionManager = mediaProjectionManager; + + if (screenshareNotificationHiding()) { + mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); + } + } + + @Override + public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.addIfAbsent(onSensitiveStateChanged); + } + + @Override + public void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.remove(onSensitiveStateChanged); + } + + @Override + public boolean isSensitiveStateActive() { + // TODO(b/316955558): Add disabled by developer option + // TODO(b/316955306): Add feature exemption for sysui and bug handlers + // TODO(b/316955346): Add feature exemption for single app screen sharing + return mProjection != null; + } + + @Override + public boolean shouldProtectNotification(NotificationEntry entry) { + if (!isSensitiveStateActive()) { + return false; + } + + // Exempt foreground service notifications from protection in effort to keep screen share + // stop actions easily accessible + // TODO(b/316955208): Exempt FGS notifications only for app that started projection + return !entry.getSbn().getNotification().isFgsOrUij(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 3304b9827fd8..15200bd0ac54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -60,6 +60,8 @@ import com.android.systemui.statusbar.policy.RotationLockController; import com.android.systemui.statusbar.policy.RotationLockControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionControllerImpl; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.SplitShadeStateControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; @@ -146,6 +148,11 @@ public interface StatusBarPolicyModule { /** */ @Binds + SensitiveNotificationProtectionController provideSensitiveNotificationProtectionController( + SensitiveNotificationProtectionControllerImpl controllerImpl); + + /** */ + @Binds UserInfoController provideUserInfoContrller(UserInfoControllerImpl controllerImpl); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt index ce811e205436..10137a0d3430 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt @@ -17,10 +17,16 @@ package com.android.systemui.statusbar.ui import com.android.internal.policy.SystemBarUtils +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -31,6 +37,8 @@ import kotlinx.coroutines.flow.onStart class SystemBarUtilsState @Inject constructor( + @Background bgContext: CoroutineContext, + @Main mainContext: CoroutineContext, configurationController: ConfigurationController, proxy: SystemBarUtilsProxy, ) { @@ -38,5 +46,10 @@ constructor( val statusBarHeight: Flow<Int> = configurationController.onConfigChanged .onStart<Any> { emit(Unit) } + .flowOn(mainContext) + .conflate() .map { proxy.getStatusBarHeight() } + .distinctUntilChanged() + .flowOn(bgContext) + .conflate() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index c2efc05132ba..c2efc05132ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index 0959f1b2bcf6..0959f1b2bcf6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 1281e4409a83..1281e4409a83 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index b57cf53911e9..754a7fd81475 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -97,6 +97,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.scene.FakeWindowRootViewComponent; @@ -150,6 +151,7 @@ import kotlinx.coroutines.test.TestScope; @TestableLooper.RunWithLooper @SmallTest public class KeyguardViewMediatorTest extends SysuiTestCase { + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private KeyguardViewMediator mViewMediator; private final TestScope mTestScope = TestScopeProvider.getTestScope(); @@ -265,7 +267,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags); + mSceneContainerFlags, + mKosmos::getCommunalInteractor); mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 90eaa5a2eeaf..dafd9e64371b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -23,8 +23,9 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.flags.FakeFeatureFlags @@ -34,6 +35,8 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel @@ -42,11 +45,15 @@ import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest 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.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -57,7 +64,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent @@ -83,7 +89,8 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class KeyguardTransitionScenariosTest : SysuiTestCase() { - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var bouncerRepository: FakeKeyguardBouncerRepository @@ -119,17 +126,14 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - val testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) - keyguardRepository = FakeKeyguardRepository() - bouncerRepository = FakeKeyguardBouncerRepository() + keyguardRepository = kosmos.fakeKeyguardRepository + bouncerRepository = kosmos.fakeKeyguardBouncerRepository commandQueue = FakeCommandQueue() - shadeRepository = FakeShadeRepository() - transitionRepository = spy(FakeKeyguardTransitionRepository()) + shadeRepository = kosmos.fakeShadeRepository + transitionRepository = spy(kosmos.fakeKeyguardTransitionRepository) powerInteractor = PowerInteractorFactory.create().powerInteractor - communalInteractor = - CommunalInteractorFactory.create(testScope = testScope).communalInteractor + communalInteractor = kosmos.communalInteractor whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) @@ -160,8 +164,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -184,8 +188,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromPrimaryBouncerTransitionInteractor = FromPrimaryBouncerTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -199,8 +203,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDreamingTransitionInteractor = FromDreamingTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -210,8 +214,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDreamingLockscreenHostedTransitionInteractor = FromDreamingLockscreenHostedTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -221,8 +225,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromAodTransitionInteractor = FromAodTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -232,8 +236,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromGoneTransitionInteractor = FromGoneTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -244,8 +248,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDozingTransitionInteractor = FromDozingTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -257,8 +261,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromOccludedTransitionInteractor = FromOccludedTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -269,8 +273,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromAlternateBouncerTransitionInteractor = FromAlternateBouncerTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -281,8 +285,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromGlanceableHubTransitionInteractor = FromGlanceableHubTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, glanceableHubTransitions = glanceableHubTransitions, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 6be92756ba8a..ba7927d6b8bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -25,8 +25,7 @@ import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.FakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController @@ -43,6 +42,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -79,6 +79,8 @@ import org.mockito.junit.MockitoJUnit @TestableLooper.RunWithLooper(setAsMainLooper = true) class MediaHierarchyManagerTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock private lateinit var lockHost: MediaHost @Mock private lateinit var qsHost: MediaHost @Mock private lateinit var qqsHost: MediaHost @@ -110,10 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() - private val communalRepository = - FakeCommunalRepository(applicationScope = testScope.backgroundScope) - private val communalInteractor = - CommunalInteractorFactory.create(communalRepository = communalRepository).communalInteractor + private val communalInteractor = kosmos.communalInteractor private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 0a464e6047d3..b701d7f315bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.models.player.MediaViewHolder @@ -171,6 +172,38 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test + fun testObtainViewState_expandedMatchesParentHeight() { + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + player.measureState = + TransitionViewState().apply { + this.height = 100 + this.measureHeight = 100 + } + mediaHostStateHolder.expandedMatchesParentHeight = true + mediaHostStateHolder.expansion = 1f + mediaHostStateHolder.measurementInput = + MeasurementInput( + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), + ) + + // Assign the height of each expanded layout + MediaViewHolder.backgroundIds.forEach { id -> + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 100 + } + + mediaViewController.obtainViewState(mediaHostStateHolder) + + // Verify height of each expanded layout is updated to match constraint + MediaViewHolder.backgroundIds.forEach { id -> + assertTrue( + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == + ConstraintSet.MATCH_CONSTRAINT + ) + } + } + + @Test fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() { whenever(mockViewState.copy()).thenReturn(mockCopiedState) whenever(mockCopiedState.widgetStates) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index 72847a6b6c45..c6cfabc61300 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -414,6 +414,18 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_verifyDeviceListCallback() { + // This test relies on mMediaOutputController.start being called while the selected device + // list has exactly one item, and that item's id is: + // - Different from both ids in mMediaDevices. + // - Different from the id of the route published by the device under test (usually the + // built-in speakers). + // So mock the selected device to respect these two preconditions. + MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class); + when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID); + doReturn(List.of(mockSelectedMediaDevice)) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaOutputController.start(mCb); reset(mCb); @@ -434,6 +446,18 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() { + // This test relies on mMediaOutputController.start being called while the selected device + // list has exactly one item, and that item's id is: + // - Different from both ids in mMediaDevices. + // - Different from the id of the route published by the device under test (usually the + // built-in speakers). + // So mock the selected device to respect these two preconditions. + MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class); + when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID); + doReturn(List.of(mockSelectedMediaDevice)) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + when(mMediaDevice1.getFeatures()).thenReturn( ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index b7a9ea751438..c1f5d85ca0f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -25,14 +25,16 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.compose.ComposeFacade import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -52,6 +54,8 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest class GlanceableHubContainerControllerTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock private lateinit var communalViewModel: CommunalViewModel @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock private lateinit var shadeInteractor: ShadeInteractor @@ -71,9 +75,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create() - communalInteractor = withDeps.communalInteractor - communalRepository = withDeps.communalRepository + communalInteractor = kosmos.communalInteractor + communalRepository = kosmos.fakeCommunalRepository underTest = GlanceableHubContainerController( @@ -91,7 +94,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { .thenReturn(bouncerShowingFlow) whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow) - overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH) } @Test @@ -132,7 +136,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { initAndAttachContainerView() // Touch events are intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() } @Test @@ -144,7 +148,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { // Initial touch down is intercepted, and so are touches outside of the region, until an up // event is received. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue() assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() @@ -165,6 +169,17 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() { + // Communal is open. + communalRepository.setDesiredScene(CommunalSceneKey.Communal) + + initAndAttachContainerView() + + // Touch event in the top swipe reqgion is not intercepted. + assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + } + + @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() { // Communal is open. communalRepository.setDesiredScene(CommunalSceneKey.Communal) @@ -210,11 +225,29 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { companion object { private const val CONTAINER_WIDTH = 100 private const val CONTAINER_HEIGHT = 100 - private const val SWIPE_REGION_WIDTH = 20 - - private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) - private val DOWN_IN_SWIPE_REGION_EVENT = + private const val RIGHT_SWIPE_REGION_WIDTH = 20 + private const val TOP_SWIPE_REGION_WIDTH = 20 + + private val DOWN_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + CONTAINER_WIDTH.toFloat(), + CONTAINER_HEIGHT.toFloat(), + 0 + ) + private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0) + private val DOWN_IN_TOP_SWIPE_REGION_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + 0f, + TOP_SWIPE_REGION_WIDTH.toFloat(), + 0 + ) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 11da2376328a..200e758245a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -57,7 +57,6 @@ 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.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlagsClassic; @@ -202,8 +201,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { new ConfigurationInteractor(configurationRepository), shadeRepository, () -> sceneInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); FakeKeyguardTransitionRepository keyguardTransitionRepository = new FakeKeyguardTransitionRepository(); @@ -303,7 +301,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags) { + mSceneContainerFlags, + () -> communalInteractor) { @Override protected boolean isDebuggable() { return false; @@ -451,6 +450,24 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void setCommunalShowing_userTimeout() { + setKeyguardShowing(); + clearInvocations(mWindowManager); + + mNotificationShadeWindowController.onCommunalShowingChanged(true); + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat(mLayoutParameters.getValue().userActivityTimeout) + .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS); + clearInvocations(mWindowManager); + + // Bouncer showing over communal overrides communal value + mNotificationShadeWindowController.setBouncerShowing(true); + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat(mLayoutParameters.getValue().userActivityTimeout) + .isEqualTo(KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS); + } + + @Test public void setKeyguardShowing_notFocusable_byDefault() { mNotificationShadeWindowController.setKeyguardShowing(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index 8f46a37bf540..6fc88ce95325 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -43,7 +43,6 @@ import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepositor import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; @@ -233,8 +232,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { new ConfigurationInteractor(configurationRepository), mShadeRepository, () -> sceneInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); FakeKeyguardTransitionRepository keyguardTransitionRepository = new FakeKeyguardTransitionRepository(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index 05e866e85112..13934dac2401 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -27,7 +27,7 @@ import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepositor import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeCommandQueue @@ -144,7 +144,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { { fromLockscreenTransitionInteractor }, { fromPrimaryBouncerTransitionInteractor } ) - val communalInteractor = CommunalInteractorFactory.create().communalInteractor + val communalInteractor = kosmos.communalInteractor fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( keyguardTransitionRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index df547ae5883e..350ed2d9ff22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -17,9 +17,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager @@ -33,6 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -55,28 +58,31 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { val statusBarStateController: StatusBarStateController = mock() val keyguardStateController: KeyguardStateController = mock() val mSelectedUserInteractor: SelectedUserInteractor = mock() + val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = + mock() val coordinator: SensitiveContentCoordinator = - DaggerTestSensitiveContentCoordinatorComponent - .factory() - .create( - dynamicPrivacyController, - lockscreenUserManager, - keyguardUpdateMonitor, - statusBarStateController, - keyguardStateController, - mSelectedUserInteractor) - .coordinator + DaggerTestSensitiveContentCoordinatorComponent.factory() + .create( + dynamicPrivacyController, + lockscreenUserManager, + keyguardUpdateMonitor, + statusBarStateController, + keyguardStateController, + mSelectedUserInteractor, + sensitiveNotificationProtectionController + ) + .coordinator @Test fun onDynamicPrivacyChanged_invokeInvalidationListener() { coordinator.attach(pipeline) - val invalidator = withArgCaptor<Invalidator> { - verify(pipeline).addPreRenderInvalidator(capture()) - } - val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> { - verify(dynamicPrivacyController).addListener(capture()) - } + val invalidator = + withArgCaptor<Invalidator> { verify(pipeline).addPreRenderInvalidator(capture()) } + val dynamicPrivacyListener = + withArgCaptor<DynamicPrivacyController.Listener> { + verify(dynamicPrivacyController).addListener(capture()) + } val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>() invalidator.setInvalidationListener(invalidationListener) @@ -89,9 +95,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -105,11 +112,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -123,11 +178,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -141,17 +244,87 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -159,17 +332,92 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -179,15 +427,37 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -195,19 +465,96 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) val entry = fakeNotification(2, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -217,9 +564,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -227,9 +575,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any())) - .thenReturn(true) - + .thenReturn(true) val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + whenever(sensitiveNotificationProtectionController.shouldProtectNotification(any())) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -237,15 +587,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry { - val mockUserHandle = mock<UserHandle>().apply { - whenever(identifier).thenReturn(notifUserId) - } - val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply { - whenever(user).thenReturn(mockUserHandle) - } - val mockEntry = mock<NotificationEntry>().apply { - whenever(sbn).thenReturn(mockSbn) - } + val mockUserHandle = + mock<UserHandle>().apply { whenever(identifier).thenReturn(notifUserId) } + val mockSbn: StatusBarNotification = + mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) } + val mockEntry = mock<NotificationEntry>().apply { whenever(sbn).thenReturn(mockSbn) } whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) whenever(mockEntry.rowExists()).thenReturn(true) return object : ListEntry("key", 0) { @@ -268,6 +614,8 @@ interface TestSensitiveContentCoordinatorComponent { @BindsInstance statusBarStateController: StatusBarStateController, @BindsInstance keyguardStateController: KeyguardStateController, @BindsInstance selectedUserInteractor: SelectedUserInteractor, + @BindsInstance + sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, ): TestSensitiveContentCoordinatorComponent } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 89f826b2049d..1ab4c32c7d08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.FlowKt.emptyFlow; @@ -39,6 +41,7 @@ import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDis import android.metrics.LogMaker; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -101,6 +104,7 @@ 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.ResourcesSplitShadeStateController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.SecureSettings; @@ -172,10 +176,16 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private ActivityStarter mActivityStarter; @Mock private KeyguardTransitionRepository mKeyguardTransitionRepo; @Mock private NotificationListViewBinder mViewBinder; + @Mock + private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController; + + @Captor + private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; + private final ActiveNotificationListRepository mActiveNotificationsRepository = new ActiveNotificationListRepository(); @@ -386,6 +396,23 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + public void testOnUserChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnUserChange_verifySensitiveProfile() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); initController(/* viewIsAttached= */ true); @@ -403,6 +430,80 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveProfile_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveActive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnStatePostChange_verifyIfProfileIsPublic() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); @@ -418,6 +519,194 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfProfileIsPublic_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfSensitiveActive_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyNotSensitive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyProfileIsPublic_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifySensitiveActive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()) + .thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfSensitiveActive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test public void testOnMenuShownLogging() { ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker( @@ -666,6 +955,20 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean()); } + @Test + @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerNotRegistered() { + initController(/* viewIsAttached= */ true); + verifyZeroInteractions(mSensitiveNotificationProtectionController); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerRegistered() { + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any()); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } @@ -744,7 +1047,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSecureSettings, mock(NotificationDismissibilityProvider.class), mActivityStarter, - new ResourcesSplitShadeStateController()); + new ResourcesSplitShadeStateController(), + mSensitiveNotificationProtectionController); } static class LogMatcher implements ArgumentMatcher<LogMaker> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index a824bc4f803f..06298b78ae57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -25,6 +25,9 @@ import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -33,20 +36,19 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -68,6 +70,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val keyguardInteractor = kosmos.keyguardInteractor val keyguardRootViewModel = kosmos.keyguardRootViewModel val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val communalInteractor = kosmos.communalInteractor val shadeRepository = kosmos.shadeRepository val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper @@ -214,6 +217,61 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun glanceableHubAlpha() = + testScope.runTest { + val alpha by collectLastValue(underTest.glanceableHubAlpha) + + // Start on lockscreen + showLockscreen() + assertThat(alpha).isEqualTo(1f) + + // Start transitioning to glanceable hub + val progress = 0.6f + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 0f, + ) + ) + runCurrent() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = progress, + ) + ) + runCurrent() + // Expected alpha is inverse of progress as notifications are fading away + assertThat(alpha).isEqualTo(1 - progress) + + // Finish transition to glanceable hub + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 1f, + ) + ) + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(alpha).isEqualTo(0f) + + // While state is GLANCEABLE_HUB, verify alpha is restored to full if glanceable hub is + // not fully visible. + shadeRepository.setLockscreenShadeExpansion(0.1f) + assertThat(alpha).isEqualTo(1f) + } + + @Test fun validateMarginTop() = testScope.runTest { overrideResource(R.bool.config_use_large_screen_shade_header, false) @@ -302,6 +360,43 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun isOnGlanceableHubWithoutShade() = + testScope.runTest { + val isOnGlanceableHubWithoutShade by + collectLastValue(underTest.isOnGlanceableHubWithoutShade) + + // Start on lockscreen + showLockscreen() + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + // Move to glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(isOnGlanceableHubWithoutShade).isTrue() + + // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isTrue() + } + + @Test fun boundsOnLockscreenNotInSplitShade() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 4827c92ce452..d9eaea1367cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -66,6 +66,7 @@ import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -145,6 +146,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private KeyguardInteractor mKeyguardInteractor; private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository(); @Mock private CoroutineDispatcher mMainDispatcher; @Mock private TypedArray mMockTypedArray; @@ -292,6 +294,7 @@ public class ScrimControllerTest extends SysuiTestCase { mPrimaryBouncerToGoneTransitionViewModel, mAlternateBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, + mKeyguardInteractor, mWallpaperRepository, mMainDispatcher, mLinearLargeScreenShadeInterpolator); @@ -1000,6 +1003,7 @@ public class ScrimControllerTest extends SysuiTestCase { mPrimaryBouncerToGoneTransitionViewModel, mAlternateBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, + mKeyguardInteractor, mWallpaperRepository, mMainDispatcher, mLinearLargeScreenShadeInterpolator); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt new file mode 100644 index 000000000000..cd5d5ed0d08e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -0,0 +1,231 @@ +/* + * 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.policy + +import android.app.Notification +import android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Handler +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { + @Mock private lateinit var handler: Handler + + @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + + @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo + + @Mock private lateinit var listener1: Runnable + @Mock private lateinit var listener2: Runnable + @Mock private lateinit var listener3: Runnable + + @Captor + private lateinit var mediaProjectionCallbackCaptor: + ArgumentCaptor<MediaProjectionManager.Callback> + + private lateinit var controller: SensitiveNotificationProtectionControllerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + // Obtain useful MediaProjectionCallback + verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any()) + } + + @Test + fun init_flagEnabled_registerMediaProjectionManagerCallback() { + assertNotNull(mediaProjectionCallbackCaptor.value) + } + + @Test + fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() { + mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + reset(mediaProjectionManager) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + verifyZeroInteractions(mediaProjectionManager) + } + + @Test + fun registerSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_afterProjectionActive() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + controller.registerSensitiveStateListener(listener1) + verifyZeroInteractions(listener1) + + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1).run() + } + + @Test + fun unregisterSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + } + + @Test + fun unregisterSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + controller.registerSensitiveStateListener(listener3) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + verify(listener3, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + controller.unregisterSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + verifyNoMoreInteractions(listener2) + verify(listener3, times(4)).run() + } + + @Test + fun isSensitiveStateActive_projectionInactive_false() { + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionInactiveAfterActive_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActiveAfterInactive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun shouldProtectNotification_projectionInactive_false() { + val notificationEntry = mock(NotificationEntry::class.java) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_fgsNotification_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(true) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_notFgsNotification_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(false) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } +} 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 589f7c23ea13..93d8dcc8f092 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -100,7 +100,6 @@ 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.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; @@ -443,8 +442,7 @@ public class BubblesTest extends SysuiTestCase { () -> keyguardInteractor, () -> mFromLockscreenTransitionInteractor, () -> mFromPrimaryBouncerTransitionInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor( keyguardTransitionRepository, @@ -536,7 +534,8 @@ public class BubblesTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags + mSceneContainerFlags, + mKosmos::getCommunalInteractor ); mNotificationShadeWindowController.fetchWindowRootView(); mNotificationShadeWindowController.attach(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt deleted file mode 100644 index 1ba863052e2d..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.communal.domain.interactor - -import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository -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.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository -import com.android.systemui.util.mockito.mock -import kotlinx.coroutines.test.TestScope - -// TODO(b/319335645): get rid of me and use kosmos. -object CommunalInteractorFactory { - - @JvmOverloads - @JvmStatic - fun create( - testScope: TestScope = TestScope(), - communalRepository: FakeCommunalRepository = - FakeCommunalRepository(testScope.backgroundScope), - keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(), - widgetRepository: FakeCommunalWidgetRepository = - FakeCommunalWidgetRepository(testScope.backgroundScope), - mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), - smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), - tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), - communalPrefsRepository: FakeCommunalPrefsRepository = FakeCommunalPrefsRepository(), - appWidgetHost: CommunalAppWidgetHost = mock(), - editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(), - ): WithDependencies { - val keyguardInteractor = - KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor - val communalTutorialInteractor = - CommunalTutorialInteractor( - testScope.backgroundScope, - tutorialRepository, - keyguardInteractor, - communalRepository, - ) - - return WithDependencies( - testScope, - communalRepository, - widgetRepository, - communalPrefsRepository, - mediaRepository, - smartspaceRepository, - tutorialRepository, - keyguardRepository, - keyguardInteractor, - communalTutorialInteractor, - appWidgetHost, - editWidgetsActivityStarter, - CommunalInteractor( - testScope.backgroundScope, - communalRepository, - widgetRepository, - communalPrefsRepository, - mediaRepository, - smartspaceRepository, - keyguardInteractor, - appWidgetHost, - editWidgetsActivityStarter, - ), - ) - } - - data class WithDependencies( - val testScope: TestScope, - val communalRepository: FakeCommunalRepository, - val widgetRepository: FakeCommunalWidgetRepository, - val communalPrefsRepository: FakeCommunalPrefsRepository, - val mediaRepository: FakeCommunalMediaRepository, - val smartspaceRepository: FakeSmartspaceRepository, - val tutorialRepository: FakeCommunalTutorialRepository, - val keyguardRepository: FakeKeyguardRepository, - val keyguardInteractor: KeyguardInteractor, - val tutorialInteractor: CommunalTutorialInteractor, - val appWidgetHost: CommunalAppWidgetHost, - val editWidgetsActivityStarter: EditWidgetsActivityStarter, - val communalInteractor: CommunalInteractor, - ) -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 7e3c10b18964..1abf71fde14c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.smartspace.data.repository.smartspaceRepository +import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -36,6 +37,7 @@ val Kosmos.communalInteractor by Fixture { mediaRepository = communalMediaRepository, communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, + userRepository = userRepository, appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, editWidgetsActivityStarter = editWidgetsActivityStarter, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt index baba88db8e55..adaea7cbf64d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt @@ -29,5 +29,6 @@ val Kosmos.communalTutorialInteractor by communalTutorialRepository = communalTutorialRepository, keyguardInteractor = keyguardInteractor, communalRepository = communalRepository, + communalInteractor = communalInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 59f56dd18f0e..5766f7a9028c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -130,6 +130,8 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { private val _isEncryptedOrLockdown = MutableStateFlow(true) override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown + override val topClippingBounds = MutableStateFlow<Int?>(null) + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 5564767e0715..d376f126e2c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -33,6 +34,7 @@ val Kosmos.keyguardRootViewModel by Fixture { deviceEntryInteractor = deviceEntryInteractor, dozeParameters = dozeParameters, keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index cc0449d7e7bb..321f94468a2e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -26,6 +26,7 @@ import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -72,6 +73,7 @@ class KosmosJavaAdapter( val powerInteractor by lazy { kosmos.powerInteractor } val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } + val communalInteractor by lazy { kosmos.communalInteractor } init { kosmos.applicationContext = testCase.context 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 5ef9a8e8edd8..db4050905200 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 @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel import com.android.systemui.kosmos.Kosmos @@ -33,7 +36,10 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, shadeInteractor = shadeInteractor, + communalInteractor = communalInteractor, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt index e208add32efa..5476d5509c0c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt @@ -17,7 +17,15 @@ package com.android.systemui.statusbar.ui import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.policy.configurationController -val Kosmos.systemBarUtilsState by - Kosmos.Fixture { SystemBarUtilsState(configurationController, systemBarUtilsProxy) } +val Kosmos.systemBarUtilsState by Fixture { + SystemBarUtilsState( + testDispatcher, + testDispatcher, + configurationController, + systemBarUtilsProxy, + ) +} diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 3d8d7b738233..d656892062d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4413,25 +4413,50 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) { mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY); + final ComponentName componentName = info.getComponentName(); // Warning is not required if the service is already enabled. synchronized (mLock) { final AccessibilityUserState userState = getCurrentUserStateLocked(); - if (userState.getEnabledServicesLocked().contains(info.getComponentName())) { + if (userState.getEnabledServicesLocked().contains(componentName)) { return false; } } // Warning is not required if the service is already assigned to a shortcut. for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) { if (getAccessibilityShortcutTargets(shortcutType).contains( - info.getComponentName().flattenToString())) { + componentName.flattenToString())) { return false; } } + // Warning is not required if the service is preinstalled and in the + // trustedAccessibilityServices allowlist. + if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices() + && isAccessibilityServicePreinstalledAndTrusted(info)) { + return false; + } + // Warning is required by default. return true; } + private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) { + final ComponentName componentName = info.getComponentName(); + final boolean isPreinstalled = + info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); + if (isPreinstalled) { + final String[] trustedAccessibilityServices = + mContext.getResources().getStringArray( + R.array.config_trustedAccessibilityServices); + if (Arrays.stream(trustedAccessibilityServices) + .map(ComponentName::unflattenFromString) + .anyMatch(componentName::equals)) { + return true; + } + } + return false; + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 42ab05fdd231..4d42f154a392 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -58,11 +58,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.autofill.AutofillManager; import android.widget.ImageView; import android.widget.RemoteViews; +import android.widget.ScrollView; import android.widget.TextView; import com.android.internal.R; @@ -370,9 +372,23 @@ final class SaveUi { params.windowAnimations = R.style.AutofillSaveAnimation; params.setTrustedOverlay(); + ScrollView scrollView = view.findViewById(R.id.autofill_sheet_scroll_view); + + View divider = view.findViewById(R.id.autofill_sheet_divider); + + ViewTreeObserver observer = scrollView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(() -> adjustDividerVisibility(scrollView, divider)); + + scrollView.getViewTreeObserver() + .addOnScrollChangedListener(() -> adjustDividerVisibility(scrollView, divider)); show(); } + private void adjustDividerVisibility(ScrollView scrollView, View divider) { + boolean canScrollDown = scrollView.canScrollVertically(1); // 1 to check scrolling down + divider.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); + } + private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView, @NonNull ValueFinder valueFinder, @NonNull SaveInfo info) { final CustomDescription customDescription = info.getCustomDescription(); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 50e18628852d..84e1d9062fd5 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -247,6 +247,11 @@ public class CompanionDeviceManagerService extends SystemService { final Context context = getContext(); mPersistentStore = new PersistentDataStore(); + mAssociationRequestsProcessor = new AssociationRequestsProcessor( + /* cdmService */ this, mAssociationStore); + mBackupRestoreProcessor = new BackupRestoreProcessor( + /* cdmService */ this, mAssociationStore, mPersistentStore, + mSystemDataTransferRequestStore, mAssociationRequestsProcessor); loadAssociationsFromDisk(); mAssociationStore.registerListener(mAssociationStoreChangeListener); @@ -254,17 +259,12 @@ public class CompanionDeviceManagerService extends SystemService { mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager, mAssociationStore, mDevicePresenceCallback); - mAssociationRequestsProcessor = new AssociationRequestsProcessor( - /* cdmService */this, mAssociationStore); mCompanionAppController = new CompanionApplicationController( context, mAssociationStore, mDevicePresenceMonitor); mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); - mBackupRestoreProcessor = new BackupRestoreProcessor( - /* cdmService */ this, mAssociationStore, mPersistentStore, - mSystemDataTransferRequestStore, mAssociationRequestsProcessor); // TODO(b/279663946): move context sync to a dedicated system service mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index 3483c1a1404a..a493d7a57500 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -95,6 +95,7 @@ public class SystemConfig { private static final int ALLOW_OVERRIDE_APP_RESTRICTIONS = 0x100; private static final int ALLOW_IMPLICIT_BROADCASTS = 0x200; private static final int ALLOW_VENDOR_APEX = 0x400; + private static final int ALLOW_SIGNATURE_PERMISSIONS = 0x800; private static final int ALLOW_ALL = ~0; // property for runtime configuration differentiation @@ -597,7 +598,7 @@ public class SystemConfig { // Vendors are only allowed to customize these int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS - | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX; + | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX; if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) { // For backward compatibility vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS); @@ -649,9 +650,9 @@ public class SystemConfig { // TODO(b/157203468): ALLOW_HIDDENAPI_WHITELISTING must be removed because we prohibited // the use of hidden APIs from the product partition. int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS - | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING - | ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS - | ALLOW_VENDOR_APEX; + | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS + | ALLOW_HIDDENAPI_WHITELISTING | ALLOW_ASSOCIATIONS + | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS | ALLOW_VENDOR_APEX; if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) { // TODO(b/157393157): This must check product interface enforcement instead of // DEVICE_INITIAL_SDK_INT for the devices without product interface enforcement. @@ -772,6 +773,8 @@ public class SystemConfig { final boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0; final boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS) != 0; + final boolean allowSignaturePermissions = (permissionFlag & ALLOW_SIGNATURE_PERMISSIONS) + != 0; final boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0; final boolean allowApiWhitelisting = (permissionFlag & ALLOW_HIDDENAPI_WHITELISTING) != 0; @@ -1246,6 +1249,38 @@ public class SystemConfig { XmlUtils.skipCurrentTag(parser); } } break; + case "signature-permissions": { + if (allowSignaturePermissions) { + // signature permissions from system, apex, vendor, product and + // system_ext partitions are stored separately. This is to + // prevent xml files in the vendor partition from granting + // permissions to signature apps in the system partition and vice versa. + boolean vendor = permFile.toPath().startsWith( + Environment.getVendorDirectory().toPath() + "/") + || permFile.toPath().startsWith( + Environment.getOdmDirectory().toPath() + "/"); + boolean product = permFile.toPath().startsWith( + Environment.getProductDirectory().toPath() + "/"); + boolean systemExt = permFile.toPath().startsWith( + Environment.getSystemExtDirectory().toPath() + "/"); + if (vendor) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getVendorSignatureAppAllowlist()); + } else if (product) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getProductSignatureAppAllowlist()); + } else if (systemExt) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getSystemExtSignatureAppAllowlist()); + } else { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getSignatureAppAllowlist()); + } + } else { + logNotAllowedInPartition(name, permFile, parser); + XmlUtils.skipCurrentTag(parser); + } + } break; case "oem-permissions": { if (allowOemPermissions) { readOemPermissions(parser); @@ -1655,6 +1690,12 @@ public class SystemConfig { readPermissionAllowlist(parser, allowlist, "privapp-permissions"); } + private void readSignatureAppPermissions(@NonNull XmlPullParser parser, + @NonNull ArrayMap<String, ArrayMap<String, Boolean>> allowlist) + throws IOException, XmlPullParserException { + readPermissionAllowlist(parser, allowlist, "signature-permissions"); + } + private void readInstallInUserType(XmlPullParser parser, Map<String, Set<String>> doInstallMap, Map<String, Set<String>> nonInstallMap) diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 2cac7a020005..db0f03f9edc6 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -1244,9 +1244,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } private void deliveryTimeout(@NonNull BroadcastProcessQueue queue) { + final int cookie = traceBegin("deliveryTimeout"); synchronized (mService) { deliveryTimeoutLocked(queue); } + traceEnd(cookie); } private void deliveryTimeoutLocked(@NonNull BroadcastProcessQueue queue) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index fb4943a9f4ca..67c23fc4db12 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1328,8 +1328,7 @@ public class InputManagerService extends IInputManager.Stub mPointerIconDisplayContext = null; } - updateAdditionalDisplayInputProperties(displayId, - AdditionalDisplayInputProperties::reset); + updateAdditionalDisplayInputProperties(displayId, AdditionalDisplayInputProperties::reset); // TODO(b/320763728): Rely on WindowInfosListener to determine when a display has been // removed in InputDispatcher instead of this callback. @@ -1812,8 +1811,6 @@ public class InputManagerService extends IInputManager.Stub mPointerIconType = icon.getType(); mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null; - if (!mCurrentDisplayProperties.pointerIconVisible) return false; - return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken); } } @@ -3478,6 +3475,10 @@ public class InputManagerService extends IInputManager.Stub private void applyAdditionalDisplayInputPropertiesLocked( AdditionalDisplayInputProperties properties) { // Handle changes to each of the individual properties. + // TODO(b/293587049): This approach for updating pointer display properties is only for when + // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is + // permanently enabled. + if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) { mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible; if (properties.pointerIconVisible) { @@ -3496,7 +3497,6 @@ public class InputManagerService extends IInputManager.Stub != mCurrentDisplayProperties.mousePointerAccelerationEnabled) { mCurrentDisplayProperties.mousePointerAccelerationEnabled = properties.mousePointerAccelerationEnabled; - mNative.setMousePointerAccelerationEnabled(properties.mousePointerAccelerationEnabled); } } @@ -3509,7 +3509,16 @@ public class InputManagerService extends IInputManager.Stub properties = new AdditionalDisplayInputProperties(); mAdditionalDisplayInputProperties.put(displayId, properties); } + final boolean oldPointerIconVisible = properties.pointerIconVisible; + final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled; updater.accept(properties); + if (oldPointerIconVisible != properties.pointerIconVisible) { + mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible); + } + if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) { + mNative.setMousePointerAccelerationEnabled(displayId, + properties.mousePointerAccelerationEnabled); + } if (properties.allDefaults()) { mAdditionalDisplayInputProperties.remove(displayId); } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index c3ef80f3624a..6f5202029998 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -119,7 +119,7 @@ interface NativeInputManagerService { void setPointerSpeed(int speed); - void setMousePointerAccelerationEnabled(boolean enabled); + void setMousePointerAccelerationEnabled(int displayId, boolean enabled); void setTouchpadPointerSpeed(int speed); @@ -190,6 +190,8 @@ interface NativeInputManagerService { boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId, @NonNull IBinder inputToken); + void setPointerIconVisibility(int displayId, boolean visible); + void requestPointerCapture(IBinder windowToken, boolean enabled); boolean canDispatchToDisplay(int deviceId, int displayId); @@ -352,7 +354,7 @@ interface NativeInputManagerService { public native void setPointerSpeed(int speed); @Override - public native void setMousePointerAccelerationEnabled(boolean enabled); + public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled); @Override public native void setTouchpadPointerSpeed(int speed); @@ -452,6 +454,9 @@ interface NativeInputManagerService { int pointerId, IBinder inputToken); @Override + public native void setPointerIconVisibility(int displayId, boolean visible); + + @Override public native void requestPointerCapture(IBinder windowToken, boolean enabled); @Override diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java index c76ca2beda96..fba71fd0ff9e 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java @@ -114,7 +114,7 @@ final class AdditionalSubtypeUtils { * @param userId The user ID to be associated with. */ static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); if (allSubtypes.isEmpty()) { @@ -143,7 +143,7 @@ final class AdditionalSubtypeUtils { @VisibleForTesting static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, AtomicFile subtypesFile) { + InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; FileOutputStream fos = null; diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java index 4b85d099b600..62adb25954b4 100644 --- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java +++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java @@ -20,7 +20,6 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.util.ArrayMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -44,24 +43,23 @@ final class HardwareKeyboardShortcutController { return mUserId; } - HardwareKeyboardShortcutController( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) { mUserId = userId; reset(methodMap); } @GuardedBy("ImfLock.class") - void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) { + void reset(@NonNull InputMethodMap methodMap) { mSubtypeHandles.clear(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId); - final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodListLocked(); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId); + final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList(); for (int i = 0; i < inputMethods.size(); ++i) { final InputMethodInfo imi = inputMethods.get(i); if (!imi.shouldShowInInputMethodPicker()) { continue; } final List<InputMethodSubtype> subtypes = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); if (subtypes.isEmpty()) { mSubtypeHandles.add(InputMethodSubtypeHandle.of(imi, null)); } else { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java index 542165d06a92..6339686629f5 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -204,7 +203,7 @@ final class InputMethodInfoUtils { */ @Nullable static InputMethodInfo chooseSystemVoiceIme( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull InputMethodMap methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId) { if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 8448fc233ae2..beb68d3566c3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -314,10 +314,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable private VirtualDeviceManagerInternal mVdmInternal = null; - // All known input methods. - final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); - private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>(); - // Mapping from deviceId to the device-specific imeId for that device. @GuardedBy("ImfLock.class") private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>(); @@ -330,7 +326,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController; /** - * Tracks how many times {@link #mMethodMap} was updated. + * Tracks how many times {@link #mSettings} was updated. */ @GuardedBy("ImfLock.class") private int mMethodMapUpdateCount = 0; @@ -472,7 +468,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) { - return mMethodMap.get(imeId); + return mSettings.getMethodMap().get(imeId); } /** @@ -1265,10 +1261,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); if (imi.getId().equals(curInputMethodId)) { for (String pkg : packages) { if (imi.getPackageName().equals(pkg)) { @@ -1339,7 +1336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { boolean changed = false; - for (InputMethodInfo imi : mMethodList) { + for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { mAdditionalSubtypeMap.remove(imi.getId()); changed = true; @@ -1347,7 +1344,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (changed) { AdditionalSubtypeUtils.save( - mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId()); + mAdditionalSubtypeMap, mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mChangedPackages.add(packageName); } } @@ -1405,10 +1403,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodInfo curIm = null; String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); final String imiId = imi.getId(); if (imiId.equals(curInputMethodId)) { curIm = imi; @@ -1426,7 +1425,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, - mMethodMap, + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } } @@ -1584,7 +1583,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (userId != currentUserId) { return; } - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); if (mSystemReady) { // We need to rebuild IMEs. buildInputMethodListLocked(false /* resetDefaultEnabledIme */); @@ -1658,14 +1657,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mLastSwitchUserId = userId; // mSettings should be created before buildInputMethodListLocked - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); mSwitchingController = - InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap, - userId); + InputMethodSubtypeSwitchingController.createInstanceLocked(context, + mSettings.getMethodMap(), userId); mHardwareKeyboardShortcutController = - new HardwareKeyboardShortcutController(mMethodMap, userId); + new HardwareKeyboardShortcutController(mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mMenuController = new InputMethodMenuController(this); mBindingController = bindingControllerForTesting != null @@ -1723,11 +1723,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = getSelectedMethodIdLocked(); - if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) { + if (selectedMethodId != null + && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( - context, mSettings.getEnabledInputMethodListLocked()); + context, mSettings.getEnabledInputMethodList()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); return; @@ -1791,7 +1792,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); - mSettings = new InputMethodSettings(mMethodMap, newUserId); + mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); @@ -1822,7 +1823,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (initialUserSwitch) { InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, newUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } if (DEBUG) { @@ -1899,7 +1900,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateFromSettingsLocked(true); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } } } @@ -2001,9 +2002,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. //TODO(b/210039666): use cache. - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod()); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get( + settings.getSelectedInputMethod()); return imi != null && imi.supportsStylusHandwriting(); } } @@ -2023,23 +2024,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { - final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId() && directBootAwareness == DirectBootAwareness.AUTO) { - // Create a copy. - methodList = new ArrayList<>(mMethodList); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, directBootAwareness); - settings = new InputMethodSettings(methodMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + directBootAwareness); } + // Create a copy. + final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList()); // filter caller's access to input methods methodList.removeIf(imi -> !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)); @@ -2052,12 +2049,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId()) { - methodList = mSettings.getEnabledInputMethodListLocked(); + methodList = mSettings.getEnabledInputMethodList(); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - settings = new InputMethodSettings(methodMap, userId); - methodList = settings.getEnabledInputMethodListLocked(); + settings = queryMethodMapForUser(userId); + methodList = settings.getEnabledInputMethodList(); } // filter caller's access to input methods methodList.removeIf(imi -> @@ -2120,27 +2116,26 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi; String selectedMethodId = getSelectedMethodIdLocked(); if (imiId == null && selectedMethodId != null) { - imi = mMethodMap.get(selectedMethodId); + imi = mSettings.getMethodMap().get(selectedMethodId); } else { - imi = mMethodMap.get(imiId); + imi = mSettings.getMethodMap().get(imiId); } if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { return Collections.emptyList(); } - return mSettings.getEnabledInputMethodSubtypeListLocked( + return mSettings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodInfo imi = methodMap.get(imiId); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get(imiId); if (imi == null) { return Collections.emptyList(); } - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) { return Collections.emptyList(); } - return settings.getEnabledInputMethodSubtypeListLocked( + return settings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } @@ -2343,7 +2338,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } String curId = getCurIdLocked(); - final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId); + final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId); final boolean suppressesSpellChecker = curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker(); final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions = @@ -2559,7 +2554,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId); if (Objects.equals(deviceMethodId, currentMethodId)) { return currentMethodId; - } else if (!mMethodMap.containsKey(deviceMethodId)) { + } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) { if (DEBUG) { Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme + " because its custom input method is not available: " + deviceMethodId); @@ -2601,7 +2596,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) { return false; } - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null) { return false; } @@ -3023,7 +3018,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked( + List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter( InputMethodInfo::shouldShowInInputMethodPicker); final int numImes = imes.size(); if (numImes > 2) return true; @@ -3035,7 +3030,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = imes.get(i); final List<InputMethodSubtype> subtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); final int subtypeCount = subtypes.size(); if (subtypeCount == 0) { ++nonAuxCount; @@ -3187,7 +3182,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); - List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); for (int i = 0; i < enabled.size(); i++) { // We allow the user to select "disabled until used" apps, so if they // are enabling one of those here we now need to make it enabled. @@ -3234,17 +3229,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO: Instantiate mSwitchingController for each user. if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } sendOnNavButtonFlagsChangedLocked(); } @@ -3268,7 +3263,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void setInputMethodLocked(String id, int subtypeId, int deviceId) { - InputMethodInfo info = mMethodMap.get(id); + InputMethodInfo info = mSettings.getMethodMap().get(id); if (info == null) { throw getExceptionForUnknownImeId(id); } @@ -4017,7 +4012,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4035,7 +4030,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4055,10 +4050,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return false; } - final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked(); + final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype(); final InputMethodInfo lastImi; if (lastIme != null) { - lastImi = mMethodMap.get(lastIme.first); + lastImi = mSettings.getMethodMap().get(lastIme.first); } else { lastImi = null; } @@ -4082,7 +4077,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // This is a safety net. If the currentSubtype can't be added to the history // and the framework couldn't find the last ime, we will make the last ime be // the most applicable enabled keyboard subtype of the system imes. - final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); if (enabled != null) { final int enabledCount = enabled.size(); final String locale; @@ -4097,7 +4092,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi = enabled.get(i); if (imi.getSubtypeCount() > 0 && imi.isSystem()) { InputMethodSubtype keyboardSubtype = - SubtypeUtils.findLastResortApplicableSubtypeLocked( + SubtypeUtils.findLastResortApplicableSubtype( SubtypeUtils.getSubtypes(imi), SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { @@ -4139,7 +4134,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) { final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype); + onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()), + mCurrentSubtype); if (nextSubtype == null) { return false; } @@ -4155,8 +4151,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()), - mCurrentSubtype); + false /* onlyCurrentIme */, + mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); return nextSubtype != null; } } @@ -4169,12 +4165,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { if (mSettings.getCurrentUserId() == userId) { - return mSettings.getLastInputMethodSubtypeLocked(); + return mSettings.getLastInputMethodSubtype(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return settings.getLastInputMethodSubtypeLocked(); + final InputMethodSettings settings = queryMethodMapForUser(userId); + return settings.getLastInputMethodSubtype(); } } @@ -4218,14 +4213,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); } @@ -4253,8 +4245,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { final boolean currentUser = (mSettings.getCurrentUserId() == userId); final InputMethodSettings settings = currentUser - ? mSettings - : new InputMethodSettings(queryMethodMapForUser(userId), userId); + ? mSettings : queryMethodMapForUser(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { return; } @@ -4626,7 +4617,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) { return; } - final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo imi = + mSettings.getMethodMap().get(getSelectedMethodIdLocked()); if (imi != null) { mSwitchingController.onUserActionLocked(imi, mCurrentSubtype); } @@ -4684,8 +4676,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } else { // Called with current IME's token. - if (mMethodMap.get(id) != null - && mSettings.getEnabledInputMethodListWithFilterLocked( + if (mSettings.getMethodMap().get(id) != null + && mSettings.getEnabledInputMethodListWithFilter( (info) -> info.getId().equals(id)).isEmpty()) { throw new IllegalStateException("Requested IME is not enabled: " + id); } @@ -4866,7 +4858,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( showAuxSubtypes, isScreenLocked, true /* forImeMenu */, - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, lastInputMethodId, lastInputMethodSubtypeId, imList); } @@ -5050,7 +5043,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); if (imi != null) { if (DEBUG) { Slog.d(TAG, "New default IME was selected: " + imi.getId()); @@ -5062,17 +5055,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - static void queryInputMethodServicesInternal(Context context, + @NonNull + static InputMethodSettings queryInputMethodServicesInternal(Context context, @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); - methodList.clear(); - methodMap.clear(); - final int directBootAwarenessFlags; switch (directBootAwareness) { case DirectBootAwareness.ANY: @@ -5095,24 +5085,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub new Intent(InputMethod.SERVICE_INTERFACE), PackageManager.ResolveInfoFlags.of(flags)); - methodList.ensureCapacity(services.size()); - methodMap.ensureCapacity(services.size()); - // Note: This is a temporary solution for Bug 261723412. If there is any better solution, // we should remove this data dependency. final List<String> enabledInputMethodList = InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId); - filterInputMethodServices(additionalSubtypeMap, methodMap, methodList, - enabledInputMethodList, userAwareContext, services); + final InputMethodMap methodMap = filterInputMethodServices( + additionalSubtypeMap, enabledInputMethodList, userAwareContext, services); + return InputMethodSettings.create(methodMap, userId); } - static void filterInputMethodServices( + @NonNull + static InputMethodMap filterInputMethodServices( ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(services.size()); for (int i = 0; i < services.size(); ++i) { ResolveInfo ri = services.get(i); @@ -5141,7 +5130,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imiPackageCount.put(packageName, 1 + imiPackageCount.getOrDefault(packageName, 0)); - methodList.add(imi); methodMap.put(imi.getId(), imi); if (DEBUG) { Slog.d(TAG, "Found an input method " + imi); @@ -5153,6 +5141,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.wtf(TAG, "Unable to load input method " + imeId, e); } } + return InputMethodMap.of(methodMap); } @GuardedBy("ImfLock.class") @@ -5168,8 +5157,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), - mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO); + mSettings = queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), + mAdditionalSubtypeMap, DirectBootAwareness.AUTO); // Construct the set of possible IME packages for onPackageChanged() to avoid false // negatives when the package state remains to be the same but only the component state is @@ -5196,11 +5185,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!resetDefaultEnabledIme) { boolean enabledImeFound = false; boolean enabledNonAuxImeFound = false; - final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList(); final int numImes = enabledImes.size(); for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = enabledImes.get(i); - if (mMethodList.contains(imi)) { + if (mSettings.getMethodMap().containsKey(imi.getId())) { enabledImeFound = true; if (!imi.isAuxiliaryIme()) { enabledNonAuxImeFound = true; @@ -5224,7 +5213,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList, + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(), reenableMinimumNonAuxSystemImes); final int numImes = defaultEnabledIme.size(); for (int i = 0; i < numImes; ++i) { @@ -5238,7 +5227,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String defaultImiId = mSettings.getSelectedInputMethod(); if (!TextUtils.isEmpty(defaultImiId)) { - if (!mMethodMap.containsKey(defaultImiId)) { + if (!mSettings.getMethodMap().containsKey(defaultImiId)) { Slog.w(TAG, "Default IME is uninstalled. Choose new default IME."); if (chooseNewDefaultIMELocked()) { updateInputMethodsFromSettingsLocked(true); @@ -5253,23 +5242,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO: Instantiate mSwitchingController for each user. if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } sendOnNavButtonFlagsChangedLocked(); // Notify InputMethodListListeners of the new installed InputMethods. - final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList); + final List<InputMethodInfo> inputMethodList = mSettings.getMethodList(); mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED, mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } @@ -5290,7 +5279,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( - mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); + mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked," @@ -5341,9 +5330,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } else { final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings - .getEnabledInputMethodsAndSubtypeListLocked(); + .getEnabledInputMethodsAndSubtypeList(); StringBuilder builder = new StringBuilder(); - if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId( builder, enabledInputMethodsList, id)) { if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { // Disabled input method is currently selected, switch to another one. @@ -5357,7 +5346,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // new default one but only update the settings. InputMethodInfo newDefaultIme = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); mSettings.putSelectedDefaultDeviceInputMethod( newDefaultIme == null ? "" : newDefaultIme.getId()); } @@ -5402,11 +5391,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) { - InputMethodInfo imi = mMethodMap.get(newDefaultIme); + InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme); int lastSubtypeId = NOT_A_SUBTYPE_ID; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { - String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); + String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, @@ -5437,8 +5426,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return getCurrentInputMethodSubtypeLocked(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); return settings.getCurrentInputMethodSubtypeForNonCurrentUsers(); } } @@ -5460,7 +5448,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return null; } final boolean subtypeIsSelected = mSettings.isSubtypeSelected(); - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null || imi.getSubtypeCount() == 0) { return null; } @@ -5472,7 +5460,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the most applicable subtype from explicitly or implicitly enabled // subtypes. List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, // just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { @@ -5480,11 +5468,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()) .get(0).toString(); - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (mCurrentSubtype == null) { - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } } @@ -5501,46 +5489,42 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { + final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId()) { - return mMethodMap.get(mSettings.getSelectedInputMethod()); + settings = mSettings; + } else { + final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = + new ArrayMap<>(); + AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); } - - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return methodMap.get(settings.getSelectedInputMethod()); + return settings.getMethodMap().get(settings.getSelectedInputMethod()); } - private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); + private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - return methodMap; + return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO); } @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId) - || !mSettings.getEnabledInputMethodListLocked() - .contains(mMethodMap.get(imeId))) { + if (!mSettings.getMethodMap().containsKey(imeId) + || !mSettings.getEnabledInputMethodList() + .contains(mSettings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId) - || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId) + || !settings.getEnabledInputMethodList().contains( + settings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); @@ -5578,7 +5562,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void switchKeyboardLayoutLocked(int direction) { - final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo currentImi = mSettings.getMethodMap().get( + getSelectedMethodIdLocked()); if (currentImi == null) { return; } @@ -5590,7 +5575,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (nextSubtypeHandle == null) { return; } - final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId()); + final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId()); if (nextImi == null) { return; } @@ -5670,15 +5655,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) { synchronized (ImfLock.class) { if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId)) { + if (!mSettings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } setInputMethodEnabledLocked(imeId, enabled); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId)) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } if (enabled) { @@ -5689,9 +5673,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub settings.putEnabledInputMethodsStr(newEnabledImeIdsStr); } } else { - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } return true; } @@ -5997,10 +5981,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { p.println("Current Input Method Manager state:"); - int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + int numImes = methodList.size(); p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount); for (int i = 0; i < numImes; i++) { - InputMethodInfo info = mMethodList.get(i); + InputMethodInfo info = methodList.get(i); p.println(" InputMethod #" + i + ":"); info.dump(p, " "); } @@ -6047,7 +6032,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" mSwitchingController:"); mSwitchingController.dump(p); p.println(" mSettings:"); - mSettings.dumpLocked(p, " "); + mSettings.dump(p, " "); p.println(" mStartInputHistory:"); mStartInputHistory.dump(pw, " "); @@ -6416,16 +6401,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub boolean failedToEnableUnknownIme = false; boolean previouslyEnabled = false; if (userId == mSettings.getCurrentUserId()) { - if (enabled && !mMethodMap.containsKey(imeId)) { + if (enabled && !mSettings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled); } } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); if (enabled) { - if (!methodMap.containsKey(imeId)) { + if (!settings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { final String enabledImeIdsStr = settings.getEnabledInputMethodsStr(); @@ -6438,9 +6422,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } else { previouslyEnabled = - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } } if (failedToEnableUnknownIme) { @@ -6537,9 +6521,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mBindingController.unbindCurrentMethod(); // Enable default IMEs, disable others - var toDisable = mSettings.getEnabledInputMethodListLocked(); + var toDisable = mSettings.getEnabledInputMethodList(); var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes( - mContext, mMethodList); + mContext, mSettings.getMethodList()); toDisable.removeAll(defaultEnabled); for (InputMethodInfo info : toDisable) { setInputMethodEnabledLocked(info.getId(), false); @@ -6554,22 +6538,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, mSettings.getCurrentUserId()), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); nextIme = mSettings.getSelectedInputMethod(); - nextEnabledImes = mSettings.getEnabledInputMethodListLocked(); + nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings( - methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, - methodList); + settings.getMethodList()); nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( nextEnabledImes).getId(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java new file mode 100644 index 000000000000..a8e5e2ef4f72 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java @@ -0,0 +1,78 @@ +/* + * 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.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.view.inputmethod.InputMethodInfo; + +import java.util.List; + +/** + * A map from IME ID to {@link InputMethodInfo}, which is guaranteed to be immutable thus + * thread-safe. + */ +final class InputMethodMap { + private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP = + new ArrayMap<>(); + + private final ArrayMap<String, InputMethodInfo> mMap; + + static InputMethodMap emptyMap() { + return new InputMethodMap(EMPTY_MAP); + } + + static InputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) { + return new InputMethodMap(map); + } + + private InputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) { + mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map); + } + + @AnyThread + @Nullable + InputMethodInfo get(@Nullable String imeId) { + return mMap.get(imeId); + } + + @AnyThread + @NonNull + List<InputMethodInfo> values() { + return List.copyOf(mMap.values()); + } + + @AnyThread + @Nullable + InputMethodInfo valueAt(int index) { + return mMap.valueAt(index); + } + + @AnyThread + boolean containsKey(@Nullable String imeId) { + return mMap.containsKey(imeId); + } + + @AnyThread + @IntRange(from = 0) + int size() { + return mMap.size(); + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index 9ddb428018c0..abd7688b60cc 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -16,6 +16,7 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -58,7 +59,9 @@ final class InputMethodSettings { private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR; - private final ArrayMap<String, InputMethodInfo> mMethodMap; + private final InputMethodMap mMethodMap; + private final List<InputMethodInfo> mMethodList; + @UserIdInt private final int mCurrentUserId; @@ -73,8 +76,17 @@ final class InputMethodSettings { } } - InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + static InputMethodSettings createEmptyMap(@UserIdInt int userId) { + return new InputMethodSettings(InputMethodMap.emptyMap(), userId); + } + + static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) { + return new InputMethodSettings(methodMap, userId); + } + + private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) { mMethodMap = methodMap; + mMethodList = methodMap.values(); mCurrentUserId = userId; String ime = getSelectedInputMethod(); String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); @@ -84,6 +96,18 @@ final class InputMethodSettings { } } + @AnyThread + @NonNull + InputMethodMap getMethodMap() { + return mMethodMap; + } + + @AnyThread + @NonNull + List<InputMethodInfo> getMethodList() { + return mMethodList; + } + private void putString(@NonNull String key, @Nullable String str) { SecureSettingsWrapper.putString(key, str, mCurrentUserId); } @@ -101,32 +125,32 @@ final class InputMethodSettings { return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); } - ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() { - return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */); + ArrayList<InputMethodInfo> getEnabledInputMethodList() { + return getEnabledInputMethodListWithFilter(null /* matchingCondition */); } @NonNull - ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked( + ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter( @Nullable Predicate<InputMethodInfo> matchingCondition) { - return createEnabledInputMethodListLocked( - getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition); + return createEnabledInputMethodList( + getEnabledInputMethodsAndSubtypeList(), matchingCondition); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + List<InputMethodSubtype> getEnabledInputMethodSubtypeList( InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { List<InputMethodSubtype> enabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi); + getEnabledInputMethodSubtypeList(imi); if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes( SystemLocaleWrapper.get(mCurrentUserId), imi); } return InputMethodSubtype.sort(imi, enabledSubtypes); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) { - final List<Pair<String, ArrayList<String>>> imsList = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); + List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) { + List<Pair<String, ArrayList<String>>> imsList = + getEnabledInputMethodsAndSubtypeList(); + ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); if (imi != null) { for (int i = 0; i < imsList.size(); ++i) { final Pair<String, ArrayList<String>> imsPair = imsList.get(i); @@ -149,7 +173,7 @@ final class InputMethodSettings { return enabledSubtypes; } - List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { + List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() { final String enabledInputMethodsStr = getEnabledInputMethodsStr(); final TextUtils.SimpleStringSplitter inputMethodSplitter = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); @@ -181,7 +205,7 @@ final class InputMethodSettings { * * @return the specified id was removed or not. */ - boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( + boolean buildAndPutEnabledInputMethodsStrRemovingId( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { boolean isRemoved = false; boolean needsAppendSeparator = false; @@ -209,7 +233,7 @@ final class InputMethodSettings { return isRemoved; } - private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked( + private ArrayList<InputMethodInfo> createEnabledInputMethodList( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition) { final ArrayList<InputMethodInfo> res = new ArrayList<>(); @@ -271,7 +295,7 @@ final class InputMethodSettings { } private void addSubtypeToHistory(String imeId, String subtypeId) { - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> ime = subtypeHistory.get(i); if (ime.first.equals(imeId)) { @@ -303,14 +327,14 @@ final class InputMethodSettings { } } - Pair<String, String> getLastInputMethodAndSubtypeLocked() { + Pair<String, String> getLastInputMethodAndSubtype() { // Gets the first one from the history - return getLastSubtypeForInputMethodLockedInternal(null); + return getLastSubtypeForInputMethodInternal(null); } @Nullable - InputMethodSubtype getLastInputMethodSubtypeLocked() { - final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked(); + InputMethodSubtype getLastInputMethodSubtype() { + final Pair<String, String> lastIme = getLastInputMethodAndSubtype(); // TODO: Handle the case of the last IME with no subtypes if (lastIme == null || TextUtils.isEmpty(lastIme.first) || TextUtils.isEmpty(lastIme.second)) { @@ -331,8 +355,8 @@ final class InputMethodSettings { } } - String getLastSubtypeForInputMethodLocked(String imeId) { - Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); + String getLastSubtypeForInputMethod(String imeId) { + Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId); if (ime != null) { return ime.second; } else { @@ -340,10 +364,10 @@ final class InputMethodSettings { } } - private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { + private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) { final List<Pair<String, ArrayList<String>>> enabledImes = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + getEnabledInputMethodsAndSubtypeList(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> imeAndSubtype = subtypeHistory.get(i); final String imeInTheHistory = imeAndSubtype.first; @@ -351,7 +375,7 @@ final class InputMethodSettings { if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { final String subtypeInTheHistory = imeAndSubtype.second; final String subtypeHashCode = - getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( + getEnabledSubtypeHashCodeForInputMethodAndSubtype( enabledImes, imeInTheHistory, subtypeInTheHistory); if (!TextUtils.isEmpty(subtypeHashCode)) { if (DEBUG) { @@ -368,7 +392,7 @@ final class InputMethodSettings { return null; } - private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, + private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); for (int i = 0; i < enabledImes.size(); ++i) { @@ -383,7 +407,7 @@ final class InputMethodSettings { // are enabled implicitly, so needs to treat them to be enabled. if (imi != null && imi.getSubtypeCount() > 0) { List<InputMethodSubtype> implicitlyEnabledSubtypes = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList, + SubtypeUtils.getImplicitlyApplicableSubtypes(localeList, imi); final int numSubtypes = implicitlyEnabledSubtypes.size(); for (int j = 0; j < numSubtypes; ++j) { @@ -420,7 +444,7 @@ final class InputMethodSettings { return null; } - private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { + private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() { ArrayList<Pair<String, String>> imsList = new ArrayList<>(); final String subtypeHistoryStr = getSubtypeHistoryStr(); if (TextUtils.isEmpty(subtypeHistoryStr)) { @@ -583,7 +607,7 @@ final class InputMethodSettings { // If there are no selected subtypes, the framework will try to find the most applicable // subtype from explicitly or implicitly enabled subtypes. final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi, true); + getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { return null; @@ -592,13 +616,13 @@ final class InputMethodSettings { return explicitlyOrImplicitlyEnabledSubtypes.get(0); } final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); - final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (subtype != null) { return subtype; } - return SubtypeUtils.findLastResortApplicableSubtypeLocked( + return SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } @@ -690,7 +714,7 @@ final class InputMethodSettings { return sb.toString(); } - void dumpLocked(final Printer pw, final String prefix) { + void dump(final Printer pw, final String prefix) { pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 834ba20b84fd..1379d166e805 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -23,7 +23,6 @@ import android.annotation.UserIdInt; import android.content.Context; import android.os.UserHandle; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.Printer; import android.util.Slog; @@ -158,15 +157,15 @@ final class InputMethodSubtypeSwitchingController { static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu, - @NonNull Context context, @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull Context context, @NonNull InputMethodMap methodMap, @UserIdInt int userId) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId); - final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked(); + final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList(); if (imis.isEmpty()) { return new ArrayList<>(); } @@ -184,7 +183,7 @@ final class InputMethodSubtypeSwitchingController { continue; } final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); final ArraySet<String> enabledSubtypeSet = new ArraySet<>(); for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); @@ -479,7 +478,7 @@ final class InputMethodSubtypeSwitchingController { private ControllerImpl mController; private InputMethodSubtypeSwitchingController(@NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { mContext = context; mUserId = userId; mController = ControllerImpl.createFrom(null, @@ -491,7 +490,7 @@ final class InputMethodSubtypeSwitchingController { @NonNull public static InputMethodSubtypeSwitchingController createInstanceLocked( @NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { return new InputMethodSubtypeSwitchingController(context, methodMap, userId); } @@ -511,8 +510,7 @@ final class InputMethodSubtypeSwitchingController { mController.onUserActionLocked(imi, subtype); } - public void resetCircularListLocked( - @NonNull ArrayMap<String, InputMethodInfo> methodMap) { + public void resetCircularListLocked(@NonNull InputMethodMap methodMap) { mController = ControllerImpl.createFrom(mController, getSortedInputMethodAndSubtypeList( false /* includeAuxiliarySubtypes */, false /* isScreenLocked */, diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java index 95df99855dcf..3d5c867768ac 100644 --- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -26,7 +26,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; @@ -52,7 +51,7 @@ final class SubtypeUtils { "EnabledWhenDefaultIsNotAsciiCapable"; // A temporary workaround for the performance concerns in - // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). + // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo). // TODO: Optimize all the critical paths including this one. // TODO(b/235661780): Make the cache supports multi-users. private static final Object sCacheLock = new Object(); @@ -121,9 +120,8 @@ final class SubtypeUtils { private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = source -> source != null ? source.getLocaleObject() : null; - @VisibleForTesting @NonNull - static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes( @NonNull LocaleList systemLocales, InputMethodInfo imi) { synchronized (sCacheLock) { // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because @@ -133,11 +131,11 @@ final class SubtypeUtils { } } - // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). - // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive + // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl(). + // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive // LocaleList rather than Resource. final ArrayList<InputMethodSubtype> result = - getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi); + getImplicitlyApplicableSubtypesImpl(systemLocales, imi); synchronized (sCacheLock) { // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. sCachedSystemLocales = systemLocales; @@ -147,7 +145,7 @@ final class SubtypeUtils { return result; } - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl( @NonNull LocaleList systemLocales, InputMethodInfo imi) { final List<InputMethodSubtype> subtypes = getSubtypes(imi); final String systemLocale = systemLocales.get(0).toString(); @@ -215,7 +213,7 @@ final class SubtypeUtils { } if (applicableSubtypes.isEmpty()) { - InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype( subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); if (lastResortKeyboardSubtype != null) { applicableSubtypes.add(lastResortKeyboardSubtype); @@ -244,7 +242,7 @@ final class SubtypeUtils { * * @return the most applicable subtypeId */ - static InputMethodSubtype findLastResortApplicableSubtypeLocked( + static InputMethodSubtype findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, boolean canIgnoreLocaleAsLastResort) { if (subtypes == null || subtypes.isEmpty()) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 308d441fb871..425659e38492 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -282,6 +282,7 @@ import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeProto; +import android.service.notification.ZenPolicy; import android.telecom.TelecomManager; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; @@ -5968,6 +5969,20 @@ public class NotificationManagerService extends SystemService { } } + /** + * Gets the device-default zen policy as a ZenPolicy. + */ + @Override + public ZenPolicy getDefaultZenPolicy() { + enforceSystemOrSystemUI("INotificationManager.getDefaultZenPolicy"); + final long identity = Binder.clearCallingIdentity(); + try { + return mZenModeHelper.getDefaultZenPolicy(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public List<String> getEnabledNotificationListenerPackages() { checkCallerIsSystem(); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 153af13b61b4..c067fa068b12 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -604,6 +604,14 @@ public class ZenModeHelper { ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (rule == null) { rule = newImplicitZenRule(callingPkg); + + // For new implicit rules, create a policy matching the current global + // (manual rule) settings, for consistency with the policy that + // would apply if changing the global interruption filter. We only do this + // for newly created rules, as existing rules have a pre-existing policy + // (whether initialized here or set via app or user). + rule.zenPolicy = mConfig.toZenPolicy(); + newConfig.automaticRules.put(rule.id, rule); } // If the user has changed the rule's *zenMode*, then don't let app overwrite it. @@ -615,6 +623,7 @@ public class ZenModeHelper { rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), Condition.STATE_TRUE); + setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, "applyGlobalZenModeAsImplicitZenRule", callingUid); } @@ -643,8 +652,10 @@ public class ZenModeHelper { return; } ZenModeConfig newConfig = mConfig.copy(); + boolean isNew = false; ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (rule == null) { + isNew = true; rule = newImplicitZenRule(callingPkg); rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; newConfig.automaticRules.put(rule.id, rule); @@ -652,10 +663,20 @@ public class ZenModeHelper { // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it. // We allow the update if the user has only changed other aspects of the rule. if (rule.zenPolicyUserModifiedFields == 0) { + ZenPolicy newZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); + if (isNew) { + // For new rules only, fill anything underspecified in the new policy with + // values from the global configuration, for consistency with the policy that + // would take effect if changing the global policy. + // Note that NotificationManager.Policy cannot have any unset priority + // categories, but *can* have unset visual effects, which is why we do this. + newZenPolicy = mConfig.toZenPolicy().overwrittenWith(newZenPolicy); + } updatePolicy( rule, - ZenAdapters.notificationPolicyToZenPolicy(policy), - /* updateBitmask= */ false); + newZenPolicy, + /* updateBitmask= */ false, + isNew); setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, "applyGlobalPolicyAsImplicitZenRule", callingUid); @@ -685,6 +706,11 @@ public class ZenModeHelper { } ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg)); if (implicitRule != null && implicitRule.zenPolicy != null) { + // toNotificationPolicy takes defaults from mConfig, and technically, those are not + // the defaults that would apply if any fields were unset. However, all rules should + // have all fields set in their ZenPolicy objects upon rule creation, so in + // practice, this is only filling in the areChannelsBypassingDnd field, which is a + // state rather than a part of the policy. return mConfig.toNotificationPolicy(implicitRule.zenPolicy); } else { return getNotificationPolicy(); @@ -1072,7 +1098,8 @@ public class ZenModeHelper { rule.zenMode = newZenMode; // Updates the bitmask and values for all policy fields, based on the origin. - updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask); + updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask, isNew); + // Updates the bitmask and values for all device effect fields, based on the origin. updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(), origin == UPDATE_ORIGIN_APP, updateBitmask); @@ -1111,14 +1138,19 @@ public class ZenModeHelper { /** * Modifies the {@link ZenPolicy} associated to a new or updated ZenRule. * - * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect - * the changes being applied (if applicable, i.e. if the update is from the user). + * <p>The update takes any set fields in {@code newPolicy} as new policy settings for the + * provided {@code ZenRule}, keeping any pre-existing settings from {@code zenRule.zenPolicy} + * for any unset policy fields in {@code newPolicy}. The user-modified bitmask is updated to + * reflect the changes being applied (if applicable, i.e. if the update is from the user). */ private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, - boolean updateBitmask) { + boolean updateBitmask, boolean isNew) { if (newPolicy == null) { - // TODO: b/319242206 - Treat as newPolicy == default policy and continue below. - zenRule.zenPolicy = null; + if (isNew) { + // Newly created rule with no provided policy; fill in with the default. + zenRule.zenPolicy = mDefaultConfig.toZenPolicy(); + } + // Otherwise, a null policy means no policy changes, so we can stop here. return; } @@ -1127,6 +1159,16 @@ public class ZenModeHelper { ZenPolicy oldPolicy = zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy(); + // If this is updating a rule rather than creating a new one, keep any fields from the + // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy + // has been set to the default settings above, so any unspecified fields in a newly created + // policy are filled with default values. Then use the fully-specified version of the new + // policy for comparison below. + // + // Although we do not expect a policy update from the user to contain any unset fields, + // filling in fields here also guards against any unset fields counting as a "diff" when + // comparing fields for bitmask editing below. + newPolicy = oldPolicy.overwrittenWith(newPolicy); zenRule.zenPolicy = newPolicy; if (updateBitmask) { @@ -1452,11 +1494,27 @@ public class ZenModeHelper { } allRulesDisabled &= !automaticRule.enabled; + + // Upon upgrading to a version with modes_api enabled, keep all behaviors of + // rules with null ZenPolicies explicitly as a copy of the global policy. + if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) { + // Keep the manual ("global") policy that from config. + ZenPolicy manualRulePolicy = config.toZenPolicy(); + if (automaticRule.zenPolicy == null) { + automaticRule.zenPolicy = manualRulePolicy; + } else { + // newPolicy is a policy with all unset fields in the rule's zenPolicy + // set to their values from the values in config. Then convert that back + // to ZenPolicy to store with the automatic zen rule. + automaticRule.zenPolicy = + manualRulePolicy.overwrittenWith(automaticRule.zenPolicy); + } + } } } if (!hasDefaultRules && allRulesDisabled - && (forRestore || config.version < ZenModeConfig.XML_VERSION)) { + && (forRestore || config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE)) { // reset zen automatic rules to default on restore or upgrade if: // - doesn't already have default rules and // - all previous automatic rules were disabled @@ -1473,7 +1531,7 @@ public class ZenModeHelper { // Resolve user id for settings. userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId; - if (config.version < ZenModeConfig.XML_VERSION) { + if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId); } else { @@ -1600,6 +1658,14 @@ public class ZenModeHelper { return mConsolidatedPolicy.copy(); } + /** + * Returns a copy of the device default policy as a ZenPolicy object. + */ + @VisibleForTesting + protected ZenPolicy getDefaultZenPolicy() { + return mDefaultConfig.toZenPolicy(); + } + @GuardedBy("mConfigLock") private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent, @ConfigChangeOrigin int origin, String reason, int callingUid) { @@ -1783,7 +1849,7 @@ public class ZenModeHelper { } @GuardedBy("mConfigLock") - private void applyCustomPolicy(ZenPolicy policy, ZenRule rule) { + private void applyCustomPolicy(ZenPolicy policy, ZenRule rule, boolean useManualConfig) { if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) { policy.apply(new ZenPolicy.Builder() .disallowAllSounds() @@ -1797,8 +1863,22 @@ public class ZenModeHelper { } else if (rule.zenPolicy != null) { policy.apply(rule.zenPolicy); } else { - // active rule with no specified policy inherits the default settings - policy.apply(mConfig.toZenPolicy()); + if (Flags.modesApi()) { + if (useManualConfig) { + // manual rule is configured using the settings stored directly in mConfig + policy.apply(mConfig.toZenPolicy()); + } else { + // under modes_api flag, an active automatic rule with no specified policy + // inherits the device default settings as stored in mDefaultConfig. While the + // rule's policy fields should be set upon creation, this is a fallback to + // catch any that may have fallen through the cracks. + Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); + policy.apply(mDefaultConfig.toZenPolicy()); + } + } else { + // active rule with no specified policy inherits the global config settings + policy.apply(mConfig.toZenPolicy()); + } } } @@ -1810,7 +1890,7 @@ public class ZenModeHelper { ZenPolicy policy = new ZenPolicy(); ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder(); if (mConfig.manualRule != null) { - applyCustomPolicy(policy, mConfig.manualRule); + applyCustomPolicy(policy, mConfig.manualRule, true); if (Flags.modesApi()) { deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); } @@ -1822,7 +1902,7 @@ public class ZenModeHelper { // policy. This is relevant in case some other active rule has a more // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy! if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) { - applyCustomPolicy(policy, automaticRule); + applyCustomPolicy(policy, automaticRule, false); } if (Flags.modesApi()) { deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); @@ -1830,6 +1910,14 @@ public class ZenModeHelper { } } + // While mConfig.toNotificationPolicy fills in any unset fields from the provided + // config (which, in this case is the manual "global" config), under modes API changes, + // we should have no remaining unset fields: the manual policy gets every field from + // the global policy, and each automatic rule has all policy fields filled in on + // creation or update. + // However, the piece of information that comes from mConfig that we must keep is the + // areChannelsBypassingDnd bit, which is a state, rather than a policy, and even when + // all policy fields are set, this state comes to the new policy from this call. Policy newPolicy = mConfig.toNotificationPolicy(policy); if (!Objects.equals(mConsolidatedPolicy, newPolicy)) { mConsolidatedPolicy = newPolicy; diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 25a39cc8456f..86d05d92c95b 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null) { + if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index b9464d96a019..a61b03fdbb39 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -32,6 +32,7 @@ import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_RRO; import static android.os.Trace.traceBegin; import static android.os.Trace.traceEnd; + import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; import android.annotation.NonNull; @@ -362,7 +363,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[defaultPackages.size()]); + return defaultPackages.toArray(new String[0]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1143,9 +1144,10 @@ public final class OverlayManagerService extends SystemService { }; private static final class PackageManagerHelperImpl implements PackageManagerHelper { - private static class PackageStateUsers { + private static final class PackageStateUsers { private PackageState mPackageState; - private final Set<Integer> mInstalledUsers = new ArraySet<>(); + private Boolean mDefinesOverlayable = null; + private final ArraySet<Integer> mInstalledUsers = new ArraySet<>(); private PackageStateUsers(@NonNull PackageState packageState) { this.mPackageState = packageState; } @@ -1160,7 +1162,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final Set<Integer> mInitializedUsers = new ArraySet<>(); + private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1176,8 +1178,7 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (!mInitializedUsers.contains(userId)) { - mInitializedUsers.add(userId); + if (mInitializedUsers.add(userId)) { mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1196,13 +1197,11 @@ public final class OverlayManagerService extends SystemService { return userPackages; } - @Override - @Nullable - public PackageState getPackageStateForUser(@NonNull final String packageName, + private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName, final int userId) { final PackageStateUsers pkg = mCache.get(packageName); if (pkg != null && pkg.mInstalledUsers.contains(userId)) { - return pkg.mPackageState; + return pkg; } try { if (!mPackageManager.isPackageAvailable(packageName, userId)) { @@ -1216,8 +1215,14 @@ public final class OverlayManagerService extends SystemService { return addPackageUser(packageName, userId); } - @NonNull - private PackageState addPackageUser(@NonNull final String packageName, + @Override + public PackageState getPackageStateForUser(@NonNull final String packageName, + final int userId) { + final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId); + return pkg != null ? pkg.mPackageState : null; + } + + private PackageStateUsers addPackageUser(@NonNull final String packageName, final int user) { final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName); if (pkg == null) { @@ -1229,20 +1234,20 @@ public final class OverlayManagerService extends SystemService { } @NonNull - private PackageState addPackageUser(@NonNull final PackageState pkg, + private PackageStateUsers addPackageUser(@NonNull final PackageState pkg, final int user) { PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); if (pkgUsers == null) { pkgUsers = new PackageStateUsers(pkg); mCache.put(pkg.getPackageName(), pkgUsers); - } else { + } else if (pkgUsers.mPackageState != pkg) { pkgUsers.mPackageState = pkg; + pkgUsers.mDefinesOverlayable = null; } pkgUsers.mInstalledUsers.add(user); - return pkgUsers.mPackageState; + return pkgUsers; } - @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { final PackageStateUsers pkgUsers = mCache.get(packageName); @@ -1260,15 +1265,15 @@ public final class OverlayManagerService extends SystemService { } } - @Nullable public PackageState onPackageAdded(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } - @Nullable public PackageState onPackageUpdated(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } public void onPackageRemoved(@NonNull final String packageName, final int userId) { @@ -1308,22 +1313,30 @@ public final class OverlayManagerService extends SystemService { return (pkgs.length == 0) ? null : pkgs[0]; } - @Nullable @Override public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @NonNull String targetOverlayableName, int userId) throws IOException { - var packageState = getPackageStateForUser(packageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(packageName, userId); + final var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } + if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) { + return null; + } + ApkAssets apkAssets = null; try { apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - return apkAssets.getOverlayableInfo(targetOverlayableName); + if (psu.mDefinesOverlayable == null) { + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } + return Boolean.FALSE.equals(psu.mDefinesOverlayable) + ? null : apkAssets.getOverlayableInfo(targetOverlayableName); } finally { if (apkAssets != null) { try { @@ -1337,24 +1350,29 @@ public final class OverlayManagerService extends SystemService { @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws IOException { - var packageState = getPackageStateForUser(targetPackageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(targetPackageName, userId); + var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath()); - return apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { + if (psu.mDefinesOverlayable == null) { + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), + ApkAssets.PROPERTY_ONLY_OVERLAYABLES); + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } } } } + return psu.mDefinesOverlayable; } @Override @@ -1545,8 +1563,7 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final OverlayPaths.Builder list = new OverlayPaths.Builder(); - list.addAll(frameworkOverlays); + final var list = new OverlayPaths.Builder(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1558,17 +1575,21 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + if (DEBUG || !invalidPackages.isEmpty()) { + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, + "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", targetPackageName, - userId)); + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", + targetPackageName, + userId)); + } } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 972c78db9460..c1b6ccc7e25c 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,24 +772,20 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, - userId); - final OverlayPaths.Builder paths = new OverlayPaths.Builder(); - final int n = overlays.size(); - for (int i = 0; i < n; i++) { - final OverlayInfo oi = overlays.get(i); + final var paths = new OverlayPaths.Builder(); + mSettings.forEachMatching(userId, null, targetPackageName, oi -> { if (!oi.isEnabled()) { - continue; + return; } if (!includeImmutableOverlays && !oi.isMutable) { - continue; + return; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - } + }); return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index eae614ac9e77..b8b49f3eed2f 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,6 +47,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -182,6 +183,23 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } + void forEachMatching(int userId, String overlayName, String targetPackageName, + @NonNull Consumer<OverlayInfo> consumer) { + for (int i = 0, n = mItems.size(); i < n; i++) { + final SettingsItem item = mItems.get(i); + if (item.getUserId() != userId) { + continue; + } + if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { + continue; + } + if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { + continue; + } + consumer.accept(item.getOverlayInfo()); + } + } + ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index f3df4244c47f..cc4c2b5bf893 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -1031,12 +1031,12 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, private void recomputeComponentVisibility( ArrayMap<String, ? extends PackageStateInternal> existingSettings) { final WatchedArraySet<String> protectedBroadcasts; - final WatchedArraySet<Integer> forceQueryable; + final ArraySet<Integer> forceQueryable; synchronized (mProtectedBroadcastsLock) { - protectedBroadcasts = mProtectedBroadcasts.snapshot(); + protectedBroadcasts = new WatchedArraySet<String>(mProtectedBroadcasts); } synchronized (mForceQueryableLock) { - forceQueryable = mForceQueryable.snapshot(); + forceQueryable = new ArraySet<Integer>(mForceQueryable.untrackedStorage()); } final ParallelComputeComponentVisibility computer = new ParallelComputeComponentVisibility( existingSettings, forceQueryable, protectedBroadcasts); diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java index 200734b37269..a02a1bc13b17 100644 --- a/services/core/java/com/android/server/pm/AppsFilterUtils.java +++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java @@ -198,12 +198,12 @@ final class AppsFilterUtils { private static final int MAX_THREADS = 4; private final ArrayMap<String, ? extends PackageStateInternal> mExistingSettings; - private final WatchedArraySet<Integer> mForceQueryable; + private final ArraySet<Integer> mForceQueryable; private final WatchedArraySet<String> mProtectedBroadcasts; ParallelComputeComponentVisibility( @NonNull ArrayMap<String, ? extends PackageStateInternal> existingSettings, - @NonNull WatchedArraySet<Integer> forceQueryable, + @NonNull ArraySet<Integer> forceQueryable, @NonNull WatchedArraySet<String> protectedBroadcasts) { mExistingSettings = existingSettings; mForceQueryable = forceQueryable; diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4f9eab91f7a8..f311034a4dd2 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2900,6 +2900,12 @@ final class InstallPackageHelper { // code is loaded by a new Activity before ApplicationInfo changes have // propagated to all application threads. mPm.scheduleDeferredNoKillPostDelete(args); + if (Flags.improveInstallDontKill()) { + synchronized (mPm.mInstallLock) { + PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller, + packageName, pkgSetting.getPath(), pkgSetting.getOldPaths()); + } + } } else { mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(), args.getInstructionSets()); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 09a91eda483a..1a20c8ddc32f 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -31,6 +31,7 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.DELETE_KEEP_DATA; import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; +import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; @@ -66,8 +67,11 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.LayerDrawable; import android.os.Binder; import android.os.Bundle; @@ -379,9 +383,8 @@ public class PackageArchiver { verifyNotSystemApp(ps.getFlags()); verifyInstalled(ps, userId); String responsibleInstallerPackage = getResponsibleInstallerPackage(ps); - verifyInstaller(responsibleInstallerPackage, userId); - ApplicationInfo installerInfo = snapshot.getApplicationInfo( - responsibleInstallerPackage, /* flags= */ 0, userId); + ApplicationInfo installerInfo = verifyInstaller( + snapshot, responsibleInstallerPackage, userId); verifyOptOutStatus(packageName, UserHandle.getUid(userId, UserHandle.getUid(userId, ps.getAppId()))); @@ -421,10 +424,10 @@ public class PackageArchiver { List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); for (int i = 0, size = mainActivities.size(); i < size; ++i) { var mainActivity = mainActivities.get(i); - Path iconPath = storeDrawable( - packageName, mainActivity.getIcon(), userId, i, iconSize); - Path monochromePath = storeDrawable( - packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize); + Path iconPath = storeAdaptiveDrawable( + packageName, mainActivity.getIcon(), userId, i * 2 + 0, iconSize); + Path monochromePath = storeAdaptiveDrawable( + packageName, mainActivity.getMonochromeIcon(), userId, i * 2 + 1, iconSize); ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( mainActivity.getLabel().toString(), @@ -451,7 +454,8 @@ public class PackageArchiver { List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); for (int i = 0, size = mainActivities.size(); i < size; i++) { LauncherActivityInfo mainActivity = mainActivities.get(i); - Path iconPath = storeIcon(packageName, mainActivity, userId, i, iconSize); + Path iconPath = storeIcon(packageName, mainActivity, userId, i * 2 + 0, iconSize); + // i * 2 + 1 reserved for monochromeIcon ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( mainActivity.getLabel().toString(), @@ -495,8 +499,30 @@ public class PackageArchiver { return iconFile.toPath(); } - private void verifyInstaller(String installerPackageName, int userId) - throws PackageManager.NameNotFoundException { + /** + * Create an <a + * href="https://developer.android.com/develop/ui/views/launch/icon_design_adaptive"> + * adaptive icon</a> from an icon. + * This is necessary so the icon can be displayed properly by different launchers. + */ + private static Path storeAdaptiveDrawable(String packageName, @Nullable Drawable iconDrawable, + @UserIdInt int userId, int index, int iconSize) throws IOException { + if (iconDrawable == null) { + return null; + } + + // see BaseIconFactory#createShapedIconBitmap + float inset = getExtraInsetFraction(); + inset = inset / (1 + 2 * inset); + Drawable d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), + new InsetDrawable(iconDrawable/*d*/, inset, inset, inset, inset)); + + return storeDrawable(packageName, d, userId, index, iconSize); + } + + + private ApplicationInfo verifyInstaller(Computer snapshot, String installerPackageName, + int userId) throws PackageManager.NameNotFoundException { if (TextUtils.isEmpty(installerPackageName)) { throw new PackageManager.NameNotFoundException("No installer found"); } @@ -505,6 +531,12 @@ public class PackageArchiver { && !verifySupportsUnarchival(installerPackageName, userId)) { throw new PackageManager.NameNotFoundException("Installer does not support unarchival"); } + ApplicationInfo appInfo = snapshot.getApplicationInfo( + installerPackageName, /* flags=*/ 0, userId); + if (appInfo == null) { + throw new PackageManager.NameNotFoundException("Failed to obtain Installer info"); + } + return appInfo; } /** @@ -570,7 +602,7 @@ public class PackageArchiver { } try { - verifyInstaller(getResponsibleInstallerPackage(ps), userId); + verifyInstaller(snapshot, getResponsibleInstallerPackage(ps), userId); getLauncherActivityInfos(packageName, userId); } catch (PackageManager.NameNotFoundException e) { return false; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 7bf9fe7aa7e2..cfafe7cc00df 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -788,6 +788,24 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + if (Flags.recoverabilityDetection()) { + if (params.rollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH + || params.rollbackImpactLevel + == PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) { + if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) { + throw new IllegalArgumentException( + "Can't set rollbackImpactLevel when rollback is not enabled"); + } + if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Setting rollbackImpactLevel requires the MANAGE_ROLLBACKS permission"); + } + } else if (params.rollbackImpactLevel < 0) { + throw new IllegalArgumentException("rollbackImpactLevel can't be negative."); + } + } + boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0; if (isApex) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGE_UPDATES) diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 117d03fd059b..27c3dad23450 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -54,6 +54,7 @@ import static com.android.internal.util.XmlUtils.writeStringAttribute; import static com.android.internal.util.XmlUtils.writeUriAttribute; import static com.android.server.pm.PackageInstallerService.prepareStageDir; import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb; import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; @@ -1294,6 +1295,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.autoRevokePermissionsMode = params.autoRevokePermissionsMode; info.installFlags = params.installFlags; info.rollbackLifetimeMillis = params.rollbackLifetimeMillis; + info.rollbackImpactLevel = params.rollbackImpactLevel; info.isMultiPackage = params.isMultiPackage; info.isStaged = params.isStaged; info.rollbackDataPolicy = params.rollbackDataPolicy; @@ -1831,7 +1833,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { Os.link(path, sourcePath); // Grant READ access for APK to be read successfully - Os.chmod(sourcePath, 0644); + Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { e.rethrowAsIOException(); } @@ -1900,7 +1902,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // If file is app metadata then set permission to 0640 to deny user read access since it // might contain sensitive information. - int mode = name.equals(APP_METADATA_FILE_NAME) ? APP_METADATA_FILE_ACCESS_MODE : 0644; + int mode = name.equals(APP_METADATA_FILE_NAME) + ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE; ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(), O_CREAT | O_WRONLY, mode); Os.chmod(target.getAbsolutePath(), mode); @@ -4245,7 +4248,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { throw new IOException("Failed to copy " + fromFile + " to " + tmpFile); } try { - Os.chmod(tmpFile.getAbsolutePath(), 0644); + Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { throw new IOException("Failed to chmod " + tmpFile); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 609b3aa3f43d..609a703137d7 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -593,6 +593,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService static final String APP_METADATA_FILE_NAME = "app.metadata"; + static final int DEFAULT_FILE_ACCESS_MODE = 0644; + final Handler mHandler; final Handler mBackgroundHandler; diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index cd3416348153..85316920f363 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -30,6 +30,7 @@ import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX; import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; @@ -69,6 +70,7 @@ import android.os.Debug; import android.os.Environment; import android.os.FileUtils; import android.os.Process; +import android.os.SELinux; import android.os.SystemProperties; import android.os.UserHandle; import android.os.incremental.IncrementalManager; @@ -129,10 +131,12 @@ import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.zip.GZIPInputStream; @@ -853,7 +857,7 @@ public class PackageManagerServiceUtils { FileUtils.copy(fileIn, outputStream); // Flush anything in buffer before chmod, because any writes after chmod will fail. outputStream.flush(); - Os.fchmod(outputStream.getFD(), 0644); + Os.fchmod(outputStream.getFD(), DEFAULT_FILE_ACCESS_MODE); atomicFile.finishWrite(outputStream); return PackageManager.INSTALL_SUCCEEDED; } catch (IOException e) { @@ -1081,8 +1085,8 @@ public class PackageManagerServiceUtils { final File targetFile = new File(targetDir, targetName); final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(), - O_RDWR | O_CREAT, 0644); - Os.chmod(targetFile.getAbsolutePath(), 0644); + O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE); + Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); FileInputStream source = null; try { source = new FileInputStream(sourcePath); @@ -1552,4 +1556,72 @@ public class PackageManagerServiceUtils { public static boolean isInstalledByAdb(String initiatingPackageName) { return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName); } + + public static void linkSplitsToOldDirs(@NonNull Installer installer, + @NonNull String packageName, + @NonNull File newPath, + @Nullable Set<File> oldPaths) { + if (oldPaths == null || oldPaths.isEmpty()) { + return; + } + if (IncrementalManager.isIncrementalPath(newPath.getPath())) { + //TODO(b/291212866): handle incremental installs + return; + } + final File[] filesInNewPath = newPath.listFiles(); + if (filesInNewPath == null || filesInNewPath.length == 0) { + return; + } + final List<String> splitApkNames = new ArrayList<String>(); + for (int i = 0; i < filesInNewPath.length; i++) { + if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) { + splitApkNames.add(filesInNewPath[i].getName()); + } + } + final int numSplits = splitApkNames.size(); + if (numSplits == 0) { + return; + } + for (File oldPath : oldPaths) { + if (!oldPath.exists()) { + continue; + } + for (int i = 0; i < numSplits; i++) { + final String splitApkName = splitApkNames.get(i); + final File linkedSplit = new File(oldPath, splitApkName); + if (linkedSplit.exists()) { + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Skipping existing linked split <" + + linkedSplit + ">"); + } + continue; + } + final File sourceSplit = new File(newPath, splitApkName); + try { + installer.linkFile(packageName, splitApkName, + newPath.getAbsolutePath(), oldPath.getAbsolutePath()); + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Linked <" + + sourceSplit + "> to <" + linkedSplit + ">"); + } + } catch (Installer.InstallerException e) { + Slog.w(PackageManagerService.TAG, "Failed to link split <" + + sourceSplit + " > to <" + linkedSplit + ">", e); + continue; + } + try { + Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); + } catch (ErrnoException e) { + Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <" + + linkedSplit + ">", e); + continue; + } + if (!SELinux.restorecon(linkedSplit)) { + Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <" + + linkedSplit + ">"); + } + } + } + //TODO(b/291212866): support native libs + } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index ca00c84da724..5c9c8c6d249a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -28,6 +28,7 @@ import static android.content.pm.PackageManager.RESTRICTION_HIDE_NOTIFICATIONS; import static android.content.pm.PackageManager.RESTRICTION_NONE; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.accounts.IAccountManager; @@ -297,6 +298,8 @@ class PackageManagerShellCommand extends ShellCommand { return runSetHiddenSetting(true); case "unhide": return runSetHiddenSetting(false); + case "unstop": + return runSetStoppedState(false); case "suspend": return runSuspend(true, 0); case "suspend-quarantine": @@ -2347,7 +2350,7 @@ class PackageManagerShellCommand extends ShellCommand { Streams.copy(inStream, outStream); } // Give read permissions to the other group. - Os.chmod(outputProfilePath, /*mode*/ 0644 ); + Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE); } catch (IOException | ErrnoException e) { pw.println("Error when reading the profile fd: " + e.getMessage()); e.printStackTrace(pw); @@ -2662,6 +2665,26 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runSetStoppedState(boolean state) throws RemoteException { + int userId = UserHandle.USER_SYSTEM; + String option = getNextOption(); + if (option != null && option.equals("--user")) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } + + String pkg = getNextArg(); + if (pkg == null) { + getErrPrintWriter().println("Error: no package specified"); + return 1; + } + final int translatedUserId = + translateUserId(userId, UserHandle.USER_NULL, "runSetStoppedState"); + mInterface.setPackageStoppedState(pkg, state, translatedUserId); + getOutPrintWriter().println("Package " + pkg + " new stopped state: " + + mInterface.isPackageStoppedForUser(pkg, translatedUserId)); + return 0; + } + private int runSetDistractingRestriction() { final PrintWriter pw = getOutPrintWriter(); int userId = UserHandle.USER_SYSTEM; @@ -3694,7 +3717,19 @@ class PackageManagerShellCommand extends ShellCommand { // remember to set it themselves. params.installerPackageName = "com.android.shell"; } - sessionParams.installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK; + int rollbackStrategy = PackageManager.ROLLBACK_DATA_POLICY_RESTORE; + try { + rollbackStrategy = Integer.parseInt(peekNextArg()); + if (rollbackStrategy < PackageManager.ROLLBACK_DATA_POLICY_RESTORE + || rollbackStrategy > PackageManager.ROLLBACK_DATA_POLICY_RETAIN) { + throw new IllegalArgumentException( + rollbackStrategy + " is not a valid rollback data policy."); + } + getNextArg(); // pop the argument + } catch (NumberFormatException e) { + // not followed by a number assume ROLLBACK_DATA_POLICY_RESTORE. + } + sessionParams.setEnableRollback(true, rollbackStrategy); break; case "--staged-ready-timeout": params.stagedReadyTimeoutMs = Long.parseLong(getNextArgRequired()); @@ -3729,6 +3764,11 @@ class PackageManagerShellCommand extends ShellCommand { } else if (staged) { sessionParams.setStaged(); } + if ((sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0 + && (sessionParams.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0 + && sessionParams.rollbackDataPolicy == PackageManager.ROLLBACK_DATA_POLICY_WIPE) { + throw new IllegalArgumentException("Data policy 'wipe' is not supported for apex."); + } return params; } @@ -4807,7 +4847,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]"); pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); - pw.println(" [--enable-rollback]"); + pw.println(" [--enable-rollback [0/1/2]]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); pw.println(" [--apex] [--non-staged] [--force-non-staged]"); pw.println(" [--staged-ready-timeout TIMEOUT] [--ignore-dexopt-profile]"); @@ -4831,6 +4871,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --abi: override the default ABI of the platform"); pw.println(" --instant: cause the app to be installed as an ephemeral install app"); pw.println(" --full: cause the app to be installed as a non-ephemeral full app"); + pw.println(" --enable-rollback: enable rollbacks for the upgrade."); + pw.println(" 0=restore (default), 1=wipe, 2=retain"); pw.println(" --install-location: force the install location:"); pw.println(" 0=auto, 1=internal only, 2=prefer external"); pw.println(" --install-reason: indicates why the app is being installed:"); @@ -4934,6 +4976,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" hide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(" unhide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(""); + pw.println(" unstop [--user USER_ID] PACKAGE"); + pw.println(""); pw.println(" suspend [--user USER_ID] PACKAGE [PACKAGE...]"); pw.println(" Suspends the specified package(s) (as user)."); pw.println(""); diff --git a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java index 3efac81d44e3..d138606369b9 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java +++ b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java @@ -26,6 +26,7 @@ import android.util.ArrayMap; public final class PermissionAllowlist { @NonNull private final ArrayMap<String, ArrayMap<String, Boolean>> mOemAppAllowlist = new ArrayMap<>(); + @NonNull private final ArrayMap<String, ArrayMap<String, Boolean>> mPrivilegedAppAllowlist = new ArrayMap<>(); @@ -43,6 +44,19 @@ public final class PermissionAllowlist { mApexPrivilegedAppAllowlists = new ArrayMap<>(); @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mVendorSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mProductSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mSystemExtSignatureAppAllowlist = + new ArrayMap<>(); + + @NonNull public ArrayMap<String, ArrayMap<String, Boolean>> getOemAppAllowlist() { return mOemAppAllowlist; } @@ -73,6 +87,26 @@ public final class PermissionAllowlist { return mApexPrivilegedAppAllowlists; } + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getSignatureAppAllowlist() { + return mSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getVendorSignatureAppAllowlist() { + return mVendorSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getProductSignatureAppAllowlist() { + return mProductSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getSystemExtSignatureAppAllowlist() { + return mSystemExtSignatureAppAllowlist; + } + @Nullable public Boolean getOemAppAllowlistState(@NonNull String packageName, @NonNull String permissionName) { @@ -137,4 +171,44 @@ public final class PermissionAllowlist { } return permissions.get(permissionName); } + + @Nullable + public Boolean getSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getVendorSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mVendorSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getProductSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mProductSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getSystemExtSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mSystemExtSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } } diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java index c73728393016..f7603b5cfb57 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java +++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java @@ -27,6 +27,8 @@ import com.android.server.pm.InstallSource; import com.android.server.pm.PackageKeySetData; import com.android.server.pm.permission.LegacyPermissionState; +import java.io.File; +import java.util.Set; import java.util.UUID; /** @@ -111,4 +113,7 @@ public interface PackageStateInternal extends PackageState { */ @Nullable String getAppMetadataFilePath(); + + @Nullable + Set<File> getOldPaths(); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index bf669fba82ce..0abf304c34ee 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5634,7 +5634,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn", 0 /* cookie */); updateScreenOffSleepToken(false /* acquire */, false /* isSwappingDisplay */); - mDefaultDisplayPolicy.screenTurnedOn(screenOnListener); + mDefaultDisplayPolicy.screenTurningOn(screenOnListener); mBootAnimationDismissable = false; synchronized (mLock) { @@ -5676,6 +5676,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mKeyguardDelegate.onScreenTurnedOn(); } } + mDefaultDisplayPolicy.screenTurnedOn(); reportScreenStateToVrManager(true); } diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java index 5e8b4de3894e..7808c4ed50a4 100644 --- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java +++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.Signature; import android.os.Environment; +import android.permission.flags.Flags; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -368,6 +369,7 @@ public class RoleServicePlatformHelperImpl implements RoleServicePlatformHelper dataOutputStream.writeUTF(profileOwner); dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0)); + dataOutputStream.writeBoolean(Flags.walletRoleEnabled()); dataOutputStream.flush(); } catch (IOException e) { // Never happens for MessageDigestOutputStream and DataOutputStream. diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 871e98bf4ab3..4bf8a78a1f16 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -319,6 +319,11 @@ public final class ShutdownThread extends Thread { pd.setMax(100); pd.setProgress(0); pd.setIndeterminate(false); + boolean showPercent = context.getResources().getBoolean( + com.android.internal.R.bool.config_showPercentageTextDuringRebootToUpdate); + if (!showPercent) { + pd.setProgressPercentFormat(null); + } pd.setProgressNumberFormat(null); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); pd.setMessage(context.getText( @@ -911,4 +916,4 @@ public final class ShutdownThread extends Thread { com.android.internal.R.string.config_defaultShutdownVibrationFile); } } -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md index 08800dada564..f6bcbd057307 100644 --- a/services/core/java/com/android/server/rollback/README.md +++ b/services/core/java/com/android/server/rollback/README.md @@ -187,13 +187,22 @@ executed. ### Installing an App with Rollback Enabled -The `adb install` command accepts the `--enable-rollback` flag to install an app +The `adb install` command accepts the `--enable-rollback [0/1/2]` flag to install an app with rollback enabled. For example: ``` $ adb install --enable-rollback FooV2.apk ``` +The default rollback data policy is `ROLLBACK_DATA_POLICY_RESTORE` (0). To use +a different `RollbackDataPolicy`, like `ROLLBACK_DATA_POLICY_RETAIN` (1) or +`ROLLBACK_DATA_POLICY_WIPE` (2), provide the int value after +`--enable-rollback`. For example: + +``` +$ adb install --enable-rollback 1 FooV2.apk +``` + ### Triggering Rollback Manually If rollback is available for an application, the pm command can be used to @@ -217,7 +226,7 @@ $ adb shell dumpsys rollback -state: committed -timestamp: 2019-04-23T14:57:35.944Z -packages: - com.android.tests.rollback.testapp.B 2 -> 1 + com.android.tests.rollback.testapp.B 2 -> 1 [0] -causePackages: -committedSessionId: 1845805640 649899517: @@ -225,7 +234,7 @@ $ adb shell dumpsys rollback -timestamp: 2019-04-23T12:55:21.342Z -stagedSessionId: 343374391 -packages: - com.android.tests.rollback.testapex 2 -> 1 + com.android.tests.rollback.testapex 2 -> 1 [0] -causePackages: -committedSessionId: 2096717281 ``` @@ -233,7 +242,8 @@ $ adb shell dumpsys rollback The example above shows two recently committed rollbacks. The update of com.android.tests.rollback.testapp.B from version 1 to version 2 was rolled back, and the update of com.android.tests.rollback.testapex from version 1 to -version 2 was rolled back. +version 2 was rolled back. For each package the value inside '[' and ']' +indicates the `RollbackDataPolicy` for the rollback back. The state is 'available' or 'committed'. The timestamp gives the time when the rollback was first made available. If a stagedSessionId is present, then the diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index a5b90f12a5cc..d1f91c89a04e 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -215,7 +215,8 @@ class Rollback { /* packages */ new ArrayList<>(), /* isStaged */ isStaged, /* causePackages */ new ArrayList<>(), - /* committedSessionId */ -1); + /* committedSessionId */ -1, + /* rollbackImpactLevel */ PackageManager.ROLLBACK_USER_IMPACT_LOW); mUserId = userId; mInstallerPackageName = installerPackageName; mBackupDir = backupDir; @@ -394,7 +395,8 @@ class Rollback { */ @WorkerThread boolean enableForPackage(String packageName, long newVersion, long installedVersion, - boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy) { + boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy, + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { assertInWorkerThread(); try { RollbackStore.backupPackageCodePath(this, packageName, sourceDir); @@ -415,6 +417,10 @@ class Rollback { isApex, false /* isApkInApex */, new ArrayList<>(), rollbackDataPolicy); info.getPackages().add(packageRollbackInfo); + + if (info.getRollbackImpactLevel() < rollbackImpactLevel) { + info.setRollbackImpactLevel(rollbackImpactLevel); + } return true; } @@ -961,7 +967,8 @@ class Rollback { for (PackageRollbackInfo pkg : info.getPackages()) { ipw.println(pkg.getPackageName() + " " + pkg.getVersionRolledBackFrom().getLongVersionCode() - + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode()); + + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode() + + " [" + pkg.getRollbackDataPolicy() + "]"); } ipw.decreaseIndent(); if (isCommitted()) { diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index 13f114138261..359678b15213 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -954,7 +954,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba ApplicationInfo appInfo = pkgInfo.applicationInfo; return rollback.enableForPackage(packageName, newPackage.getVersionCode(), pkgInfo.getLongVersionCode(), isApex, appInfo.sourceDir, - appInfo.splitSourceDirs, rollbackDataPolicy); + appInfo.splitSourceDirs, rollbackDataPolicy, session.rollbackImpactLevel); } @ExtThread diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index 0af137faf5b4..14539d544bf9 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -193,16 +193,27 @@ class RollbackStore { json.put("isStaged", rollback.isStaged()); json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages())); json.put("committedSessionId", rollback.getCommittedSessionId()); + if (Flags.recoverabilityDetection()) { + json.put("rollbackImpactLevel", rollback.getRollbackImpactLevel()); + } return json; } private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException { - return new RollbackInfo( + RollbackInfo rollbackInfo = new RollbackInfo( json.getInt("rollbackId"), packageRollbackInfosFromJson(json.getJSONArray("packages")), json.getBoolean("isStaged"), versionedPackagesFromJson(json.getJSONArray("causePackages")), json.getInt("committedSessionId")); + + if (Flags.recoverabilityDetection()) { + // to make it backward compatible. + rollbackInfo.setRollbackImpactLevel(json.optInt("rollbackImpactLevel", + PackageManager.ROLLBACK_USER_IMPACT_LOW)); + } + + return rollbackInfo; } /** diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index b384725711c4..92b57645b9a3 100755 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -309,6 +309,24 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + public SparseArray<String> getHardwareInputIdMap() { + synchronized (mLock) { + return mHardwareInputIdMap.clone(); + } + } + + public SparseArray<String> getHdmiInputIdMap() { + synchronized (mLock) { + return mHdmiInputIdMap.clone(); + } + } + + public Map<String, TvInputInfo> getInputMap() { + synchronized (mLock) { + return Collections.unmodifiableMap(mInputMap); + } + } + public Map<String, List<String>> getHdmiParentInputMap() { synchronized (mLock) { return Collections.unmodifiableMap(mHdmiParentInputMap); diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 73fc8e9cfbc4..e434df7836c4 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -135,6 +135,7 @@ public final class TvInputManagerService extends SystemService { private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF; private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; + private static final long UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds // There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d, // another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the @@ -174,7 +175,7 @@ public final class TvInputManagerService extends SystemService { @GuardedBy("mLock") private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>(); - private final WatchLogHandler mWatchLogHandler; + private final MessageHandler mMessageHandler; private final ActivityManager mActivityManager; @@ -187,8 +188,8 @@ public final class TvInputManagerService extends SystemService { super(context); mContext = context; - mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(), - IoThread.get().getLooper()); + mMessageHandler = + new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); mActivityManager = @@ -372,10 +373,10 @@ public final class TvInputManagerService extends SystemService { // service to populate the hardware list. serviceState = new ServiceState(component, userId); userState.serviceStateMap.put(component, serviceState); + updateServiceConnectionLocked(component, userId); } else { inputList.addAll(serviceState.hardwareInputMap.values()); } - updateServiceConnectionLocked(component, userId); } else { try { TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build(); @@ -510,6 +511,7 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") private void startProfileLocked(int userId) { mRunningProfiles.add(userId); buildTvInputListLocked(userId, null); @@ -538,8 +540,10 @@ public final class TvInputManagerService extends SystemService { mCurrentUserId = userId; buildTvInputListLocked(userId, null); buildTvContentRatingSystemListLocked(userId); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER, - getContentResolverForUser(userId)).sendToTarget(); + mMessageHandler + .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER, + getContentResolverForUser(userId)) + .sendToTarget(); } } @@ -593,7 +597,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); it.remove(); } } @@ -661,7 +665,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); } } userState.serviceStateMap.clear(); @@ -774,7 +778,8 @@ public final class TvInputManagerService extends SystemService { boolean shouldBind; if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { - shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware; + shouldBind = !serviceState.sessionTokens.isEmpty() + || (serviceState.isHardware && serviceState.neverConnected); } else { // For a non-current user, // if sessionTokens is not empty, it contains recording sessions only @@ -783,31 +788,14 @@ public final class TvInputManagerService extends SystemService { shouldBind = !serviceState.sessionTokens.isEmpty(); } - if (serviceState.service == null && shouldBind) { - // This means that the service is not yet connected but its state indicates that we - // have pending requests. Then, connect the service. - if (serviceState.bound) { - // We have already bound to the service so we don't try to bind again until after we - // unbind later on. - return; - } - if (DEBUG) { - Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); - } - - Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component); - serviceState.bound = mContext.bindServiceAsUser( - i, serviceState.connection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, - new UserHandle(userId)); - } else if (serviceState.service != null && !shouldBind) { - // This means that the service is already connected but its state indicates that we have - // nothing to do with it. Then, disconnect the service. - if (DEBUG) { - Slog.d(TAG, "unbindService(service=" + component + ")"); + // only bind/unbind when necessary. + if (shouldBind && !serviceState.bound) { + bindService(serviceState, userId); + } else if (!shouldBind && serviceState.bound) { + unbindService(serviceState); + if (!serviceState.isHardware) { + userState.serviceStateMap.remove(component); } - mContext.unbindService(serviceState.connection); - userState.serviceStateMap.remove(component); } } @@ -829,7 +817,11 @@ public final class TvInputManagerService extends SystemService { sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null, null, sessionState.seq); } - updateServiceConnectionLocked(serviceState.component, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(serviceState.component, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } } @GuardedBy("mLock") @@ -948,13 +940,17 @@ public final class TvInputManagerService extends SystemService { if (serviceState != null) { serviceState.sessionTokens.remove(sessionToken); } - updateServiceConnectionLocked(sessionState.componentName, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(sessionState.componentName, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } // Log the end of watch. SomeArgs args = SomeArgs.obtain(); args.arg1 = sessionToken; args.arg2 = System.currentTimeMillis(); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget(); + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_END, args).sendToTarget(); } @GuardedBy("mLock") @@ -1153,8 +1149,7 @@ public final class TvInputManagerService extends SystemService { ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent()); int oldState = inputState.state; inputState.state = state; - if (serviceState != null && serviceState.service == null - && (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) { + if (serviceState != null && serviceState.reconnecting) { // We don't notify state change while reconnecting. It should remain disconnected. return; } @@ -1881,7 +1876,7 @@ public final class TvInputManagerService extends SystemService { args.arg3 = ContentUris.parseId(channelUri); args.arg4 = params; args.arg5 = sessionToken; - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args) + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_START, args) .sendToTarget(); } catch (RemoteException | SessionNotFoundException e) { Slog.e(TAG, "error in tune", e); @@ -3327,16 +3322,21 @@ public final class TvInputManagerService extends SystemService { private final ComponentName component; private final boolean isHardware; private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>(); + private final List<TvInputHardwareInfo> hardwareDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceUpdatedBuffer = new ArrayList<>(); private ITvInputService service; private ServiceCallback callback; private boolean bound; private boolean reconnecting; + private boolean neverConnected; private ServiceState(ComponentName component, int userId) { this.component = component; this.connection = new InputServiceConnection(component, userId); this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component); + this.neverConnected = true; } } @@ -3449,6 +3449,97 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") + private void bindService(ServiceState serviceState, int userId) { + if (serviceState.bound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + // For hardware services, call updateHardwareServiceConnectionDelayed() to delay the + // possible unbinding. + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(userId); + } + return; + } + if (DEBUG) { + Slog.d(TAG, + "bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId + + ")"); + } + Intent i = + new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component); + serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + if (!serviceState.bound) { + Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId); + mContext.unbindService(serviceState.connection); + } + } + + @GuardedBy("mLock") + private void unbindService(ServiceState serviceState) { + if (!serviceState.bound) { + return; + } + if (DEBUG) { + Slog.d(TAG, "unbindService(service=" + serviceState.component + ")"); + } + mContext.unbindService(serviceState.connection); + serviceState.bound = false; + serviceState.service = null; + serviceState.callback = null; + } + + @GuardedBy("mLock") + private void updateHardwareTvInputServiceBindingLocked(int userId) { + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = + pm.queryIntentServicesAsUser(new Intent(TvInputService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId); + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) { + continue; + } + ComponentName component = new ComponentName(si.packageName, si.name); + if (hasHardwarePermission(pm, component)) { + updateServiceConnectionLocked(component, userId); + } + } + } + + private void updateHardwareServiceConnectionDelayed(int userId) { + mMessageHandler.removeMessages(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = userId; + Message msg = + mMessageHandler.obtainMessage(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING, args); + mMessageHandler.sendMessageDelayed(msg, UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS); + } + + @GuardedBy("mLock") + private void addHardwareInputLocked( + TvInputInfo inputInfo, ComponentName component, int userId) { + ServiceState serviceState = getServiceStateLocked(component, userId); + serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); + buildTvInputListLocked(userId, null); + } + + @GuardedBy("mLock") + private void removeHardwareInputLocked(String inputId, int userId) { + if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) { + return; + } + ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent(); + ServiceState serviceState = getServiceStateLocked(component, userId); + boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; + if (removed) { + buildTvInputListLocked(userId, null); + mTvInputHardwareManager.removeHardwareInput(inputId); + } + } + private final class InputServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -3472,6 +3563,7 @@ public final class TvInputManagerService extends SystemService { } ServiceState serviceState = userState.serviceStateMap.get(mComponent); serviceState.service = ITvInputService.Stub.asInterface(service); + serviceState.neverConnected = false; // Register a callback, if we need to. if (serviceState.isHardware && serviceState.callback == null) { @@ -3483,19 +3575,6 @@ public final class TvInputManagerService extends SystemService { } } - List<IBinder> tokensToBeRemoved = new ArrayList<>(); - - // And create sessions, if any. - for (IBinder sessionToken : serviceState.sessionTokens) { - if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { - tokensToBeRemoved.add(sessionToken); - } - } - - for (IBinder sessionToken : tokensToBeRemoved) { - removeSessionStateLocked(sessionToken, mUserId); - } - for (TvInputState inputState : userState.inputMap.values()) { if (inputState.info.getComponent().equals(component) && inputState.state != INPUT_STATE_CONNECTED) { @@ -3505,7 +3584,24 @@ public final class TvInputManagerService extends SystemService { } if (serviceState.isHardware) { - serviceState.hardwareInputMap.clear(); + for (TvInputHardwareInfo hardwareToBeRemoved : + serviceState.hardwareDeviceRemovedBuffer) { + try { + serviceState.service.notifyHardwareRemoved(hardwareToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hardwareDeviceRemovedBuffer", e); + } + } + serviceState.hardwareDeviceRemovedBuffer.clear(); + for (HdmiDeviceInfo hdmiDeviceToBeRemoved : + serviceState.hdmiDeviceRemovedBuffer) { + try { + serviceState.service.notifyHdmiDeviceRemoved(hdmiDeviceToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceRemovedBuffer", e); + } + } + serviceState.hdmiDeviceRemovedBuffer.clear(); for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) { try { serviceState.service.notifyHardwareAdded(hardware); @@ -3520,6 +3616,32 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + for (HdmiDeviceInfo hdmiDeviceToBeUpdated : + serviceState.hdmiDeviceUpdatedBuffer) { + try { + serviceState.service.notifyHdmiDeviceUpdated(hdmiDeviceToBeUpdated); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceUpdatedBuffer", e); + } + } + serviceState.hdmiDeviceUpdatedBuffer.clear(); + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.sessionTokens) { + if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeSessionStateLocked(sessionToken, mUserId); + } + + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(mUserId); } } } @@ -3570,13 +3692,6 @@ public final class TvInputManagerService extends SystemService { } } - @GuardedBy("mLock") - private void addHardwareInputLocked(TvInputInfo inputInfo) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); - buildTvInputListLocked(mUserId, null); - } - public void addHardwareInput(int deviceId, TvInputInfo inputInfo) { ensureHardwarePermission(); ensureValidInput(inputInfo); @@ -3587,8 +3702,11 @@ public final class TvInputManagerService extends SystemService { if (serviceState.hardwareInputMap.containsKey(inputInfo.getId())) { return; } + Slog.d("ServiceCallback", + "addHardwareInput: device id " + deviceId + ", " + + inputInfo.toString()); mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -3606,7 +3724,7 @@ public final class TvInputManagerService extends SystemService { return; } mTvInputHardwareManager.addHdmiInput(id, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); if (mOnScreenInputId != null && mOnScreenSessionState != null) { if (TextUtils.equals(mOnScreenInputId, inputInfo.getParentId())) { // catch the use case when a CEC device is plugged in an HDMI port, @@ -3635,14 +3753,9 @@ public final class TvInputManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; - if (removed) { - buildTvInputListLocked(mUserId, null); - mTvInputHardwareManager.removeHardwareInput(inputId); - } else { - Slog.e(TAG, "failed to remove input " + inputId); - } + Slog.d("ServiceCallback", + "removeHardwareInput " + inputId + " by " + mComponent); + removeHardwareInputLocked(inputId, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -3860,6 +3973,23 @@ public final class TvInputManagerService extends SystemService { } @Override + public void onVideoFreezeUpdated(boolean isFrozen) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onVideoFreezeUpdated(" + isFrozen + ")"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onVideoFreezeUpdated(isFrozen, mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onVideoFreezeUpdated", e); + } + } + } + + @Override public void onContentAllowed() { synchronized (mLock) { if (DEBUG) { @@ -4209,11 +4339,12 @@ public final class TvInputManagerService extends SystemService { return loggedReason; } + @GuardedBy("mLock") private UserState getUserStateLocked(int userId) { return mUserStates.get(userId); } - private static final class WatchLogHandler extends Handler { + private final class MessageHandler extends Handler { // There are only two kinds of watch events that can happen on the system: // 1. The current TV input session is tuned to a new channel. // 2. The session is released for some reason. @@ -4225,10 +4356,11 @@ public final class TvInputManagerService extends SystemService { static final int MSG_LOG_WATCH_START = 1; static final int MSG_LOG_WATCH_END = 2; static final int MSG_SWITCH_CONTENT_RESOLVER = 3; + static final int MSG_UPDATE_HARDWARE_TIS_BINDING = 4; private ContentResolver mContentResolver; - WatchLogHandler(ContentResolver contentResolver, Looper looper) { + MessageHandler(ContentResolver contentResolver, Looper looper) { super(looper); mContentResolver = contentResolver; } @@ -4287,6 +4419,14 @@ public final class TvInputManagerService extends SystemService { mContentResolver = (ContentResolver) msg.obj; break; } + case MSG_UPDATE_HARDWARE_TIS_BINDING: + SomeArgs args = (SomeArgs) msg.obj; + int userId = (int) args.arg1; + synchronized (mLock) { + updateHardwareTvInputServiceBindingLocked(userId); + } + args.recycle(); + break; default: { Slog.w(TAG, "unhandled message code: " + msg.what); break; @@ -4342,29 +4482,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareAdded(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareAdded(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHardwareDeviceRemoved(TvInputHardwareInfo info) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareRemoved(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareRemoved(info); + } else { + serviceState.hardwareDeviceRemovedBuffer.add(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4374,29 +4531,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + } else { + serviceState.hdmiDeviceRemovedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4424,13 +4598,21 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + } else { + serviceState.hdmiDeviceUpdatedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 7ab075e2f3a7..f0c84370c922 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -1038,7 +1038,7 @@ public class TvInteractiveAppManagerService extends SystemService { } } finally { if (surface != null) { - // surface is not used in TvInteractiveAppManagerService. + // surface is not used in TvAdManagerService. surface.release(); } Binder.restoreCallingIdentity(identity); @@ -1106,6 +1106,67 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @Override + public void createMediaView(IBinder sessionToken, IBinder windowToken, Rect frame, + int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "createMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .createMediaView(windowToken, frame); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in createMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void relayoutMediaView(IBinder sessionToken, Rect frame, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "relayoutMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .relayoutMediaView(frame); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in relayoutMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void removeMediaView(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "removeMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .removeMediaView(); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in removeMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { @@ -1470,6 +1531,28 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void notifyVideoFreezeUpdated(IBinder sessionToken, boolean isFrozen, int userId) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "notifyVideoFreezeUpdated"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).notifyVideoFreezeUpdated(isFrozen); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in notifyVideoFreezeUpdated", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void notifyContentAllowed(IBinder sessionToken, int userId) { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); @@ -1557,7 +1640,6 @@ public class TvInteractiveAppManagerService extends SystemService { } } - @Override public void notifyRecordingStarted(IBinder sessionToken, String recordingId, String requestId, int userId) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9b1f9c8441ad..f273bcbaa65c 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -7978,6 +7978,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { return; } + final int originalRelaunchingCount = mPendingRelaunchCount; // This is necessary in order to avoid going into size compat mode when the orientation // change request comes from the app if (getRequestedConfigurationOrientation(false, requestedOrientation) @@ -7995,8 +7996,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // the request is handled at task level with letterbox. if (!getMergedOverrideConfiguration().equals( mLastReportedConfiguration.getMergedConfiguration())) { - ensureActivityConfiguration( - false /* ignoreVisibility */, true /* isRequestedOrientationChanged */); + ensureActivityConfiguration(false /* ignoreVisibility */); + if (mPendingRelaunchCount > originalRelaunchingCount) { + mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); + } if (mTransitionController.inPlayingTransition(this)) { mTransitionController.mValidateActivityCompat.add(this); } @@ -9502,11 +9505,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return ensureActivityConfiguration(false /* ignoreVisibility */); } - boolean ensureActivityConfiguration(boolean ignoreVisibility) { - return ensureActivityConfiguration(ignoreVisibility, - false /* isRequestedOrientationChanged */); - } - /** * Make sure the given activity matches the current configuration. Ensures the HistoryRecord * is updated with the correct configuration and all other bookkeeping is handled. @@ -9515,13 +9513,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * (stopped state). This is useful for the case where we know the * activity will be visible soon and we want to ensure its configuration * before we make it visible. - * @param isRequestedOrientationChanged whether this is triggered in response to an app calling - * {@link android.app.Activity#setRequestedOrientation}. * @return False if the activity was relaunched and true if it wasn't relaunched because we * can't or the app handles the specific configuration that is changing. */ - boolean ensureActivityConfiguration(boolean ignoreVisibility, - boolean isRequestedOrientationChanged) { + boolean ensureActivityConfiguration(boolean ignoreVisibility) { final Task rootTask = getRootTask(); if (rootTask.mConfigWillChange) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check " @@ -9658,9 +9653,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mRelaunchReason = RELAUNCH_REASON_NONE; } - if (isRequestedOrientationChanged) { - mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); - } if (mState == PAUSING) { // A little annoying: we are waiting for this activity to finish pausing. Let's not // do anything now, but just flag that it needs to be restarted when done pausing. diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java index f1a2159d6dbe..db27f607c867 100644 --- a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java +++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java @@ -43,7 +43,7 @@ class ActivitySecurityModelFeatureFlags { static final String DOC_LINK = "go/android-asm"; /** Used to determine which version of the ASM logic was used in logs while we iterate */ - static final int ASM_VERSION = 8; + static final int ASM_VERSION = 9; private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; private static final String KEY_ASM_PREFIX = "ActivitySecurity__"; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f6d77ea33598..d6f52b89819e 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2053,8 +2053,8 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, targetTask, mLaunchFlags, mBalCode, mCallingUid, - mRealCallingUid)) { + mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, + mCallingUid, mRealCallingUid)) { return START_ABORTED; } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 2bd49bfa6219..a4d15e07a3ed 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.View.FOCUS_FORWARD; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_NONE; @@ -60,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; import com.android.server.wm.utils.InsetUtils; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -167,6 +169,24 @@ class BackNavigationController { return null; } + // Move focus to the adjacent embedded window if it is higher than this window + final TaskFragment taskFragment = window.getTaskFragment(); + final TaskFragment adjacentTaskFragment = + taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null; + if (adjacentTaskFragment != null && taskFragment.isEmbedded() + && Flags.embeddedActivityBackNavFlag()) { + final WindowContainer parent = taskFragment.getParent(); + if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf( + adjacentTaskFragment)) { + mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD); + window = wmService.getFocusedWindowLocked(); + if (window == null) { + Slog.e(TAG, "Adjacent window is null, returning null."); + return null; + } + } + } + // This is needed to bridge the old and new back behavior with recents. While in // Overview with live tile enabled, the previous app is technically focused but we // add an input consumer to capture all input that would otherwise go to the apps diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 0f36d8eafbe4..39dd77ef8fe0 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -57,6 +57,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Process; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; @@ -1042,8 +1043,9 @@ public class BackgroundActivityStartController { * create a new task or bring an existing one into the foreground */ boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord, - @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask, - int launchFlags, int balCode, int callingUid, int realCallingUid) { + @NonNull ActivityRecord targetRecord, boolean newTask, boolean avoidMoveTaskToFront, + @Nullable Task targetTask, int launchFlags, int balCode, int callingUid, + int realCallingUid) { // BAL Exception allowed in all cases if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { return true; @@ -1067,14 +1069,36 @@ public class BackgroundActivityStartController { } if (balCode == BAL_ALLOW_GRACE_PERIOD) { + // Allow if launching into new task, and caller matches most recently finished activity if (taskToFront && mTopFinishedActivity != null && mTopFinishedActivity.mUid == callingUid) { return true; - } else if (!taskToFront) { - FinishedActivityEntry finishedEntry = - mTaskIdToFinishedActivity.get(targetTask.mTaskId); - if (finishedEntry != null && finishedEntry.mUid == callingUid) { - return true; + } + + // Launching into existing task - allow if matches most recently finished activity + // within the task. + // We can reach here multiple ways: + // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available) + // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available) + // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true, + // avoidMoveTaskToFront = true, sourceRecord is available) + // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true, + // sourceRecord is not available, targetTask may be available) + if (!taskToFront || avoidMoveTaskToFront) { + if (targetTask != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(targetTask.mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } + } + + if (sourceRecord != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } } } } @@ -1098,7 +1122,7 @@ public class BackgroundActivityStartController { bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(), sourceRecord); } - } else if (!taskToFront) { + } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) { // We don't have a sourceRecord, and we're launching into an existing task. // Allow if callingUid is top of stack. bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid, @@ -1111,12 +1135,14 @@ public class BackgroundActivityStartController { // ASM rules have failed. Log why return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid, - newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront); + newTask, avoidMoveTaskToFront, targetTask, targetRecord, balCode, launchFlags, + bas, taskToFront); } private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid, - int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord, - @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) { + int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask, + ActivityRecord targetRecord, @BalCode int balCode, int launchFlags, + BlockActivityStart bas, boolean taskToFront) { ActivityRecord targetTopActivity = targetTask == null ? null : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); @@ -1133,7 +1159,7 @@ public class BackgroundActivityStartController { String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord, targetRecord, targetTask, targetTopActivity, realCallingUid, balCode, - blockActivityStartAndFeatureEnabled, taskToFront); + blockActivityStartAndFeatureEnabled, taskToFront, avoidMoveTaskToFront); FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, /* caller_uid */ @@ -1265,7 +1291,7 @@ public class BackgroundActivityStartController { Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord, targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart, - /* taskToFront */ true)); + /* taskToFront */ true, /* avoidMoveTaskToFront */ false)); } } @@ -1379,7 +1405,7 @@ public class BackgroundActivityStartController { private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task, int uid, @Nullable ActivityRecord sourceRecord) { // If the source is visible, consider it 'top'. - if (sourceRecord != null && sourceRecord.isVisible()) { + if (sourceRecord != null && sourceRecord.isVisibleRequested()) { return new BlockActivityStart(false, false); } @@ -1389,6 +1415,12 @@ public class BackgroundActivityStartController { return new BlockActivityStart(false, false); } + // If UID is visible in target task, allow launch + if (task.forAllActivities((Predicate<ActivityRecord>) + ar -> ar.isUid(uid) && ar.isVisibleRequested())) { + return new BlockActivityStart(false, false); + } + // Consider the source activity, whether or not it is finishing. Do not consider any other // finishing activity. Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord) @@ -1480,27 +1512,26 @@ public class BackgroundActivityStartController { @Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord, @Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity, int realCallingUid, @BalCode int balCode, - boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) { + boolean blockActivityStartAndFeatureEnabled, boolean taskToFront, + boolean avoidMoveTaskToFront) { final String prefix = "[ASM] "; Function<ActivityRecord, String> recordToString = (ar) -> { if (ar == null) { return null; } - return (ar == sourceRecord ? " [source]=> " + + return (ar == sourceRecord ? " [source]=> " : ar == targetTopActivity ? " [ top ]=> " - : ar == targetRecord ? " [target]=> " - : " => ") - + ar - + " :: visible=" + ar.isVisible() - + ", finishing=" + ar.isFinishing() - + ", alwaysOnTop=" + ar.isAlwaysOnTop() - + ", taskFragment=" + ar.getTaskFragment(); + : ar == targetRecord ? " [target]=> " + : " => ") + + getDebugStringForActivityRecord(ar); }; StringJoiner joiner = new StringJoiner("\n"); joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------"); joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled); joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION); + joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis()); boolean targetTaskMatchesSourceTask = targetTask != null && sourceRecord != null && sourceRecord.getTask() == targetTask; @@ -1512,6 +1543,8 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage); } else { joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord)); + joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage); + joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent); if (targetTaskMatchesSourceTask) { joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask()); joiner.add(prefix + "Source/Target Task Stack: "); @@ -1536,7 +1569,30 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord)); joiner.add(prefix + "Intent: " + targetRecord.intent); joiner.add(prefix + "TaskToFront: " + taskToFront); + joiner.add(prefix + "AvoidMoveToFront: " + avoidMoveTaskToFront); joiner.add(prefix + "BalCode: " + balCodeToString(balCode)); + joiner.add(prefix + "LastResumedActivity: " + + recordToString.apply(mService.mLastResumedActivity)); + + if (mTopFinishedActivity != null) { + joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo); + } + + if (!mTaskIdToFinishedActivity.isEmpty()) { + joiner.add(prefix + "TaskIdToFinishedActivity: "); + mTaskIdToFinishedActivity.values().forEach( + (fae) -> joiner.add(prefix + " " + fae.mDebugInfo)); + } + + if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW + || balCode == BAL_ALLOW_FOREGROUND) { + Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask; + if (task != null && task.getDisplayArea() != null) { + joiner.add(prefix + "Tasks: "); + task.getDisplayArea().forAllTasks((Consumer<Task>) + t -> joiner.add(prefix + " T: " + t.toFullString())); + } + } joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------"); return joiner.toString(); @@ -1620,7 +1676,7 @@ public class BackgroundActivityStartController { return; } - if (!finishActivity.mVisibleRequested + if (!finishActivity.isVisibleRequested() && finishActivity != finishActivity.getTask().getTopMostActivity()) { return; } @@ -1666,10 +1722,22 @@ public class BackgroundActivityStartController { } } + private static String getDebugStringForActivityRecord(ActivityRecord ar) { + return ar + + " :: visible=" + ar.isVisible() + + ", visibleRequested=" + ar.isVisibleRequested() + + ", finishing=" + ar.isFinishing() + + ", alwaysOnTop=" + ar.isAlwaysOnTop() + + ", lastLaunchTime=" + ar.lastLaunchTime + + ", lastVisibleTime=" + ar.lastVisibleTime + + ", taskFragment=" + ar.getTaskFragment(); + } + private class FinishedActivityEntry { int mUid; int mTaskId; int mLaunchCount; + String mDebugInfo; FinishedActivityEntry(ActivityRecord ar) { FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId); @@ -1677,6 +1745,7 @@ public class BackgroundActivityStartController { this.mUid = ar.getUid(); this.mTaskId = taskId; this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1; + this.mDebugInfo = getDebugStringForActivityRecord(ar); mService.mH.postDelayed(() -> { synchronized (mService.mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 63ca5929e34d..e2bc59bb6550 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -794,6 +794,9 @@ public class DisplayPolicy { } mService.mAtmService.mKeyguardController.updateDeferTransitionForAod( mAwake /* waiting */); + if (!awake) { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } } } @@ -836,7 +839,8 @@ public class DisplayPolicy { mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars; } - public void screenTurnedOn(ScreenOnListener screenOnListener) { + /** Prepares to turn on screen. The given listener is used to notify that it is ready. */ + public void screenTurningOn(ScreenOnListener screenOnListener) { WindowProcessController visibleDozeUiProcess = null; synchronized (mLock) { mScreenOnEarly = true; @@ -858,6 +862,11 @@ public class DisplayPolicy { } } + /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */ + public void screenTurnedOn() { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } + public void screenTurnedOff() { synchronized (mLock) { mScreenOnEarly = false; diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 02b3f15979ce..587cc7489763 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2783,6 +2783,9 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } else { throw new RuntimeException("Create the same sleep token twice: " + token); } + if (isSwappingDisplay) { + display.mWallpaperController.onDisplaySwitchStarted(); + } return token; } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index d68f932400a2..0fc62a758c5e 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -120,6 +121,11 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + /** + * Whether the wallpaper has been notified about a physical display switch event is started. + */ + private volatile boolean mIsWallpaperNotifiedOnDisplaySwitch; + private final Consumer<WindowState> mFindWallpapers = w -> { if (w.mAttrs.type == TYPE_WALLPAPER) { WallpaperWindowToken token = w.mToken.asWallpaperToken(); @@ -1083,6 +1089,52 @@ class WallpaperController { } /** + * Notifies the wallpaper that the display turns off when switching physical device. If the + * wallpaper is currently visible, its client visibility will be preserved until the display is + * confirmed to be off or on. + */ + void onDisplaySwitchStarted() { + mIsWallpaperNotifiedOnDisplaySwitch = notifyDisplaySwitch(true /* start */); + } + + /** + * Called when the screen has finished turning on or the device goes to sleep. This is no-op if + * the operation is not part of a display switch. + */ + void onDisplaySwitchFinished() { + // The method can be called outside WM lock (turned on), so only acquire lock if needed. + // This is to optimize the common cases that regular devices don't have display switch. + if (mIsWallpaperNotifiedOnDisplaySwitch) { + synchronized (mService.mGlobalLock) { + mIsWallpaperNotifiedOnDisplaySwitch = false; + notifyDisplaySwitch(false /* start */); + } + } + } + + private boolean notifyDisplaySwitch(boolean start) { + boolean notified = false; + for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { + final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); + for (int i = token.getChildCount() - 1; i >= 0; i--) { + final WindowState w = token.getChildAt(i); + if (start && !w.mWinAnimator.getShown()) { + continue; + } + try { + w.mClient.dispatchWallpaperCommand(COMMAND_DISPLAY_SWITCH, 0 /* x */, 0 /* y */, + start ? 1 : 0 /* use z as start or finish */, + null /* bundle */, false /* sync */); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to dispatch COMMAND_DISPLAY_SWITCH " + e); + } + notified = true; + } + } + return notified; + } + + /** * Each window can request a zoom, example: * - User is in overview, zoomed out. * - User also pulls down the shade. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f8ac8da710c8..9650b8bc2281 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9156,55 +9156,63 @@ public class WindowManagerService extends IWindowManager.Stub if (fromWin == null || !fromWin.isFocused()) { return false; } - final TaskFragment fromFragment = fromWin.getTaskFragment(); - if (fromFragment == null) { - return false; - } - final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); - if (adjacentFragment == null || adjacentFragment.asTask() != null) { - // Don't move the focus to another task. - return false; - } - final Rect fromBounds = fromFragment.getBounds(); - final Rect adjacentBounds = adjacentFragment.getBounds(); - switch (direction) { - case View.FOCUS_LEFT: - if (adjacentBounds.left >= fromBounds.left) { - return false; - } - break; - case View.FOCUS_UP: - if (adjacentBounds.top >= fromBounds.top) { - return false; - } - break; - case View.FOCUS_RIGHT: - if (adjacentBounds.right <= fromBounds.right) { - return false; - } - break; - case View.FOCUS_DOWN: - if (adjacentBounds.bottom <= fromBounds.bottom) { - return false; - } - break; - case View.FOCUS_BACKWARD: - case View.FOCUS_FORWARD: - // These are not absolute directions. Skip checking the bounds. - break; - default: + return moveFocusToAdjacentWindow(fromWin, direction); + } + } + + boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) { + final TaskFragment fromFragment = fromWin.getTaskFragment(); + if (fromFragment == null) { + return false; + } + final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (adjacentFragment == null || adjacentFragment.asTask() != null) { + // Don't move the focus to another task. + return false; + } + if (adjacentFragment.isIsolatedNav()) { + // Don't move the focus if the adjacent TF is isolated navigation. + return false; + } + final Rect fromBounds = fromFragment.getBounds(); + final Rect adjacentBounds = adjacentFragment.getBounds(); + switch (direction) { + case View.FOCUS_LEFT: + if (adjacentBounds.left >= fromBounds.left) { return false; - } - final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( - true /* focusableOnly */); - if (topRunningActivity == null) { - return false; - } - moveDisplayToTopInternal(topRunningActivity.getDisplayId()); - handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); - if (fromWin.isFocused()) { + } + break; + case View.FOCUS_UP: + if (adjacentBounds.top >= fromBounds.top) { + return false; + } + break; + case View.FOCUS_RIGHT: + if (adjacentBounds.right <= fromBounds.right) { + return false; + } + break; + case View.FOCUS_DOWN: + if (adjacentBounds.bottom <= fromBounds.bottom) { + return false; + } + break; + case View.FOCUS_BACKWARD: + case View.FOCUS_FORWARD: + // These are not absolute directions. Skip checking the bounds. + break; + default: return false; - } + } + final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( + true /* focusableOnly */); + if (topRunningActivity == null) { + return false; + } + moveDisplayToTopInternal(topRunningActivity.getDisplayId()); + handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); + if (fromWin.isFocused()) { + return false; } return true; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 1c90e30c256e..2049331e8efe 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -46,6 +46,7 @@ #include <com_android_input_flags.h> #include <input/Input.h> #include <input/PointerController.h> +#include <input/PrintTools.h> #include <input/SpriteController.h> #include <inputflinger/InputManager.h> #include <limits.h> @@ -230,10 +231,6 @@ inline static T max(const T& a, const T& b) { return a > b ? a : b; } -static inline const char* toString(bool value) { - return value ? "true" : "false"; -} - static SpriteIcon toSpriteIcon(PointerIcon pointerIcon) { // As a minor optimization, do not make a copy of the PointerIcon bitmap here. The loaded // PointerIcons are only cached by InputManagerService in java, so we can safely assume they @@ -288,7 +285,7 @@ public: void setSystemUiLightsOut(bool lightsOut); void setPointerDisplayId(int32_t displayId); void setPointerSpeed(int32_t speed); - void setMousePointerAccelerationEnabled(bool enabled); + void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled); void setTouchpadPointerSpeed(int32_t speed); void setTouchpadNaturalScrollingEnabled(bool enabled); void setTouchpadTapToClickEnabled(bool enabled); @@ -304,6 +301,7 @@ public: bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId, DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken); + void setPointerIconVisibility(int32_t displayId, bool visible); void setMotionClassifierEnabled(bool enabled); std::optional<std::string> getBluetoothAddress(int32_t deviceId); void setStylusButtonMotionEventsEnabled(bool enabled); @@ -400,8 +398,8 @@ private: // Pointer speed. int32_t pointerSpeed{0}; - // True if pointer acceleration is enabled for mice. - bool mousePointerAccelerationEnabled{true}; + // Displays on which its associated mice will have pointer acceleration disabled. + std::set<int32_t> displaysWithMousePointerAccelerationDisabled{}; // True if pointer gestures are enabled. bool pointerGesturesEnabled{true}; @@ -492,8 +490,8 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); - dump += StringPrintf(INDENT "Mouse Pointer Acceleration: %s\n", - mLocked.mousePointerAccelerationEnabled ? "Enabled" : "Disabled"); + dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n", + dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", toString(mLocked.pointerGesturesEnabled)); dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches)); @@ -676,11 +674,13 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon std::scoped_lock _l(mLock); outConfig->mousePointerSpeed = mLocked.pointerSpeed; - outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled; - outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed - * POINTER_SPEED_EXPONENT); + outConfig->displaysWithMousePointerAccelerationDisabled = + mLocked.displaysWithMousePointerAccelerationDisabled; + outConfig->pointerVelocityControlParameters.scale = + exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = - mLocked.mousePointerAccelerationEnabled + mLocked.displaysWithMousePointerAccelerationDisabled.count( + mLocked.pointerDisplayId) == 0 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION : 1; outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled; @@ -1224,16 +1224,23 @@ void NativeInputManager::setPointerSpeed(int32_t speed) { InputReaderConfiguration::Change::POINTER_SPEED); } -void NativeInputManager::setMousePointerAccelerationEnabled(bool enabled) { +void NativeInputManager::setMousePointerAccelerationEnabled(int32_t displayId, bool enabled) { { // acquire lock std::scoped_lock _l(mLock); - if (mLocked.mousePointerAccelerationEnabled == enabled) { + const bool oldEnabled = + mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0; + if (oldEnabled == enabled) { return; } - ALOGI("Setting mouse pointer acceleration to %s", toString(enabled)); - mLocked.mousePointerAccelerationEnabled = enabled; + ALOGI("Setting mouse pointer acceleration to %s on display %d", toString(enabled), + displayId); + if (enabled) { + mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId); + } else { + mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId); + } } // release lock mInputManager->getReader().requestRefreshConfiguration( @@ -1397,6 +1404,13 @@ bool NativeInputManager::setPointerIcon( return mInputManager->getChoreographer().setPointerIcon(std::move(icon), displayId, deviceId); } +void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) { + if (!ENABLE_POINTER_CHOREOGRAPHER) { + return; + } + mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible); +} + TouchAffineTransformation NativeInputManager::getTouchAffineTransformation( JNIEnv *env, jfloatArray matrixArr) { ATRACE_CALL(); @@ -2168,10 +2182,10 @@ static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed } static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj, - jboolean enabled) { + jint displayId, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setMousePointerAccelerationEnabled(enabled); + im->setMousePointerAccelerationEnabled(displayId, enabled); } static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { @@ -2550,6 +2564,13 @@ static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject ico ibinderForJavaObject(env, inputTokenObj)); } +static void nativeSetPointerIconVisibility(JNIEnv* env, jobject nativeImplObj, jint displayId, + jboolean visible) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + im->setPointerIconVisibility(displayId, visible); +} + static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint displayId) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2791,7 +2812,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeTransferTouchFocus}, {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch}, {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, - {"setMousePointerAccelerationEnabled", "(Z)V", + {"setMousePointerAccelerationEnabled", "(IZ)V", (void*)nativeSetMousePointerAccelerationEnabled}, {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed}, {"setTouchpadNaturalScrollingEnabled", "(Z)V", @@ -2828,6 +2849,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeSetCustomPointerIcon}, {"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z", (void*)nativeSetPointerIcon}, + {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility}, {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay}, {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged}, {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation}, diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp index 11c40d7bcd9b..9c033e25c04e 100644 --- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp +++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp @@ -675,7 +675,8 @@ static jboolean android_location_gnss_hal_GnssNative_start_measurement_collectio options.enableCorrVecOutputs = enableCorrVecOutputs; options.intervalMs = intervalMs; - return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(), + return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>( + gnssMeasurementIface->getInterfaceVersion()), options); } diff --git a/services/core/jni/gnss/Gnss.cpp b/services/core/jni/gnss/Gnss.cpp index 8934c3a6abde..da8928b5f97f 100644 --- a/services/core/jni/gnss/Gnss.cpp +++ b/services/core/jni/gnss/Gnss.cpp @@ -196,7 +196,8 @@ void GnssHal::linkToDeath() { jboolean GnssHal::setCallback() { if (gnssHalAidl != nullptr) { - sp<IGnssCallbackAidl> gnssCbIfaceAidl = new GnssCallbackAidl(); + sp<IGnssCallbackAidl> gnssCbIfaceAidl = + new GnssCallbackAidl(gnssHalAidl->getInterfaceVersion()); auto status = gnssHalAidl->setCallback(gnssCbIfaceAidl); if (!checkAidlStatus(status, "IGnssAidl setCallback() failed.")) { return JNI_FALSE; diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp index 60eed8e6d716..3d598f7a7203 100644 --- a/services/core/jni/gnss/GnssCallback.cpp +++ b/services/core/jni/gnss/GnssCallback.cpp @@ -120,7 +120,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz) { Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) { ALOGD("%s: %du\n", __func__, capabilities); - bool isAdrCapabilityKnown = (getInterfaceVersion() >= 3) ? true : false; + bool isAdrCapabilityKnown = (interfaceVersion >= 3) ? true : false; JNIEnv* env = getJniEnv(); env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities, isAdrCapabilityKnown); @@ -178,7 +178,7 @@ Status GnssCallbackAidl::gnssLocationCb(const hardware::gnss::GnssLocation& loca Status GnssCallbackAidl::gnssNmeaCb(const int64_t timestamp, const std::string& nmea) { // In AIDL v1, if no listener is registered, do not report nmea to the framework. - if (getInterfaceVersion() <= 1) { + if (interfaceVersion <= 1) { if (!isNmeaRegistered) { return Status::ok(); } diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h index 33acec8b5660..0622e533e5b5 100644 --- a/services/core/jni/gnss/GnssCallback.h +++ b/services/core/jni/gnss/GnssCallback.h @@ -60,6 +60,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz); */ class GnssCallbackAidl : public hardware::gnss::BnGnssCallback { public: + GnssCallbackAidl(int version) : interfaceVersion(version){}; binder::Status gnssSetCapabilitiesCb(const int capabilities) override; binder::Status gnssSetSignalTypeCapabilitiesCb( const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override; @@ -73,6 +74,9 @@ public: binder::Status gnssRequestTimeCb() override; binder::Status gnssRequestLocationCb(const bool independentFromGnss, const bool isUserEmergency) override; + +private: + const int interfaceVersion; }; /* diff --git a/services/core/jni/gnss/GnssMeasurement.h b/services/core/jni/gnss/GnssMeasurement.h index 7a95db8ed7b6..20400fd8f587 100644 --- a/services/core/jni/gnss/GnssMeasurement.h +++ b/services/core/jni/gnss/GnssMeasurement.h @@ -41,6 +41,7 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) = 0; virtual jboolean close() = 0; + virtual int getInterfaceVersion() = 0; }; class GnssMeasurement : public GnssMeasurementInterface { @@ -50,6 +51,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return mIGnssMeasurement->getInterfaceVersion(); + } private: const sp<android::hardware::gnss::IGnssMeasurementInterface> mIGnssMeasurement; @@ -63,6 +67,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return 0; + } private: const sp<android::hardware::gnss::V1_0::IGnssMeasurement> mIGnssMeasurement_V1_0; diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp index 2982546bfa47..ebab4c342ca0 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.cpp +++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp @@ -392,7 +392,7 @@ void GnssMeasurementCallbackAidl::translateAndSetGnssData(const GnssData& data) jobjectArray gnssAgcArray = nullptr; gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs); - if (this->getInterfaceVersion() >= 3) { + if (interfaceVersion >= 3) { setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray, /*hasIsFullTracking=*/true, data.isFullTracking); } else { @@ -467,7 +467,7 @@ void GnssMeasurementCallbackAidl::translateSingleGnssMeasurement(JNIEnv* env, satellitePvt.tropoDelayMeters); } - if (this->getInterfaceVersion() >= 2) { + if (interfaceVersion >= 2) { callObjectMethodIgnoringResult(env, satellitePvtBuilderObject, method_satellitePvtBuilderSetTimeOfClock, satellitePvt.timeOfClockSeconds); diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h index b3de486e6fa9..3cb47ce2fe18 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.h +++ b/services/core/jni/gnss/GnssMeasurementCallback.h @@ -53,7 +53,8 @@ void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback { public: - GnssMeasurementCallbackAidl() : mCallbacksObj(getCallbacksObj()) {} + GnssMeasurementCallbackAidl(int version) + : mCallbacksObj(getCallbacksObj()), interfaceVersion(version) {} android::binder::Status gnssMeasurementCb(const hardware::gnss::GnssData& data) override; private: @@ -71,6 +72,7 @@ private: void translateGnssClock(JNIEnv* env, const hardware::gnss::GnssData& data, JavaObject& object); jobject& mCallbacksObj; + const int interfaceVersion; }; /* @@ -110,10 +112,10 @@ private: class GnssMeasurementCallback { public: - GnssMeasurementCallback() {} + GnssMeasurementCallback(int version) : interfaceVersion(version) {} sp<GnssMeasurementCallbackAidl> getAidl() { if (callbackAidl == nullptr) { - callbackAidl = sp<GnssMeasurementCallbackAidl>::make(); + callbackAidl = sp<GnssMeasurementCallbackAidl>::make(interfaceVersion); } return callbackAidl; } @@ -128,6 +130,7 @@ public: private: sp<GnssMeasurementCallbackAidl> callbackAidl; sp<GnssMeasurementCallbackHidl> callbackHidl; + const int interfaceVersion; }; template <class T> diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 62d2d7ee848a..4c74878dab70 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -22,6 +22,7 @@ import android.content.pm.PermissionGroupInfo import android.content.pm.PermissionInfo import android.content.pm.SigningDetails import android.os.Build +import android.permission.flags.Flags import android.util.Slog import com.android.internal.os.RoSystemProperties import com.android.internal.pm.permission.CompatibilityPermissionInfo @@ -1197,15 +1198,80 @@ class AppIdPermissionPolicy : SchemePolicy() { newState.externalState.packageStates[PLATFORM_PACKAGE_NAME]!! .androidPackage!! .signingDetails - return sourceSigningDetails?.hasCommonSignerWithCapability( - packageSigningDetails, - SigningDetails.CertCapabilities.PERMISSION - ) == true || - packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) || - platformSigningDetails.checkCapability( + val hasCommonSigner = + sourceSigningDetails?.hasCommonSignerWithCapability( packageSigningDetails, SigningDetails.CertCapabilities.PERMISSION - ) + ) == true || + packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) || + platformSigningDetails.checkCapability( + packageSigningDetails, + SigningDetails.CertCapabilities.PERMISSION + ) + if (!Flags.signaturePermissionAllowlistEnabled()) { + return hasCommonSigner; + } + if (!hasCommonSigner) { + return false + } + // A platform signature permission also needs to be allowlisted on non-debuggable builds. + if (permission.packageName == PLATFORM_PACKAGE_NAME) { + val isRequestedByFactoryApp = + if (packageState.isSystem) { + // For updated system applications, a signature permission still needs to be + // allowlisted if it wasn't requested by the original application. + if (packageState.isUpdatedSystemApp) { + val disabledSystemPackage = + newState.externalState.disabledSystemPackageStates[ + packageState.packageName] + ?.androidPackage + disabledSystemPackage != null && + permission.name in disabledSystemPackage.requestedPermissions + } else { + true + } + } else { + false + } + if ( + !(isRequestedByFactoryApp || + getSignaturePermissionAllowlistState(packageState, permission.name) == true) + ) { + Slog.w( + LOG_TAG, + "Signature permission ${permission.name} for package" + + " ${packageState.packageName} (${packageState.path}) not in" + + " signature permission allowlist" + ) + if (!Build.isDebuggable()) { + return false + } + } + } + return true + } + + private fun MutateStateScope.getSignaturePermissionAllowlistState( + packageState: PackageState, + permissionName: String + ): Boolean? { + val permissionAllowlist = newState.externalState.permissionAllowlist + val packageName = packageState.packageName + return when { + packageState.isVendor || packageState.isOdm -> + permissionAllowlist.getVendorSignatureAppAllowlistState(packageName, permissionName) + packageState.isProduct -> + permissionAllowlist.getProductSignatureAppAllowlistState( + packageName, + permissionName + ) + packageState.isSystemExt -> + permissionAllowlist.getSystemExtSignatureAppAllowlistState( + packageName, + permissionName + ) + else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName) + } } private fun MutateStateScope.checkPrivilegedPermissionAllowlist( diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index f469ab547763..097d73a9a05b 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -960,8 +960,8 @@ class PermissionService(private val service: AccessCheckingService) : if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) { if (reportError) { - throw SecurityException( - "Permission $permissionName isn't requested by package $packageName" + Slog.e( + LOG_TAG, "Permission $permissionName isn't requested by package $packageName" ) } return diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index d82e6abeb502..0edb3dfc0bc0 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 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. @@ -26,18 +26,13 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; -import org.junit.runner.RunWith; import java.io.File; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AdditionalSubtypeUtilsTest { +public final class AdditionalSubtypeUtilsTest { @Test public void testSaveAndLoad() throws Exception { @@ -60,7 +55,7 @@ public class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); - AdditionalSubtypeUtils.saveToFile(allSubtypes, methodMap, atomicFile); + AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile); ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>(); AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile); diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java index 6eedeea8c333..b7223d6615b9 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java @@ -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. @@ -19,17 +19,11 @@ package com.android.server.inputmethod; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import org.junit.Test; -import org.junit.runner.RunWith; import java.util.Arrays; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) public final class HardwareKeyboardShortcutControllerTest { @Test diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java index 570132f5a7d8..71752ba3b393 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java @@ -126,11 +126,9 @@ public class InputMethodManagerServiceRestrictImeAmountTest extends List<String> enabledComponents) { final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap = new ArrayMap<>(); - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - InputMethodManagerService.filterInputMethodServices(emptyAdditionalSubtypeMap, methodMap, - methodList, enabledComponents, mContext, resolveInfoList); - return methodList; + final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices( + emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList); + return methodMap.values(); } private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) { diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java index 0884b784ac73..fbe384a62658 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 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. @@ -28,22 +28,16 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class InputMethodSubtypeSwitchingControllerTest { +public final class InputMethodSubtypeSwitchingControllerTest { private static final String DUMMY_PACKAGE_NAME = "dummy package name"; private static final String DUMMY_IME_LABEL = "dummy ime label"; private static final String DUMMY_SETTING_ACTIVITY_NAME = ""; diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 9688ef6cc83b..2857619c70d3 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 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. @@ -41,15 +41,12 @@ import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.inputmethod.StartInputFlags; import com.google.common.truth.Truth; import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Collections; @@ -57,9 +54,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class InputMethodUtilsTest { +public final class InputMethodUtilsTest { private static final boolean IS_AUX = true; private static final boolean IS_DEFAULT = true; private static final boolean IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE = true; @@ -274,7 +269,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); @@ -298,7 +293,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -322,7 +317,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_GB), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); @@ -347,7 +342,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -368,7 +363,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR_CA), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -390,7 +385,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -412,7 +407,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoHi, result.get(0)); @@ -429,7 +424,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -446,7 +441,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -468,7 +463,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Latn-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrLatn, is(in(result))); @@ -488,7 +483,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Cyrl-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrCyrl, is(in(result))); @@ -514,7 +509,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList( Locale.forLanguageTag("sr-Latn-RS-x-android"), Locale.forLanguageTag("ja-JP"), @@ -541,7 +536,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); @@ -559,7 +554,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -575,7 +570,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -589,7 +584,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -603,7 +598,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -617,7 +612,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -639,7 +634,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); assertThat(nonAutoFrCA, is(in(result))); assertThat(nonAutoEnUS, is(in(result))); @@ -801,19 +796,22 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), + null, "")); } // Returns null when the config value is empty. { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), "", + "")); } // Returns null when the configured package doesn't have an IME. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), "")); } @@ -821,7 +819,8 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), null)); } @@ -829,13 +828,15 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } // Returns null when the current default isn't found. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), systemIme.getId())); } @@ -846,7 +847,7 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } @@ -857,7 +858,8 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), systemIme.getId())); } @@ -867,7 +869,7 @@ public class InputMethodUtilsTest { final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", "fake.voice0", false /* isSystem */); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), nonSystemIme.getId())); } @@ -878,7 +880,7 @@ public class InputMethodUtilsTest { "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); methodMap.put(systemIme.getId(), systemIme); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), "")); } } diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java index 01f8129c2cd7..d0b46f58d626 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 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. @@ -22,18 +22,12 @@ import static org.junit.Assert.assertEquals; import android.os.LocaleList; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Locale; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class LocaleUtilsTest { +public final class LocaleUtilsTest { private static final LocaleUtils.LocaleExtractor<Locale> sIdentityMapper = source -> source; diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java index 978044045ab3..a185ad99a8f3 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java @@ -419,7 +419,7 @@ public class PackageUserStateTest { "installerTitle"); packageUserState.setArchiveState(archiveState); assertEquals(archiveState, packageUserState.getArchiveState()); - assertTrue(archiveState.getArchiveTimeMillis() > currentTimeMillis); + assertTrue(archiveState.getArchiveTimeMillis() >= currentTimeMillis); } @Test diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 57c3a1d5f364..95cfc2a304d4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -22,6 +22,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; +import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -82,6 +83,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.TestUtils; +import com.android.internal.R; import com.android.internal.compat.IPlatformCompat; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener; @@ -857,8 +859,7 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); - info.setComponentName(COMPONENT_NAME); + final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME); assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue(); } @@ -867,10 +868,9 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(COMPONENT_NAME); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mEnabledServices.clear(); userState.mEnabledServices.add(info_b.getComponentName()); @@ -883,12 +883,12 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(new ComponentName("package_a", "class_a")); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); - final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo(); - info_c.setComponentName(new ComponentName("package_c", "class_c")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a")); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mAccessibilityButtonTargets.clear(); userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString()); @@ -900,6 +900,51 @@ public class AccessibilityManagerServiceTest { assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); } + @Test + @RequiresFlagsEnabled({ + FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG, + FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES}) + public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), true); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), false); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c"), true); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{ + info_b.getComponentName().flattenToString(), + info_c.getComponentName().flattenToString()}); + + // info_a is not in the allowlist => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue(); + // info_b is not preinstalled => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue(); + // info_c is both in the allowlist and preinstalled => do not require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName) { + return mockAccessibilityServiceInfo(componentName, false); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName, + boolean isSystemApp) { + AccessibilityServiceInfo accessibilityServiceInfo = + Mockito.spy(new AccessibilityServiceInfo()); + accessibilityServiceInfo.setComponentName(componentName); + ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class); + when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo); + mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class); + mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); + when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp); + return accessibilityServiceInfo; + } + // Single package intents can trigger multiple PackageMonitor callbacks. // Collect the state of the lock in a set, since tests only care if calls // were all locked or all unlocked. diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java index 9d56a36196bb..5e11e17f9414 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java @@ -18,6 +18,7 @@ package com.android.server.rollback; import static com.google.common.truth.Truth.assertThat; +import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.util.SparseIntArray; @@ -81,7 +82,8 @@ public class RollbackStoreTest { + "'installedUsers':[55,79]," + "'ceSnapshotInodes':[]}],'isStaged':false,'causePackages':[{'packageName':'hello'," + "'longVersionCode':23},{'packageName':'something','longVersionCode':999}]," - + "'committedSessionId':45654465},'timestamp':'2019-10-01T12:29:08.855Z'," + + "'committedSessionId':45654465, 'rollbackImpactLevel':1}," + + "'timestamp':'2019-10-01T12:29:08.855Z'," + "'originalSessionId':567,'state':'enabling','apkSessionId':-1," + "'restoreUserDataInProgress':true, 'userId':0," + "'installerPackageName':'some.installer'}"; @@ -138,6 +140,8 @@ public class RollbackStoreTest { assertThat(rollback.getOriginalSessionId()).isEqualTo(567); assertThat(rollback.info.getRollbackId()).isEqualTo(ID); assertThat(rollback.info.getPackages()).isEmpty(); + assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); assertThat(rollback.isEnabling()).isTrue(); assertThat(rollback.getExtensionVersions().toString()) .isEqualTo(extensionVersions.toString()); @@ -158,6 +162,8 @@ public class RollbackStoreTest { assertThat(rollback.info.getRollbackId()).isEqualTo(ID); assertThat(rollback.info.getPackages()).isEmpty(); + assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); assertThat(rollback.isEnabling()).isTrue(); assertThat(rollback.getExtensionVersions().toString()) .isEqualTo(extensionVersions.toString()); @@ -175,6 +181,7 @@ public class RollbackStoreTest { origRb.info.getCausePackages().add(new VersionedPackage("com.made.up", 2)); origRb.info.getCausePackages().add(new VersionedPackage("com.pack.age", 99)); origRb.info.setCommittedSessionId(123456); + origRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH); PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("com.made.up", 18), @@ -226,6 +233,7 @@ public class RollbackStoreTest { expectedRb.info.getCausePackages().add(new VersionedPackage("hello", 23)); expectedRb.info.getCausePackages().add(new VersionedPackage("something", 999)); expectedRb.info.setCommittedSessionId(45654465); + expectedRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH); PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("blah", 55), new VersionedPackage("blah1", 50), new ArrayList<>(), new ArrayList<>(), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java index ea948ca0e28b..863cda4905f1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java @@ -118,12 +118,19 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { // Constructors that should be used to create instances of specific classes. Overrides scoring. private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS; + // Setter methods that receive String parameters, but where those Strings represent Uris + // (and are visited/validated). + private static final ImmutableSet<Method> SETTERS_WITH_STRING_AS_URI; + static { try { PREFERRED_CONSTRUCTORS = ImmutableMap.of( Notification.Builder.class, Notification.Builder.class.getConstructor(Context.class, String.class)); + SETTERS_WITH_STRING_AS_URI = ImmutableSet.of( + Person.Builder.class.getMethod("setUri", String.class)); + EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder() .put(RemoteViews.class, // b/245950570: Tries to connect to service and will crash. @@ -257,7 +264,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { @Nullable Class<?> styleClass, @Nullable Class<?> extenderClass, @Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) { SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context); - Set<Class<?>> excludedClasses = includeRemoteViews + ImmutableSet<Class<?>> excludedClasses = includeRemoteViews ? ImmutableSet.of() : ImmutableSet.of(RemoteViews.class); Location location = Location.root(Notification.Builder.class); @@ -294,7 +301,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object generateObject(Class<?> clazz, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { if (excludingClasses.contains(clazz)) { throw new IllegalArgumentException( String.format("Asked to generate a %s but it's part of the excluded set (%s)", @@ -369,7 +376,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object constructEmpty(Class<?> clazz, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { Constructor<?> bestConstructor; if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) { // Use the preferred constructor. @@ -431,7 +438,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static void invokeAllSetters(Object instance, Location where, boolean allOverloads, - boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes, + boolean includingVoidMethods, ImmutableSet<Class<?>> excludingParameterTypes, SpecialParameterGenerator specialGenerator) { for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where, allOverloads, includingVoidMethods, excludingParameterTypes)) { @@ -462,24 +469,34 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object[] generateParameters(Executable executable, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable) + " in " + where); Type[] parameterTypes = executable.getGenericParameterTypes(); Object[] parameterValues = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { - parameterValues[i] = generateParameter( - parameterTypes[i], + boolean generateUriAsString = false; + Type parameterType = parameterTypes[i]; + if (SETTERS_WITH_STRING_AS_URI.contains(executable) + && parameterType.equals(String.class)) { + generateUriAsString = true; + } + Object value = generateParameter( + generateUriAsString ? Uri.class : parameterType, where.plus(executable, String.format("[%d,%s]", i, parameterTypes[i].getTypeName())), excludingClasses, specialGenerator); + if (generateUriAsString) { + value = ((Uri) value).toString(); + } + parameterValues[i] = value; } return parameterValues; } private static Object generateParameter(Type parameterType, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { if (parameterType instanceof Class<?> parameterClass) { return generateObject( parameterClass, @@ -487,7 +504,8 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { excludingClasses, specialGenerator); } else if (parameterType instanceof ParameterizedType parameterizedType) { - if (parameterizedType.getRawType().equals(List.class) + if ((parameterizedType.getRawType().equals(List.class) + || parameterizedType.getRawType().equals(ArrayList.class)) && parameterizedType.getActualTypeArguments()[0] instanceof Class<?>) { ArrayList listValue = new ArrayList(); for (int i = 0; i < NUM_ELEMENTS_IN_ARRAY; i++) { @@ -503,12 +521,14 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static class ReflectionUtils { - static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) { - return Arrays.stream(containerClass.getDeclaredClasses()) - .filter( - innerClass -> clazz.isAssignableFrom(innerClass) - && !Modifier.isAbstract(innerClass.getModifiers())) - .collect(Collectors.toSet()); + static ImmutableSet<Class<?>> getConcreteSubclasses(Class<?> clazz, + Class<?> containerClass) { + return ImmutableSet.copyOf( + Arrays.stream(containerClass.getDeclaredClasses()) + .filter( + innerClass -> clazz.isAssignableFrom(innerClass) + && !Modifier.isAbstract(innerClass.getModifiers())) + .collect(Collectors.toSet())); } static String methodToString(Executable executable) { @@ -611,9 +631,16 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static class SpecialParameterGenerator { + + private static final ImmutableSet<Class<?>> INTERESTING_CLASSES_WITH_SPECIAL_GENERATION = + ImmutableSet.of(Uri.class, Icon.class, Intent.class, PendingIntent.class, + RemoteViews.class); + private static final ImmutableSet<Class<?>> INTERESTING_CLASSES = - ImmutableSet.of(Person.class, Uri.class, Icon.class, Intent.class, - PendingIntent.class, RemoteViews.class); + new ImmutableSet.Builder<Class<?>>() + .addAll(INTERESTING_CLASSES_WITH_SPECIAL_GENERATION) + .add(Person.class) // Constructed via reflection, but high-score. + .build(); private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of(); private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES = @@ -637,7 +664,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } static boolean canGenerate(Class<?> clazz) { - return INTERESTING_CLASSES.contains(clazz) + return INTERESTING_CLASSES_WITH_SPECIAL_GENERATION.contains(clazz) || MOCKED_CLASSES.contains(clazz) || clazz.equals(Context.class) || clazz.equals(Bundle.class) @@ -672,17 +699,6 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { return Icon.createWithContentUri(iconUri); } - if (clazz == Person.class) { - // TODO(b/310189261): Person.setUri takes a string instead of a URI. We should - // find a way to use the SpecialParameterGenerator instead of this custom one. - Uri personUri = generateUri( - where.plus(Person.Builder.class).plus("setUri", String.class)); - Uri iconUri = generateUri(where.plus(Person.Builder.class).plus("setIcon", - Icon.class).plus(Icon.class).plus("createWithContentUri", Uri.class)); - return new Person.Builder().setUri(personUri.toString()).setIcon( - Icon.createWithContentUri(iconUri)).setName("John Doe").build(); - } - if (clazz == Intent.class) { return new Intent("action", generateUri(where.plus(Intent.class))); } 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 248683836336..25ad7dbac30c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -59,6 +59,7 @@ import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS; @@ -123,6 +124,7 @@ import android.os.Parcel; import android.os.Process; import android.os.SimpleClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; @@ -238,15 +240,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { public TestWithLooperRule mLooperRule = new TestWithLooperRule(); ConditionProviders mConditionProviders; - @Mock NotificationManager mNotificationManager; - @Mock PackageManager mPackageManager; + @Mock + NotificationManager mNotificationManager; + @Mock + PackageManager mPackageManager; private Resources mResources; private TestableLooper mTestableLooper; private final TestClock mTestClock = new TestClock(); private ZenModeHelper mZenModeHelper; private ContentResolver mContentResolver; - @Mock DeviceEffectsApplier mDeviceEffectsApplier; - @Mock AppOpsManager mAppOps; + @Mock + DeviceEffectsApplier mDeviceEffectsApplier; + @Mock + AppOpsManager mAppOps; TestableFlagResolver mTestFlagResolver = new TestableFlagResolver(); ZenModeEventLoggerFake mZenModeEventLogger; @@ -290,7 +296,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mPackageManager.getPackageUidAsUser(eq(CUSTOM_PKG_NAME), anyInt())) .thenReturn(CUSTOM_PKG_UID); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {pkg}); + new String[]{pkg}); ApplicationInfo appInfoSpy = spy(new ApplicationInfo()); appInfoSpy.icon = ICON_RES_ID; @@ -305,24 +311,26 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException { - String xml = "<zen version=\"8\" user=\"0\">\n" - + "<allow calls=\"false\" repeatCallers=\"false\" messages=\"true\" " - + "reminders=\"false\" events=\"false\" callsFrom=\"1\" messagesFrom=\"2\" " - + "visualScreenOff=\"true\" alarms=\"true\" " - + "media=\"true\" system=\"false\" conversations=\"true\"" - + " conversationsFrom=\"2\"/>\n" - + "<automatic ruleId=\"" + EVENTS_DEFAULT_RULE_ID - + "\" enabled=\"false\" snoozing=\"false\"" - + " name=\"Event\" zen=\"1\"" - + " component=\"android/com.android.server.notification.EventConditionProvider\"" - + " conditionId=\"condition://android/event?userId=-10000&calendar=&" + String xml = "<zen version=\"10\">\n" + + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" " + + "callsFrom=\"2\" messages=\"true\"\n" + + "messagesFrom=\"2\" reminders=\"false\" events=\"false\" " + + "repeatCallers=\"true\" convos=\"true\"\n" + + "convosFrom=\"2\"/>\n" + + "<automatic ruleId=" + EVENTS_DEFAULT_RULE_ID + + " enabled=\"false\" snoozing=\"false\"" + + " name=\"Event\" zen=\"1\"\n" + + " component=\"android/com.android.server.notification.EventConditionProvider\"\n" + + " conditionId=\"condition://android/event?userId=-10000&calendar=&" + "reply=1\"/>\n" - + "<automatic ruleId=\"" + SCHEDULE_DEFAULT_RULE_ID + "\" enabled=\"false\"" - + " snoozing=\"false\" name=\"Sleeping\" zen=\"1\"" - + " component=\"android/com.android.server.notification.ScheduleConditionProvider\"" - + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7 &start=22.0" - + "&end=7.0&exitAtAlarm=true\"/>" - + "<disallow visualEffects=\"511\" />" + + "<automatic ruleId=" + SCHEDULE_DEFAULT_RULE_ID + " enabled=\"false\"" + + " snoozing=\"false\" name=\"Sleeping\"\n zen=\"1\"" + + " component=\"android/com.android.server.notification" + + ".ScheduleConditionProvider\"\n" + + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7&start=22.0" + + "&end=7.0&exitAtAlarm=true\"/>\n" + + "<disallow visualEffects=\"157\" />\n" + + "<state areChannelsBypassingDnd=\"false\" />\n" + "</zen>"; TypedXmlPullParser parser = Xml.newFastPullParser(); parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), null); @@ -408,7 +416,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOff_NoMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_OFF; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -421,7 +429,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_NotificationApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -442,7 +450,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_StarredCallers_CallTypesBlocked() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -462,7 +470,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_AllCallers_CallTypesAllowed() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -481,7 +489,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_AllowAlarmsMedia_NoAlarmMediaMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); @@ -493,7 +501,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_DisallowAlarmsMedia_AlarmMediaMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); verifyApplyRestrictions(true, true, AudioAttributes.USAGE_ALARM); @@ -506,7 +514,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testTotalSilence() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -525,7 +533,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testAlarmsOnly_alarmMediaMuteNotApplied() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -545,7 +553,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testAlarmsOnly_callsMuteApplied() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -559,7 +567,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testAlarmsOnly_allZenConfigToggledCannotBypass_alarmMuteNotApplied() { // Only audio attributes with SUPPRESIBLE_NEVER can bypass mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -571,7 +579,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Only audio attributes with SUPPRESIBLE_NEVER can bypass // with special case USAGE_ASSISTANCE_SONIFICATION mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -592,7 +600,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_priorityOnlyMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -607,7 +615,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_alarmsOnlyMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = Global.ZEN_MODE_ALARMS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -622,7 +630,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_totalSilenceMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = Global.ZEN_MODE_NO_INTERRUPTIONS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -1007,7 +1015,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); assertEquals("Config mismatch: current vs expected: " - + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected, + + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected, mZenModeHelper.mConfig); } @@ -1336,7 +1344,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM); assertEquals("Config mismatch: current vs original: " - + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original), + + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original), original, mZenModeHelper.mConfig); assertEquals(original.hashCode(), mZenModeHelper.mConfig.hashCode()); } @@ -1778,6 +1786,225 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_onModesApi_noUpgrade() throws Exception { + // When reading XML for something that is already on the modes API system, make sure no + // rules' policies get changed. + setupZenConfig(); + + // Shared for rules + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRules = new ArrayMap<>(); + final ScheduleInfo weeknights = new ScheduleInfo(); + + // Custom rule with a custom policy + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + ZenPolicy policy = new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_CONTACTS) + .allowAlarms(true) + .allowRepeatCallers(false) + .build(); + // Fill in policy fields, since on modes api we do not expect any rules to have unset fields + customRule.zenPolicy = mZenModeHelper.getDefaultZenPolicy().overwrittenWith(policy); + enabledAutoRules.put("customRule", customRule); + mZenModeHelper.mConfig.automaticRules = enabledAutoRules; + + // set version to post-modes-API = 11 + ByteArrayOutputStream baos = writeXmlAndPurge(11); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rules. + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isEqualTo(customRule.zenPolicy); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception { + // When reading in an XML file written from a pre-modes-API version, confirm that we create + // a custom policy matching the global config for any automatic rule with no specified + // policy. + setupZenConfig(); + + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo weeknights = new ScheduleInfo(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + enabledAutoRule.put("customRule", customRule); // no custom policy set + mZenModeHelper.mConfig.automaticRules = enabledAutoRule; + + // set version to pre-modes-API = 10 + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rule and check that it has a policy set now + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isNotNull(); + + // Check policy values as set up in setupZenConfig() to confirm they match + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception { + // When reading in an XML file written from a pre-modes-API version, confirm that for an + // underspecified ZenPolicy, we fill in all of the gaps with things from the global config + // in order to maintain consistency of behavior. + setupZenConfig(); + + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo weeknights = new ScheduleInfo(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + customRule.zenPolicy = new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowRepeatCallers(false) + .build(); + enabledAutoRule.put("customRule", customRule); + mZenModeHelper.mConfig.automaticRules = enabledAutoRule; + + // set version to pre-modes-API = 10 + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rule and check that it has a policy set now + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isNotNull(); + + // Check unset policy values match values in setupZenConfig(). + // Check that set policy values match the values set in the policy. + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_DISALLOW); + + // Check that the rest is filled in from the default + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy() + throws Exception { + setupZenConfig(); + + // Default rules, if they exist and have no policies, should get a snapshot of the global + // policy, even if they are disabled upon upgrade. + ArrayMap<String, ZenModeConfig.ZenRule> automaticRules = new ArrayMap<>(); + ZenModeConfig.ZenRule defaultScheduleRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo defaultScheduleRuleInfo = new ScheduleInfo(); + defaultScheduleRule.enabled = false; + defaultScheduleRule.name = "Default Schedule Rule"; + defaultScheduleRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + defaultScheduleRule.conditionId = ZenModeConfig.toScheduleConditionId( + defaultScheduleRuleInfo); + defaultScheduleRule.id = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID; + automaticRules.put(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, defaultScheduleRule); + + ZenModeConfig.ZenRule defaultEventRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo defaultEventRuleInfo = new ScheduleInfo(); + defaultEventRule.enabled = false; + defaultEventRule.name = "Default Event Rule"; + defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId( + defaultEventRuleInfo); + defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; + automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule); + + mZenModeHelper.mConfig.automaticRules = automaticRules; + + // set previous version + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // check default rules + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules.size()).isGreaterThan(0); + for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + assertThat(rules).containsKey(defaultId); + ZenRule rule = rules.get(defaultId); + assertThat(rule.zenPolicy).isNotNull(); + + // Check policy values as set up in setupZenConfig() to confirm they match + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + } + + @Test public void testCountdownConditionSubscription() throws Exception { ZenModeConfig config = new ZenModeConfig(); mZenModeHelper.mConfig = config; @@ -2036,6 +2263,69 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() { + // When a new automatic zen rule is added with only some fields filled in, ensure that + // all unset fields are filled in with device defaults. + + // Zen rule with null policy: should get entirely the default state + AutomaticZenRule zenRule1 = new AutomaticZenRule("name", + new ComponentName("android", "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // Zen rule with partially-filled policy: should get all of the filled fields set, and the + // rest filled with default state + AutomaticZenRule zenRule2 = new AutomaticZenRule("name", + null, + new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_NONE) + .allowMessages(PEOPLE_TYPE_CONTACTS) + .showFullScreenIntent(true) + .build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // rule 1 should exist + assertThat(id1).isNotNull(); + ZenModeConfig.ZenRule rule1InConfig = mZenModeHelper.mConfig.automaticRules.get(id1); + assertThat(rule1InConfig).isNotNull(); + assertThat(rule1InConfig.zenPolicy).isNotNull(); // we passed in null; it should now not be + + // all of rule 1 should be the device default's policy + assertThat(rule1InConfig.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + + // rule 2 should exist + assertThat(id2).isNotNull(); + ZenModeConfig.ZenRule rule2InConfig = mZenModeHelper.mConfig.automaticRules.get(id2); + assertThat(rule2InConfig).isNotNull(); + + // rule 2: values set from the policy itself + assertThat(rule2InConfig.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(rule2InConfig.zenPolicy.getPriorityMessageSenders()) + .isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(rule2InConfig.zenPolicy.getVisualEffectFullScreenIntent()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + + // the rest of rule 2's settings should be the device defaults + assertThat(rule2InConfig.zenPolicy.getPriorityConversationSenders()) + .isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + assertThat(rule2InConfig.zenPolicy.getPriorityCategorySystem()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule2InConfig.zenPolicy.getPriorityCategoryAlarms()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(rule2InConfig.zenPolicy.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule2InConfig.zenPolicy.getVisualEffectNotificationList()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + } + + @Test public void testSetAutomaticZenRuleState_nullPkg() { AutomaticZenRule zenRule = new AutomaticZenRule("name", null, @@ -2357,6 +2647,68 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_nullPolicy_doesNothing() { + // Test that when updateAutomaticZenRule is called with a null policy, nothing changes + // about the existing policy. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setOwner(OWNER) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars + .build()) + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + mZenModeHelper.updateAutomaticZenRule(ruleId, + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + // no zen policy + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_overwritesExistingPolicy() { + // Test that when updating an automatic zen rule with an existing policy, the newly set + // fields overwrite those from the previous policy, but unset fields in the new policy + // keep values from the previous one. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setOwner(OWNER) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars + .allowAlarms(false) + .allowReminders(true) + .build()) + .build(), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + + mZenModeHelper.updateAutomaticZenRule(ruleId, + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) + .build()) + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from update + assertThat(savedRule.getZenPolicy().getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from update + assertThat(savedRule.getZenPolicy().getPriorityCategoryAlarms()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from original + assertThat(savedRule.getZenPolicy().getPriorityCategoryReminders()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from original + } + + + @Test + @EnableFlags(Flags.FLAG_MODES_API) public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -2460,7 +2812,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI); private final boolean mEnabled; - @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi; + @ConfigChangeOrigin + private final int mOriginForUserActionInSystemUi; ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) { this.mEnabled = enabled; @@ -2506,7 +2859,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - rules active = 1 // - user action = true (system-based turning zen mode on) // - package uid = system (as set above) - // - resulting DNDPolicyProto the same as the values in setupZenConfig() + // - resulting DNDPolicyProto the same as the values in setupZenConfig() (global policy) assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2600,7 +2953,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - 1 rule (newly) active // - automatic (is not a user action) // - package UID is written to be the rule *owner* even though it "comes from system" - // - zen policy is the same as the set-up zen config + // - zen policy is the default as it's unspecified assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2609,10 +2962,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // When the automatic rule is disabled, this should turn off zen mode and also count as a - // user action. + // user action. We don't care what the consolidated policy is when DND turns off. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), mZenModeEventLogger.getEventId(1)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); @@ -2835,28 +3188,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { // First: turn on rule 1 mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Second: turn on rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Third: turn on rule 3 mZenModeHelper.setAutomaticZenRuleState(id3, new Condition(zenRule3.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Fourth: Turn *off* rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // This should result in a total of four events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); // Event 1: rule 1 turns on. We expect this to turn on DND (zen mode) overall, so that's - // what the event should reflect. At this time, the policy is the same as initial setup. + // what the event should reflect. At this time, the policy is the default. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2864,7 +3217,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // Event 2: rule 2 turns on. This should not change anything about the policy, so the only // change is that there are more rules active now. @@ -2873,7 +3226,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(2, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1)); // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such, // but meanwhile also change the number of active rules. @@ -2926,12 +3279,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); + // Explicitly set up all rules with the same policy as the manual rule so there will be + // no policy changes in this test case. + ZenPolicy manualRulePolicy = mZenModeHelper.mConfig.toZenPolicy(); + // Rule 1, owned by a package AutomaticZenRule zenRule = new AutomaticZenRule("name", null, new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, + manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID); @@ -2941,7 +3298,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, + manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID); @@ -3172,7 +3529,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_defaultRulesOnly() { + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() { setupZenConfig(); // When there's one automatic rule active and it doesn't specify a policy, test that the @@ -3205,12 +3563,39 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_customPolicyOnly() { + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() { + setupZenConfig(); + + // When there's one automatic rule active and it doesn't specify a policy, test that the + // resulting consolidated policy is one that matches the default *device* settings. + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, // null policy + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable the rule + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // inspect the consolidated policy, which should match the device default settings. + assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy)) + .isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + } + + @Test + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() { setupZenConfig(); // when there's only one automatic rule active and it has a custom policy, make sure that's - // what the consolidated policy reflects whether or not it's stricter than what the default - // would specify. + // what the consolidated policy reflects whether or not it's stricter than what the global + // config would specify. ZenPolicy customPolicy = new ZenPolicy.Builder() .allowAlarms(true) // more lenient than default .allowMedia(true) // more lenient than default @@ -3249,7 +3634,51 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_defaultAndCustomActive() { + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() { + setupZenConfig(); + + // when there's only one automatic rule active and it has a custom policy, make sure that's + // what the consolidated policy reflects whether or not it's stricter than what the default + // would specify. + ZenPolicy customPolicy = new ZenPolicy.Builder() + .allowSystem(true) // more lenient than default + .allowRepeatCallers(false) // more restrictive than default + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default + .showFullScreenIntent(true) // more lenient + .showBadges(false) // more restrictive + .build(); + + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + customPolicy, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable the rule; this will update the consolidated policy + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // since this is the only active rule, the consolidated policy should match the custom + // policy for every field specified, and take default values for unspecified things + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.showFullScreenIntents()).isTrue(); // custom + } + + @Test + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() { setupZenConfig(); // when there are two rules active, one inheriting the default policy and one setting its @@ -3309,6 +3738,68 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() { + setupZenConfig(); + + // when there are two rules active, one inheriting the default policy and one setting its + // own custom policy, they should be merged to form the most restrictive combination. + + // rule 1: no custom policy + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable rule 1 + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // custom policy for rule 2 + ZenPolicy customPolicy = new ZenPolicy.Builder() + .allowAlarms(false) // more restrictive than default + .allowSystem(true) // more lenient than default + .allowRepeatCallers(false) // more restrictive than default + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default + .showBadges(false) // more restrictive + .showPeeking(true) // more lenient + .build(); + + AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + customPolicy, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable rule 2; this will update the consolidated policy + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // now both rules should be on, and the consolidated policy should reflect the most + // restrictive option of each of the two + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()) + .isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse(); // default stricter + } + + @Test public void testUpdateConsolidatedPolicy_allowChannels() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); setupZenConfig(); @@ -3372,7 +3863,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, new ComponentName(CUSTOM_PKG_NAME, "cls"), Uri.parse("priority"), - new ZenPolicy.Builder().allowMedia(true).build(), + new ZenPolicy.Builder() + .allowMedia(true) + .allowSystem(true) + .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRuleWithPriority, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); @@ -3394,10 +3888,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); // Consolidated Policy should be default + rule1. - assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule - assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default - assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // priority rule + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default @@ -3408,7 +3902,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void zenRuleToAutomaticZenRule_allFields() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {OWNER.getPackageName()}); + new String[]{OWNER.getPackageName()}); ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = CONFIG_ACTIVITY; @@ -3452,7 +3946,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void automaticZenRuleToZenRule_allFields() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {OWNER.getPackageName()}); + new String[]{OWNER.getPackageName()}); AutomaticZenRule azr = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setEnabled(true) @@ -3478,7 +3972,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(CONDITION_ID, storedRule.conditionId); assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode); assertEquals(ENABLED, storedRule.enabled); - assertEquals(POLICY, storedRule.zenPolicy); + assertEquals(mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY), + storedRule.zenPolicy); assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity); assertEquals(TYPE, storedRule.type); assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation); @@ -3561,12 +4056,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Modifies the zen policy and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) - .allowPriorityChannels(true) + .allowPriorityChannels(false) .build(); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects()) - .setShouldDisplayGrayscale(true) - .build(); + .setShouldDisplayGrayscale(true) + .build(); AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(policy) @@ -3580,7 +4075,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -3770,9 +4266,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) public void updateAutomaticZenRule_nullPolicyUpdate() { - // Adds a starting rule with empty zen policies and device effects + // Adds a starting rule with set zen policy and empty device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) - .setZenPolicy(new ZenPolicy.Builder().build()) + .setZenPolicy(POLICY) .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), @@ -3790,8 +4286,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); - // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null. - assertThat(rule.getZenPolicy()).isNull(); + // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged + // (equivalent to the provided policy, with additional fields filled in with defaults). + assertThat(rule.getZenPolicy()).isEqualTo( + mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY)); } @Test @@ -3847,13 +4345,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(storedRule.canBeUpdatedByApp()).isFalse(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM - | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT - | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS - | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK - | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT + | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM + | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT + | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS + | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK + | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT ); } @@ -4016,6 +4514,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { final int[] actualStatus = new int[2]; ZenModeHelper.Callback callback = new ZenModeHelper.Callback() { int i = 0; + @Override void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) { if (Objects.equals(createdId, id)) { @@ -4056,6 +4555,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { final int[] actualStatus = new int[2]; ZenModeHelper.Callback callback = new ZenModeHelper.Callback() { int i = 0; + @Override void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) { if (Objects.equals(createdId, id)) { @@ -4202,6 +4702,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()), eq(UPDATE_ORIGIN_APP)); } + @Test public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); @@ -4607,7 +5108,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, - null, true)); + mZenModeHelper.mConfig.toZenPolicy(), // copy of global config + true)); } @Test @@ -4626,7 +5128,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_METADATA) .containsExactly( - expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true)); + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, + mZenModeHelper.mConfig.toZenPolicy(), // copy of global config + true)); } @Test @@ -4820,6 +5324,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + // Store this for checking later. + ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder( + mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build(); + // From user, update that rule's policy. AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() @@ -4839,7 +5347,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS, - userUpdateZenPolicy, + // the final policy for the rule should contain the user's update + // overlaid on top of the original existing policy. + originalEffectiveZenPolicy.overwrittenWith(userUpdateZenPolicy), /* conditionActive= */ null)); } @@ -4854,6 +5364,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + // Store this for checking later. + ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder( + mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build(); + // From user, update something in that rule, but not the ZenPolicy. AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) @@ -4873,7 +5387,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowPriorityChannels(true) .build(); assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy) - .isEqualTo(appsSecondZenPolicy); + .isEqualTo(originalEffectiveZenPolicy.overwrittenWith(appsSecondZenPolicy)); } @Test @@ -4905,13 +5419,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() { + public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.allowCalls = true; + mZenModeHelper.mConfig.allowConversations = false; + // Implicit rule will get the global policy at the time of rule creation. mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_ALARMS); - mZenModeHelper.mConfig.allowCalls = true; - mZenModeHelper.mConfig.allowConversations = false; + + // If the policy then changes afterwards, we should keep the snapshotted version. + mZenModeHelper.mConfig.allowCalls = false; Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -4952,7 +5470,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { p.recycle(); } }, - "Ignoring timestamp and userModifiedFields"); + "Ignoring timestamp and userModifiedFields"); private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -4962,7 +5480,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { if (conditionActive != null) { rule.condition = conditionActive ? new Condition(rule.conditionId, - mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE) + mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE) : new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_deactivated), STATE_FALSE); @@ -5030,8 +5548,35 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber()); } + private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) { + if (!Flags.modesApi()) { + checkDndProtoMatchesSetupZenConfig(dndProto); + return; + } + + // When modes_api flag is on, the default zen config is the device defaults. + assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getReminders().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getCalls().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowCallsFrom().getNumber()).isEqualTo(PEOPLE_STARRED); + assertThat(dndProto.getMessages().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED); + assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getStatusBar().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW); + } + private static void withoutWtfCrash(Runnable test) { - Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {}); + Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> { + }); try { test.run(); } finally { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index 4ed55df7775c..57e11328d5e1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -269,6 +269,78 @@ public class ZenPolicyTest extends UiServiceTestCase { } @Test + public void testZenPolicyOverwrite_allUnsetPolicies() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + + ZenPolicy.Builder builder = new ZenPolicy.Builder(); + ZenPolicy unset = builder.build(); + + builder.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS); + builder.allowMedia(false); + builder.allowEvents(true); + builder.showFullScreenIntent(false); + builder.showInNotificationList(false); + ZenPolicy set = builder.build(); + + ZenPolicy overwritten = set.overwrittenWith(unset); + assertThat(overwritten).isEqualTo(set); + + // should actually work the other way too. + ZenPolicy overwrittenWithSet = unset.overwrittenWith(set); + assertThat(overwrittenWithSet).isEqualTo(set); + } + + @Test + public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + + ZenPolicy p1 = new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) + .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED) + .allowMedia(false) + .showBadges(true) + .build(); + + ZenPolicy p2 = new ZenPolicy.Builder() + .allowRepeatCallers(false) + .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT) + .allowMessages(ZenPolicy.PEOPLE_TYPE_NONE) + .showBadges(false) + .showPeeking(true) + .build(); + + // when p1 is overwritten with p2, all values from p2 win regardless of strictness, and + // remaining fields take values from p1. + ZenPolicy p1OverwrittenWithP2 = p1.overwrittenWith(p2); + assertThat(p1OverwrittenWithP2.getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1 + assertThat(p1OverwrittenWithP2.getPriorityMessageSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE); // from p2 + assertThat(p1OverwrittenWithP2.getPriorityCategoryRepeatCallers()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p1OverwrittenWithP2.getPriorityCategoryMedia()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1 + assertThat(p1OverwrittenWithP2.getVisualEffectBadge()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p1OverwrittenWithP2.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2 + + ZenPolicy p2OverwrittenWithP1 = p2.overwrittenWith(p1); + assertThat(p2OverwrittenWithP1.getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1 + assertThat(p2OverwrittenWithP1.getPriorityMessageSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED); // from p1 + assertThat(p2OverwrittenWithP1.getPriorityCategoryRepeatCallers()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p2OverwrittenWithP1.getPriorityCategoryMedia()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1 + assertThat(p2OverwrittenWithP1.getVisualEffectBadge()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p1 + assertThat(p2OverwrittenWithP1.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2 + } + + @Test public void testZenPolicyMessagesInvalid() { ZenPolicy.Builder builder = new ZenPolicy.Builder(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 1c57623442c5..29faed195b4e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -132,8 +132,10 @@ import org.junit.runner.RunWith; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -563,6 +565,86 @@ public class ActivityStarterTests extends WindowTestsBase { return Pair.create(splitPrimaryActivity, splitSecondActivity); } + /** + * This test ensures that if the intent is being delivered to a desktop mode unfocused task + * while it is already on top, reports it as delivering to top. + */ + @Test + public void testDesktopModeDeliverToTop() { + final ActivityStarter starter = prepareStarter( + FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, + false /* mockGetRootTask */); + final List<ActivityRecord> activities = createActivitiesInDesktopMode(); + + // Set focus back to the first task. + activities.get(0).moveFocusableActivityToTop("testDesktopModeDeliverToTop"); + + // Start activity and delivered new intent. + starter.getIntent().setComponent(activities.get(3).mActivityComponent); + doReturn(activities.get(3)).when(mRootWindowContainer).findTask(any(), any()); + final int result = starter.setReason("testDesktopModeDeliverToTop").execute(); + + // Ensure result is delivering intent to top. + assertEquals(START_DELIVERED_TO_TOP, result); + } + + /** + * This test ensures that if the intent is being delivered to a desktop mode unfocused task + * reports it is brought to front instead of delivering to top. + */ + @Test + public void testDesktopModeTaskToFront() { + final ActivityStarter starter = prepareStarter( + FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, false); + final List<ActivityRecord> activities = createActivitiesInDesktopMode(); + final ActivityRecord desktopModeFocusActivity = activities.get(0); + final ActivityRecord desktopModeReusableActivity = activities.get(1); + final ActivityRecord desktopModeTopActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .setParentTask(desktopModeReusableActivity.getRootTask()).build(); + assertTrue(desktopModeTopActivity.inMultiWindowMode()); + + // Let first stack has focus. + desktopModeFocusActivity.moveFocusableActivityToTop("testDesktopModeTaskToFront"); + + // Start activity and delivered new intent. + starter.getIntent().setComponent(desktopModeReusableActivity.mActivityComponent); + doReturn(desktopModeReusableActivity).when(mRootWindowContainer).findTask(any(), any()); + final int result = starter.setReason("testDesktopModeMoveToFront").execute(); + + // Ensure result is moving task to front. + assertEquals(START_TASK_TO_FRONT, result); + } + + /** Returns 4 activities. */ + private List<ActivityRecord> createActivitiesInDesktopMode() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + List<ActivityRecord> activityRecords = new ArrayList<>(); + + for (int i = 0; i < 4; i++) { + Rect bounds = new Rect(desktopOrganizer.getDefaultDesktopTaskBounds()); + bounds.offset(20 * i, 20 * i); + desktopOrganizer.createTask(bounds); + } + + for (int i = 0; i < 4; i++) { + activityRecords.add(new TaskBuilder(mSupervisor) + .setParentTask(desktopOrganizer.mTasks.get(i)) + .setCreateActivity(true) + .build() + .getTopMostActivity()); + } + + for (int i = 0; i < 4; i++) { + activityRecords.get(i).setVisibleRequested(true); + } + + for (int i = 0; i < 4; i++) { + assertEquals(desktopOrganizer.mTasks.get(i), activityRecords.get(i).getRootTask()); + } + + return activityRecords; + } + @Test public void testMoveVisibleTaskToFront() { final ActivityRecord activity = new TaskBuilder(mSupervisor) diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 402cbccbca01..c44be7b9db51 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -56,6 +56,7 @@ import android.os.Bundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; import android.view.WindowManager; import android.window.BackAnimationAdapter; @@ -69,6 +70,7 @@ import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -81,6 +83,12 @@ import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +/** + * Tests for the {@link BackNavigationController} class. + * + * Build/Install/Run: + * atest WmTests:BackNavigationControllerTests + */ @Presubmit @RunWith(WindowTestRunner.class) public class BackNavigationControllerTests extends WindowTestsBase { @@ -623,6 +631,22 @@ public class BackNavigationControllerTests extends WindowTestsBase { 0, navigationObserver.getCount()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG) + public void testAdjacentFocusInActivityEmbedding() { + Task task = createTask(mDefaultDisplay); + TaskFragment primary = createTaskFragmentWithActivity(task); + TaskFragment secondary = createTaskFragmentWithActivity(task); + primary.setAdjacentTaskFragment(secondary); + secondary.setAdjacentTaskFragment(primary); + + WindowState windowState = mock(WindowState.class); + doReturn(windowState).when(mWm).getFocusedWindowLocked(); + doReturn(primary).when(windowState).getTaskFragment(); + + startBackNavigation(); + verify(mWm).moveFocusToAdjacentWindow(any(), anyInt()); + } /** * Test with diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 782d89cdcd29..95850ac2f3b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2131,8 +2131,8 @@ public class DisplayContentTests extends WindowTestsBase { // Once transition starts, rotation is applied and transition shows DC rotating. testPlayer.startTransition(); waitUntilHandlersIdle(); - verify(activity1).ensureActivityConfiguration(anyBoolean(), anyBoolean()); - verify(activity2).ensureActivityConfiguration(anyBoolean(), anyBoolean()); + verify(activity1).ensureActivityConfiguration(anyBoolean()); + verify(activity2).ensureActivityConfiguration(anyBoolean()); assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation()); assertNotNull(testPlayer.mLastReady); assertTrue(testPlayer.mController.isPlaying()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index be96e60917a3..9e00f927a568 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -283,12 +283,12 @@ public class DisplayPolicyTests extends WindowTestsBase { policy.screenTurnedOff(); policy.setAwake(false); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.screenTurnedOff(); assertFalse(wpc.isShowingUiWhileDozing()); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.setAwake(true); assertFalse(wpc.isShowingUiWhileDozing()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 5518c604446d..db58ea3bdccd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1020,8 +1020,17 @@ public class SizeCompatTests extends WindowTestsBase { // Activity is sandboxed due to fixed aspect ratio. assertActivityMaxBoundsSandboxed(); + // Prepare the states for verifying relaunching after changing orientation. + mActivity.finishRelaunching(); + mActivity.setState(RESUMED, "testFixedAspectRatioOrientationChangeOrientation"); + mActivity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(), + mActivity.getConfiguration()); + // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); + assertTrue(mActivity.isRelaunching()); + assertTrue(mActivity.mLetterboxUiController + .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); assertEquals(originalBounds.width(), mActivity.getBounds().height()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 45ecc3f762ec..00ecd008cde7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -215,7 +215,7 @@ class TestDisplayContent extends DisplayContent { doReturn(false).when(newDisplay).supportsSystemDecorations(); } // Update the display policy to make the screen fully turned on so animation is allowed - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 114b9c3a68f2..a0bafb64090f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -20,6 +20,7 @@ import static android.app.AppOpsManager.OP_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -133,7 +134,9 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** Common base class for window manager unit test classes. */ class WindowTestsBase extends SystemServiceTestsBase { @@ -226,7 +229,7 @@ class WindowTestsBase extends SystemServiceTestsBase { mDefaultDisplay = mWm.mRoot.getDefaultDisplay(); // Update the display policy to make the screen fully turned on so animation is allowed final DisplayPolicy displayPolicy = mDefaultDisplay.getDisplayPolicy(); - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); @@ -1892,6 +1895,55 @@ class WindowTestsBase extends SystemServiceTestsBase { } } + static class TestDesktopOrganizer extends WindowOrganizerTests.StubOrganizer { + final int mDesktopModeDefaultWidthDp = 840; + final int mDesktopModeDefaultHeightDp = 630; + final int mDesktopDensity = 284; + + final ActivityTaskManagerService mService; + final TaskDisplayArea mDefaultTDA; + List<Task> mTasks; + final DisplayContent mDisplay; + Rect mStableBounds; + + TestDesktopOrganizer(ActivityTaskManagerService service, DisplayContent display) { + mService = service; + mDefaultTDA = display.getDefaultTaskDisplayArea(); + mDisplay = display; + mService.mTaskOrganizerController.registerTaskOrganizer(this); + mTasks = new ArrayList<>(); + mStableBounds = display.getBounds(); + } + + TestDesktopOrganizer(ActivityTaskManagerService service) { + this(service, service.mTaskSupervisor.mRootWindowContainer.getDefaultDisplay()); + } + + public Task createTask(Rect bounds) { + Task task = mService.mTaskOrganizerController.createRootTask( + mDisplay, WINDOWING_MODE_FREEFORM, null); + task.setBounds(bounds); + mTasks.add(task); + spyOn(task); + return task; + } + + public Rect getDefaultDesktopTaskBounds() { + int width = (int) (mDesktopModeDefaultWidthDp * mDesktopDensity + 0.5f); + int height = (int) (mDesktopModeDefaultHeightDp * mDesktopDensity + 0.5f); + Rect outBounds = new Rect(); + + outBounds.set(0, 0, width, height); + // Center the task in stable bounds + outBounds.offset( + mStableBounds.centerX() - outBounds.centerX(), + mStableBounds.centerY() - outBounds.centerY() + ); + return outBounds; + } + + } + static TestWindowToken createTestWindowToken(int type, DisplayContent dc) { return createTestWindowToken(type, dc, false /* persistOnEmpty */); } diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index a5c6d57aed82..1bf11df7059a 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1618,14 +1618,15 @@ public class SubscriptionManager { } /** - * Register for changes to the list of active {@link SubscriptionInfo} records or to the - * individual records themselves. When a change occurs the onSubscriptionsChanged method of - * the listener will be invoked immediately if there has been a notification. The - * onSubscriptionChanged method will also be triggered once initially when calling this - * function. + * Register for changes to the list of {@link SubscriptionInfo} records or to the + * individual records (active or inactive) themselves. When a change occurs, the + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of + * the listener will be invoked immediately. The + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked + * once initially when calling this method. * * @param listener an instance of {@link OnSubscriptionsChangedListener} with - * onSubscriptionsChanged overridden. + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden. * @param executor the executor that will execute callbacks. */ public void addOnSubscriptionsChangedListener( @@ -1953,7 +1954,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public List<SubscriptionInfo> getActiveSubscriptionInfoList() { List<SubscriptionInfo> activeList = null; @@ -2010,6 +2010,9 @@ public class SubscriptionManager { * Create a new subscription manager instance that can see all subscriptions across * user profiles. * + * The permission check for accessing all subscriptions will be enforced upon calling the + * individual APIs linked below. + * * @return a SubscriptionManager that can see all subscriptions regardless its user profile * association. * @@ -2018,9 +2021,7 @@ public class SubscriptionManager { * @see UserHandle */ @FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER) - // @RequiresPermission(TODO(b/308809058)) - // The permission check for accessing all subscriptions will be enforced upon calling the - // individual APIs linked above. + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) @NonNull public SubscriptionManager createForAllUserProfiles() { return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/); } @@ -2215,7 +2216,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public int getActiveSubscriptionInfoCount() { int result = 0; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9ec5f7a77b2c..cbd552454642 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -6888,6 +6888,7 @@ public class TelephonyManager { } } + // TODO(b/316183370): replace all @code with @link in javadoc after feature is released /** * @return true if the current device is "voice capable". * <p> @@ -6901,7 +6902,10 @@ public class TelephonyManager { * PackageManager.FEATURE_TELEPHONY system feature, which is available * on any device with a telephony radio, even if the device is * data-only. - * @deprecated Replaced by {@link #isDeviceVoiceCapable()} + * @deprecated Replaced by {@code #isDeviceVoiceCapable()}. Starting from Android 15, voice + * capability may also be overridden by carriers for a given subscription. For voice capable + * device (when {@code #isDeviceVoiceCapable} return {@code true}), caller should check for + * subscription-level voice capability as well. See {@code #isDeviceVoiceCapable} for details. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @Deprecated @@ -6923,9 +6927,10 @@ public class TelephonyManager { * .FEATURE_TELEPHONY system feature, which is available on any device with a telephony * radio, even if the device is data-only. * <p> - * To check if a subscription is "voice capable", call method - * {@link SubscriptionInfo#getServiceCapabilities()} and compare the result with - * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE}. + * Starting from Android 15, voice capability may also be overridden by carrier for a given + * subscription on a voice capable device. To check if a subscription is "voice capable", + * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if + * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ @@ -6943,7 +6948,10 @@ public class TelephonyManager { * <p> * Note: Voicemail waiting sms, cell broadcasting sms, and MMS are * disabled when device doesn't support sms. - * @deprecated Replaced by {@link #isDeviceSmsCapable()} + * @deprecated Replaced by {@code #isDeviceSmsCapable()}. Starting from Android 15, SMS + * capability may also be overridden by carriers for a given subscription. For SMS capable + * device (when {@code #isDeviceSmsCapable} return {@code true}), caller should check for + * subscription-level SMS capability as well. See {@code #isDeviceSmsCapable} for details. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public boolean isSmsCapable() { @@ -6961,9 +6969,10 @@ public class TelephonyManager { * Note: Voicemail waiting SMS, cell broadcasting SMS, and MMS are * disabled when device doesn't support SMS. * <p> - * To check if a subscription is "SMS capable", call method - * {@link SubscriptionInfo#getServiceCapabilities()} and compare result with - * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_SMS}. + * Starting from Android 15, SMS capability may also be overridden by carriers for a given + * subscription on an SMS capable device. To check if a subscription is "SMS capable", + * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if + * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ @@ -13139,7 +13148,7 @@ public class TelephonyManager { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) public ServiceState getServiceStateForSubscriber(int subId) { - return getServiceStateForSubscriber(getSubId(), false, false); + return getServiceStateForSubscriber(subId, false, false); } /** @@ -18483,7 +18492,6 @@ public class TelephonyManager { /** * Get last known cell identity. - * Require appropriate permissions, otherwise throws SecurityException. * * If there is current registered network this value will be same as the registered cell * identity. If the device goes out of service the previous cell identity is cached and @@ -18495,7 +18503,7 @@ public class TelephonyManager { @SystemApi @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) @RequiresPermission(allOf = {Manifest.permission.ACCESS_FINE_LOCATION, - "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) + Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public @Nullable CellIdentity getLastKnownCellIdentity() { try { ITelephony telephony = getITelephony(); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index b05863188beb..256a4696b763 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -292,14 +292,16 @@ class InputManagerServiceTests { setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) + verify(native).setPointerIconVisibility(10, false) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(eq(false)) + verify(native).setMousePointerAccelerationEnabled(10, false) service.onDisplayRemoved(10) + verify(native).setPointerIconVisibility(10, true) verify(native).displayRemoved(eq(10)) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) - verify(native).setMousePointerAccelerationEnabled(true) + verify(native).setMousePointerAccelerationEnabled(10, true) verifyNoMoreInteractions(native) // This call should not block because the virtual mouse pointer override was never removed. @@ -315,25 +317,26 @@ class InputManagerServiceTests { localService.setPointerIconVisible(false, 10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + verify(native).setPointerIconVisibility(10, false) localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(eq(false)) + verify(native).setMousePointerAccelerationEnabled(10, false) localService.setPointerIconVisible(true, 10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) + verify(native).setPointerIconVisibility(10, true) localService.setMousePointerAccelerationEnabled(true, 10) - verify(native).setMousePointerAccelerationEnabled(eq(true)) + verify(native).setMousePointerAccelerationEnabled(10, true) - // Verify that setting properties on a different display is not propagated until the - // pointer is moved to that display. localService.setPointerIconVisible(false, 20) + verify(native).setPointerIconVisibility(20, false) localService.setMousePointerAccelerationEnabled(false, 20) + verify(native).setMousePointerAccelerationEnabled(20, false) verifyNoMoreInteractions(native) clearInvocations(native) setVirtualMousePointerDisplayIdAndVerify(20) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - verify(native).setMousePointerAccelerationEnabled(eq(false)) } @Test @@ -341,12 +344,13 @@ class InputManagerServiceTests { localService.setPointerIconVisible(false, 10) localService.setMousePointerAccelerationEnabled(false, 10) + verify(native).setPointerIconVisibility(10, false) + verify(native).setMousePointerAccelerationEnabled(10, false) verifyNoMoreInteractions(native) setVirtualMousePointerDisplayIdAndVerify(10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - verify(native).setMousePointerAccelerationEnabled(eq(false)) } @Test diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index cbdcb8869628..518183f9cd64 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -30,6 +30,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; import android.os.UserManager; @@ -146,7 +147,8 @@ public class RollbackTest { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); // Upgrade from v1 to v2, with rollbacks enabled. - Install.single(TestApp.A2).setEnableRollback().commit(); + Install.single(TestApp.A2).setEnableRollback().setRollbackImpactLevel( + PackageManager.ROLLBACK_USER_IMPACT_HIGH).commit(); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); // The app should now be available for rollback. @@ -154,6 +156,8 @@ public class RollbackTest { assertThat(available).isNotStaged(); assertThat(available).packagesContainsExactly( Rollback.from(TestApp.A2).to(TestApp.A1)); + assertThat(available.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_HIGH); // We should not have received any rollback requests yet. // TODO: Possibly flaky if, by chance, some other app on device @@ -264,6 +268,8 @@ public class RollbackTest { RollbackInfo rollbackB = waitForAvailableRollback(TestApp.B); assertThat(rollbackB).packagesContainsExactly( Rollback.from(TestApp.B2).to(TestApp.B1)); + assertThat(rollbackB.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); // Register rollback committed receiver RollbackBroadcastReceiver rollbackReceiver = new RollbackBroadcastReceiver(); |