diff options
362 files changed, 8861 insertions, 2251 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index fde139b60ca5..60be8a76e3b2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -17621,7 +17621,6 @@ package android.graphics { method public void setIntUniform(@NonNull String, int, int, int); method public void setIntUniform(@NonNull String, int, int, int, int); method public void setIntUniform(@NonNull String, @NonNull int[]); - method @FlaggedApi("com.android.graphics.hwui.flags.shader_color_space") public void setWorkingColorSpace(@Nullable android.graphics.ColorSpace); } @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode { @@ -48744,6 +48743,7 @@ package android.telephony.satellite { @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void registerStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateChangeListener); method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void unregisterStateChangeListener(@NonNull android.telephony.satellite.SatelliteStateChangeListener); + field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"; } @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public interface SatelliteStateChangeListener { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 03607d45eabb..0d5ec199b953 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2938,6 +2938,14 @@ package android.app.smartspace.uitemplatedata { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); + } + +} + package android.app.time { public final class Capabilities { @@ -3801,6 +3809,7 @@ package android.content { field public static final String SHARED_CONNECTIVITY_SERVICE = "shared_connectivity"; field public static final String SMARTSPACE_SERVICE = "smartspace"; field public static final String STATS_MANAGER = "stats"; + field @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public static final String SUPERVISION_SERVICE = "supervision"; field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network"; @@ -18584,6 +18593,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatelliteDataOptimizedApps(); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons(); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 85b65bb6605e..975c2c27cb22 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -850,6 +850,14 @@ package android.app.prediction { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method public void setSupervisionEnabled(boolean); + } + +} + package android.app.usage { public class StorageStatsManager { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 252d23f69400..ee9c64f97382 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -292,13 +292,15 @@ import java.util.function.Consumer; * to the user, it must be completely restarted and restored to its previous state.</li> * </ul> * - * <p>The following diagram shows the important state paths of an Activity. + * <p>The following diagram shows the important state paths of an activity. * The square rectangles represent callback methods you can implement to - * perform operations when the Activity moves between states. The colored - * ovals are major states the Activity can be in.</p> + * perform operations when the activity moves between states. The colored + * ovals are major states the activity can be in.</p> * - * <p><img src="../../../images/activity_lifecycle.png" - * alt="State diagram for an Android Activity Lifecycle." border="0" /></p> + * <p><img class="invert" + * style="display: block; margin: auto;" + * src="../../../images/activity_lifecycle.png" + * alt="State diagram for the Android activity lifecycle." /></p> * * <p>There are three key loops you may be interested in monitoring within your * activity: @@ -505,7 +507,7 @@ import java.util.function.Consumer; * changes.</p> * * <p>Unless you specify otherwise, a configuration change (such as a change - * in screen orientation, language, input devices, etc) will cause your + * in screen orientation, language, input devices, etc.) will cause your * current activity to be <em>destroyed</em>, going through the normal activity * lifecycle process of {@link #onPause}, * {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity @@ -1838,7 +1840,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case onDestroy() will be immediately called after {@link #onCreate} without any of the - * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc) + * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc.) * executing. * * <p><em>Derived classes must call through to the super class's @@ -2132,7 +2134,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case {@link #onStop} will be immediately called after {@link #onStart} without the - * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc) executing. + * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc.) executing. * * <p><em>Derived classes must call through to the super class's * implementation of this method. If they do not, an exception will be diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index 3214bd8f01fc..2e8031dd22fe 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -840,7 +840,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ // LINT.IfChange(write_proto) - public void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException { + public void writeToProto(ProtoOutputStream proto, long fieldId, + ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream, + TypedXmlSerializer typedXmlSerializer) throws IOException { final long token = proto.start(fieldId); proto.write(ApplicationStartInfoProto.PID, mPid); proto.write(ApplicationStartInfoProto.REAL_UID, mRealUid); @@ -850,38 +852,38 @@ public final class ApplicationStartInfo implements Parcelable { proto.write(ApplicationStartInfoProto.STARTUP_STATE, mStartupState); proto.write(ApplicationStartInfoProto.REASON, mReason); if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) { - ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream(); - ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes); - TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut); - serializer.startDocument(null, true); - serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + byteArrayOutputStream = new ByteArrayOutputStream(); + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + typedXmlSerializer = Xml.resolveSerializer(objectOutputStream); + typedXmlSerializer.startDocument(null, true); + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); for (int i = 0; i < mStartupTimestampsNs.size(); i++) { - serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); - serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + typedXmlSerializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, mStartupTimestampsNs.keyAt(i)); - serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, + typedXmlSerializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, mStartupTimestampsNs.valueAt(i)); - serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); } - serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); - serializer.endDocument(); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + typedXmlSerializer.endDocument(); proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS, - timestampsBytes.toByteArray()); - timestampsOut.close(); + byteArrayOutputStream.toByteArray()); + objectOutputStream.close(); } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { - 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(); + byteArrayOutputStream = new ByteArrayOutputStream(); + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + typedXmlSerializer = Xml.resolveSerializer(objectOutputStream); + typedXmlSerializer.startDocument(null, true); + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent.saveToXml(typedXmlSerializer); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + typedXmlSerializer.endDocument(); proto.write(ApplicationStartInfoProto.START_INTENT, - intentBytes.toByteArray()); - intentOut.close(); + byteArrayOutputStream.toByteArray()); + objectOutputStream.close(); } proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode); proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped); @@ -900,7 +902,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ // LINT.IfChange(read_proto) - public void readFromProto(ProtoInputStream proto, long fieldId) + public void readFromProto(ProtoInputStream proto, long fieldId, + ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream, + TypedXmlPullParser typedXmlPullParser) throws IOException, WireTypeMismatchException, ClassNotFoundException { final long token = proto.start(fieldId); while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -927,19 +931,21 @@ public final class ApplicationStartInfo implements Parcelable { mReason = proto.readInt(ApplicationStartInfoProto.REASON); break; case (int) ApplicationStartInfoProto.STARTUP_TIMESTAMPS: - ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes( + byteArrayInputStream = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.STARTUP_TIMESTAMPS)); - ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes); + objectInputStream = new ObjectInputStream(byteArrayInputStream); mStartupTimestampsNs = new ArrayMap<Integer, Long>(); try { - TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn); - XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); - int depth = parser.getDepth(); - while (XmlUtils.nextElementWithin(parser, depth)) { - if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) { - int key = parser.getAttributeInt(null, + typedXmlPullParser = Xml.resolvePullParser(objectInputStream); + XmlUtils.beginDocument(typedXmlPullParser, + PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + int depth = typedXmlPullParser.getDepth(); + while (XmlUtils.nextElementWithin(typedXmlPullParser, depth)) { + if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals( + typedXmlPullParser.getName())) { + int key = typedXmlPullParser.getAttributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY); - long ts = parser.getAttributeLong(null, + long ts = typedXmlPullParser.getAttributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS); mStartupTimestampsNs.put(key, ts); } @@ -947,23 +953,24 @@ public final class ApplicationStartInfo implements Parcelable { } catch (XmlPullParserException e) { // Timestamps lost } - timestampsIn.close(); + objectInputStream.close(); break; case (int) ApplicationStartInfoProto.START_TYPE: mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); break; case (int) ApplicationStartInfoProto.START_INTENT: - ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes( + byteArrayInputStream = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.START_INTENT)); - ObjectInputStream intentIn = new ObjectInputStream(intentBytes); + objectInputStream = new ObjectInputStream(byteArrayInputStream); try { - TypedXmlPullParser parser = Xml.resolvePullParser(intentIn); - XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT); - mStartIntent = Intent.restoreFromXml(parser); + typedXmlPullParser = Xml.resolvePullParser(objectInputStream); + XmlUtils.beginDocument(typedXmlPullParser, + PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent = Intent.restoreFromXml(typedXmlPullParser); } catch (XmlPullParserException e) { // Intent lost } - intentIn.close(); + objectInputStream.close(); break; case (int) ApplicationStartInfoProto.LAUNCH_MODE: mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 0ca4a329fd5a..5dca1c70a2e6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6002,7 +6002,7 @@ public class Notification implements Parcelable // HUNS, which use a different layout that already accounts for that). Templates that // have content that will be displayed under the small icon also use a different margin. if (Flags.notificationsRedesignTemplates() - && !p.mHeaderless && !p.mHasContentInLeftMargin) { + && !p.mHeaderless && !p.mSkipTopLineAlignment) { int margin = getContentMarginTop(mContext, R.dimen.notification_2025_content_margin_top); contentView.setViewLayoutMargin(R.id.notification_main_column, @@ -6594,13 +6594,8 @@ public class Notification implements Parcelable int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin); // Spacing between the text lines, scaling with the font size (originally in sp) int spacing = resources.getDimensionPixelSize(spacingRes); - // Size of the text in the notification top line (originally in sp) - int[] textSizeAttr = new int[] { android.R.attr.textSize }; - TypedArray typedArray = context.obtainStyledAttributes( - R.style.TextAppearance_DeviceDefault_Notification_Info, textSizeAttr); - int textSize = typedArray.getDimensionPixelSize(0 /* index */, -1 /* default */); - typedArray.recycle(); + int textSize = resources.getDimensionPixelSize(R.dimen.notification_subtext_size); // Adding up all the values as pixels return notifMargin + spacing + textSize; @@ -9503,7 +9498,7 @@ public class Notification implements Parcelable .hideLeftIcon(isOneToOne) .hideRightIcon(hideRightIcons || isOneToOne) .headerTextSecondary(isHeaderless ? null : conversationTitle) - .hasContentInLeftMargin(true); + .skipTopLineAlignment(true); RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( isConversationLayout ? mBuilder.getConversationLayoutResource() @@ -14681,7 +14676,7 @@ public class Notification implements Parcelable Icon mPromotedPicture; boolean mCallStyleActions; boolean mAllowTextWithProgress; - boolean mHasContentInLeftMargin; + boolean mSkipTopLineAlignment; int mTitleViewId; int mTextViewId; @Nullable CharSequence mTitle; @@ -14707,7 +14702,7 @@ public class Notification implements Parcelable mPromotedPicture = null; mCallStyleActions = false; mAllowTextWithProgress = false; - mHasContentInLeftMargin = false; + mSkipTopLineAlignment = false; mTitleViewId = R.id.title; mTextViewId = R.id.text; mTitle = null; @@ -14774,8 +14769,8 @@ public class Notification implements Parcelable return this; } - public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) { - mHasContentInLeftMargin = hasContentInLeftMargin; + public StandardTemplateParams skipTopLineAlignment(boolean skipTopLineAlignment) { + mSkipTopLineAlignment = skipTopLineAlignment; return this; } diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index bb4f556532f7..8e6b88c66408 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -83,6 +83,13 @@ flag { } flag { + name: "modes_cleanup_implicit" + namespace: "systemui" + description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI" + bug: "394087495" +} + +flag { name: "api_tvextender" is_exported: true namespace: "systemui" diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index e583302e4d3b..2f67a8abcd17 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -16,11 +16,14 @@ package android.app.supervision; +import android.content.Intent; + /** * Internal IPC interface to the supervision service. * {@hide} */ interface ISupervisionManager { + Intent createConfirmSupervisionCredentialsIntent(); boolean isSupervisionEnabledForUser(int userId); void setSupervisionEnabledForUser(int userId, boolean enabled); String getActiveSupervisionAppPackage(int userId); diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index d30705536045..0270edf080a9 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -16,13 +16,22 @@ package android.app.supervision; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; + +import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; +import android.app.supervision.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.Intent; import android.os.RemoteException; /** @@ -31,6 +40,8 @@ import android.os.RemoteException; * @hide */ @SystemService(Context.SUPERVISION_SERVICE) +@SystemApi +@FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) public class SupervisionManager { private final Context mContext; @Nullable private final ISupervisionManager mService; @@ -47,7 +58,8 @@ public class SupervisionManager { * * @hide */ - public static final String ACTION_ENABLE_SUPERVISION = "android.app.action.ENABLE_SUPERVISION"; + public static final String ACTION_ENABLE_SUPERVISION = + "android.app.supervision.action.ENABLE_SUPERVISION"; /** * Activity action: ask the human user to disable supervision for this user. Only the app that @@ -62,7 +74,7 @@ public class SupervisionManager { * @hide */ public static final String ACTION_DISABLE_SUPERVISION = - "android.app.action.DISABLE_SUPERVISION"; + "android.app.supervision.action.DISABLE_SUPERVISION"; /** @hide */ @UnsupportedAppUsage @@ -72,11 +84,46 @@ public class SupervisionManager { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this API + * is called, the launched activity still need to perform validity checks as the supervision + * state can change when the activity is launched. A null intent is returned if supervision is + * disabled at the time of this API call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + * + * @hide + */ + @RequiresPermission(value = android.Manifest.permission.QUERY_USERS) + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + if (mService != null) { + try { + Intent result = mService.createConfirmSupervisionCredentialsIntent(); + if (result != null) { + result.prepareToEnterProcess( + Intent.LOCAL_FLAG_FROM_SYSTEM, mContext.getAttributionSource()); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + + /** * Returns whether the device is supervised. * * @hide */ - @UserHandleAware + @SystemApi + @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabled() { return isSupervisionEnabledForUser(mContext.getUserId()); } @@ -84,14 +131,10 @@ public class SupervisionManager { /** * Returns whether the device is supervised. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { if (mService != null) { try { @@ -108,7 +151,8 @@ public class SupervisionManager { * * @hide */ - @UserHandleAware + @TestApi + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabled(boolean enabled) { setSupervisionEnabledForUser(mContext.getUserId(), enabled); } @@ -116,14 +160,9 @@ public class SupervisionManager { /** * Sets whether the device is supervised for a given user. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { if (mService != null) { try { diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index 232883cbfe00..94de03877fd7 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -64,3 +64,11 @@ flag { description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point" bug: "390500290" } + +flag { + name: "supervision_manager_apis" + is_exported: true + namespace: "supervision" + description: "Flag that enables system APIs in Supervision Manager" + bug: "382034839" +} diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index fcdb02ab5da2..ba1473cf5ed7 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -120,6 +120,16 @@ flag { } flag { + name: "correct_virtual_display_power_state" + namespace: "virtual_devices" + description: "Fix the virtual display power state" + bug: "371125136" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "vdm_settings" namespace: "virtual_devices" description: "Show virtual devices in Settings" diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 3391e79b2ae4..55d78f9b8c48 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -17,8 +17,8 @@ package android.content; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; -import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; +import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.security.Flags.FLAG_SECURE_LOCKDOWN; import android.annotation.AttrRes; @@ -6858,6 +6858,8 @@ public abstract class Context { * @see android.app.supervision.SupervisionManager * @hide */ + @SystemApi + @FlaggedApi(android.app.supervision.flags.Flags.FLAG_SUPERVISION_MANAGER_APIS) public static final String SUPERVISION_SERVICE = "supervision"; /** diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 40c532498fbc..36fa05905814 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -29,6 +29,8 @@ import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.pm.pkg.component.AconfigFlags; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.util.XmlUtils; import dalvik.annotation.optimization.CriticalNative; @@ -50,6 +52,7 @@ import java.io.Reader; @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class XmlBlock implements AutoCloseable { private static final boolean DEBUG=false; + public static final String ANDROID_RESOURCES = "http://schemas.android.com/apk/res/android"; @UnsupportedAppUsage public XmlBlock(byte[] data) { @@ -343,6 +346,23 @@ public final class XmlBlock implements AutoCloseable { if (ev == ERROR_BAD_DOCUMENT) { throw new XmlPullParserException("Corrupt XML binary file"); } + if (useLayoutReadwrite() && ev == START_TAG) { + AconfigFlags flags = ParsingPackageUtils.getAconfigFlags(); + if (flags.skipCurrentElement(/* pkg= */ null, this)) { + int depth = 1; + while (depth > 0) { + int ev2 = nativeNext(mParseState); + if (ev2 == ERROR_BAD_DOCUMENT) { + throw new XmlPullParserException("Corrupt XML binary file"); + } else if (ev2 == START_TAG) { + depth++; + } else if (ev2 == END_TAG) { + depth--; + } + } + return next(); + } + } if (mDecNextDepth) { mDepth--; mDecNextDepth = false; @@ -368,6 +388,18 @@ public final class XmlBlock implements AutoCloseable { } return ev; } + + // Until ravenwood supports AconfigFlags, we just don't do layoutReadwriteFlags(). + @android.ravenwood.annotation.RavenwoodReplace( + bug = 396458006, blockedBy = AconfigFlags.class) + private static boolean useLayoutReadwrite() { + return Flags.layoutReadwriteFlags(); + } + + private static boolean useLayoutReadwrite$ravenwood() { + return false; + } + public void require(int type, String namespace, String name) throws XmlPullParserException,IOException { if (type != getEventType() || (namespace != null && !namespace.equals( getNamespace () ) ) diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 3d4b8854b01f..7c82abe083c2 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -386,7 +386,7 @@ public class InputSettings { */ public static boolean isTouchpadAccelerationEnabled(@NonNull Context context) { if (!isPointerAccelerationFeatureFlagEnabled()) { - return false; + return true; } return Settings.System.getIntForUser(context.getContentResolver(), @@ -833,7 +833,7 @@ public class InputSettings { */ public static boolean isMousePointerAccelerationEnabled(@NonNull Context context) { if (!isPointerAccelerationFeatureFlagEnabled()) { - return false; + return true; } return Settings.System.getIntForUser(context.getContentResolver(), diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 4011574da879..6f94c1b2d274 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -309,6 +309,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin"; private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects"; private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride"; + private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation"; private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale"; private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY = @@ -1187,11 +1188,7 @@ public class ZenModeConfig implements Parcelable { rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); - Long deletionInstant = tryParseLong( - parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); - if (deletionInstant != null) { - rt.deletionInstant = Instant.ofEpochMilli(deletionInstant); - } + rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null); if (Flags.modesUi()) { rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, ORIGIN_UNKNOWN); @@ -1199,6 +1196,9 @@ public class ZenModeConfig implements Parcelable { RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE, ZenRule.OVERRIDE_NONE); + if (Flags.modesCleanupImplicit()) { + rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null); + } } return rt; @@ -1249,10 +1249,7 @@ public class ZenModeConfig implements Parcelable { out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, rule.zenDeviceEffectsUserModifiedFields); - if (rule.deletionInstant != null) { - out.attributeLong(null, RULE_ATT_DELETION_INSTANT, - rule.deletionInstant.toEpochMilli()); - } + writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant); if (Flags.modesUi()) { out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin); out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, @@ -1260,6 +1257,16 @@ public class ZenModeConfig implements Parcelable { if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) { out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride); } + if (Flags.modesCleanupImplicit()) { + writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation); + } + } + } + + private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att, + @Nullable Instant instant) throws IOException { + if (instant != null) { + out.attributeLong(null, att, instant.toEpochMilli()); } } @@ -1600,6 +1607,19 @@ public class ZenModeConfig implements Parcelable { return values; } + @Nullable + private static Instant safeInstant(TypedXmlPullParser parser, String att, + @Nullable Instant defValue) { + final String strValue = parser.getAttributeValue(null, att); + if (!TextUtils.isEmpty(strValue)) { + Long longValue = tryParseLong(strValue, null); + if (longValue != null) { + return Instant.ofEpochMilli(longValue); + } + } + return defValue; + } + @Override public int describeContents() { return 0; @@ -2598,6 +2618,18 @@ public class ZenModeConfig implements Parcelable { @ConditionOverride int conditionOverride = OVERRIDE_NONE; + /** + * Last time at which the rule was activated (for any reason, including overrides). + * If {@code null}, the rule has never been activated since its creation. + * + * <p>Note that this was previously untracked, so it will also be {@code null} for rules + * created before we started tracking and never activated since -- make sure to account for + * it, for example by falling back to {@link #creationTime} in logic involving this field. + */ + @Nullable + @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT) + public Instant lastActivation; + public ZenRule() { } public ZenRule(Parcel source) { @@ -2635,24 +2667,29 @@ public class ZenModeConfig implements Parcelable { disabledOrigin = source.readInt(); legacySuppressedEffects = source.readInt(); conditionOverride = source.readInt(); + if (Flags.modesCleanupImplicit()) { + if (source.readInt() == 1) { + lastActivation = Instant.ofEpochMilli(source.readLong()); + } + } } } /** - * Whether this ZenRule can be updated by an app. In general, rules that have been - * customized by the user cannot be further updated by an app, with some exceptions: + * Whether this ZenRule has been customized by the user in any way. + + * <p>In general, rules that have been customized by the user cannot be further updated by + * an app, with some exceptions: * <ul> * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. * <li>Name, if the name was not specifically modified by the user (to support language * switches). * </ul> */ - public boolean canBeUpdatedByApp() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return userModifiedFields == 0 - && zenPolicyUserModifiedFields == 0 - && zenDeviceEffectsUserModifiedFields == 0; + public boolean isUserModified() { + return userModifiedFields != 0 + || zenPolicyUserModifiedFields != 0 + || zenDeviceEffectsUserModifiedFields != 0; } @Override @@ -2708,6 +2745,14 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(disabledOrigin); dest.writeInt(legacySuppressedEffects); dest.writeInt(conditionOverride); + if (Flags.modesCleanupImplicit()) { + if (lastActivation != null) { + dest.writeInt(1); + dest.writeLong(lastActivation.toEpochMilli()); + } else { + dest.writeInt(0); + } + } } } @@ -2760,6 +2805,9 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { sb.append(",disabledOrigin=").append(disabledOrigin); sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects); + if (Flags.modesCleanupImplicit()) { + sb.append(",lastActivation=").append(lastActivation); + } } return sb.append(']').toString(); @@ -2838,6 +2886,10 @@ public class ZenModeConfig implements Parcelable { && other.disabledOrigin == disabledOrigin && other.legacySuppressedEffects == legacySuppressedEffects && other.conditionOverride == conditionOverride; + if (Flags.modesCleanupImplicit()) { + finalEquals = finalEquals + && Objects.equals(other.lastActivation, lastActivation); + } } return finalEquals; @@ -2846,13 +2898,23 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { if (Flags.modesUi()) { - return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, - component, configurationActivity, pkg, id, enabler, zenPolicy, - zenDeviceEffects, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields, - zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant, disabledOrigin, legacySuppressedEffects, - conditionOverride); + if (Flags.modesCleanupImplicit()) { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride, lastActivation); + } else { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); + } } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index ca0959af3ff8..231aa6816908 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1599,7 +1599,6 @@ public final class Display { mGlobal.registerDisplayListener(toRegister, executor, DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, ActivityThread.currentPackageName()); diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 73cd5ecd39ef..df680c054f56 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -32,7 +32,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; -import android.util.TypedValue; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.RemoteViews; @@ -266,20 +265,14 @@ public class NotificationHeaderView extends RelativeLayout { ? R.style.TextAppearance_DeviceDefault_Notification_Title : R.style.TextAppearance_DeviceDefault_Notification_Info; // Most of the time, we're showing text in the minimized state - if (findViewById(R.id.header_text) instanceof TextView headerText) { - headerText.setTextAppearance(styleResId); - if (notificationsRedesignTemplates()) { - // TODO: b/378660052 - When inlining the redesign flag, this should be updated - // directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to - // override it here. - float textSize = getContext().getResources().getDimension( - R.dimen.notification_2025_title_text_size); - headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - } + View headerText = findViewById(R.id.header_text); + if (headerText instanceof TextView) { + ((TextView) headerText).setTextAppearance(styleResId); } // If there's no summary or text, we show the app name instead of nothing - if (findViewById(R.id.app_name_text) instanceof TextView appNameText) { - appNameText.setTextAppearance(styleResId); + View appNameText = findViewById(R.id.app_name_text); + if (appNameText instanceof TextView) { + ((TextView) appNameText).setTextAppearance(styleResId); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index de3e45b8ebde..6b6147a3749d 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -118,6 +118,7 @@ import static android.view.flags.Flags.addSchandleToVriSurface; import static android.view.flags.Flags.disableDrawWakeLock; import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix; +import static android.view.flags.Flags.toolkitFrameRateDebug; import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly; import static android.view.flags.Flags.toolkitFrameRateTouchBoost25q1; import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly; @@ -1227,6 +1228,7 @@ public final class ViewRootImpl implements ViewParent, com.android.graphics.surfaceflinger.flags.Flags.vrrBugfix24q4(); private static final boolean sEnableVrr = ViewProperties.vrr_enabled().orElse(true); private static final boolean sToolkitInitialTouchBoostFlagValue = toolkitInitialTouchBoost(); + private static boolean sToolkitFrameRateDebugFlagValue = toolkitFrameRateDebug(); static { sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); @@ -13139,6 +13141,11 @@ public final class ViewRootImpl implements ViewParent, if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, frameRateCategory, false).applyAsyncUnsafe(); + + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRateCategory '" + + categoryToString(frameRateCategory) + "'"); + } } mLastPreferredFrameRateCategory = frameRateCategory; } @@ -13201,8 +13208,15 @@ public final class ViewRootImpl implements ViewParent, if (preferredFrameRate > 0) { mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, mFrameRateCompatibility); + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRate '" + + preferredFrameRate + "'"); + } } else { mFrameRateTransaction.clearFrameRate(mSurfaceControl); + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRate 0 Hz"); + } } mFrameRateTransaction.applyAsyncUnsafe(); } @@ -13256,6 +13270,12 @@ public final class ViewRootImpl implements ViewParent, // mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName(); } mDrawnThisFrame = true; + + if (sToolkitFrameRateDebugFlagValue) { + String viewName = view == null ? "-" : view.getClass().getSimpleName(); + Log.v(mTag, "### View: " + viewName + " votes '" + + categoryToString(frameRateCategory) + "'"); + } } /** diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 3bc2205f8e1c..18fa0f353f36 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -143,4 +143,11 @@ flag { namespace: "toolkit" description: "Feature flag to update initial touch boost logic" bug: "393004744" +} + +flag { + name: "toolkit_frame_rate_debug" + namespace: "toolkit" + description: "Feature flag to ennable ARR debug message" + bug: "394614443" }
\ No newline at end of file diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0f5476f58f74..0a5c14e3a08b 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -8565,12 +8565,16 @@ public class RemoteViews implements Parcelable, Filter { return context; } try { - // Use PackageManager as the source of truth for application information, rather - // than the parceled ApplicationInfo provided by the app. - ApplicationInfo sanitizedApplication = - context.getPackageManager().getApplicationInfoAsUser( - mApplication.packageName, 0, - UserHandle.getUserId(mApplication.uid)); + ApplicationInfo sanitizedApplication = mApplication; + try { + // Use PackageManager as the source of truth for application information, rather + // than the parceled ApplicationInfo provided by the app. + sanitizedApplication = context.getPackageManager().getApplicationInfoAsUser( + mApplication.packageName, 0, UserHandle.getUserId(mApplication.uid)); + } catch(SecurityException se) { + Log.d(LOG_TAG, "Unable to fetch appInfo for " + mApplication.packageName); + } + Context applicationContext = context.createApplicationContext( sanitizedApplication, Context.CONTEXT_RESTRICTED); diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index d43469fa76ca..ca1017b72854 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -96,6 +96,7 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true), ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true), + ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false), ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index e358540afe6b..79120b22c205 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -79,6 +79,16 @@ flag { } flag { + name: "enable_drag_resize_set_up_in_bg_thread" + namespace: "lse_desktop_experience" + description: "Enables setting up the drag-resize input listener in a bg thread" + bug: "396445663" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_windowing_wallpaper_activity" namespace: "lse_desktop_experience" description: "Enables desktop wallpaper activity to show wallpaper in the desktop mode" diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 2ba6bc4912c3..b679688959b1 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -664,14 +664,16 @@ static void android_media_AudioSystem_vol_range_init_req_callback() static jint android_media_AudioSystem_setDeviceConnectionState(JNIEnv *env, jobject thiz, jint state, jobject jParcel, - jint codec) { + jint codec, jboolean deviceSwitch) { int status; if (Parcel *parcel = parcelForJavaObject(env, jParcel); parcel != nullptr) { android::media::audio::common::AudioPort port{}; if (status_t statusOfParcel = port.readFromParcel(parcel); statusOfParcel == OK) { - status = check_AudioSystem_Command( - AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(state), - port, static_cast<audio_format_t>(codec))); + status = check_AudioSystem_Command( + AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>( + state), + port, static_cast<audio_format_t>(codec), + deviceSwitch)); } else { ALOGE("Failed to read from parcel: %s", statusToString(statusOfParcel).c_str()); status = kAudioStatusError; @@ -3457,7 +3459,7 @@ static const JNINativeMethod gMethods[] = { MAKE_AUDIO_SYSTEM_METHOD(newAudioSessionId), MAKE_AUDIO_SYSTEM_METHOD(newAudioPlayerId), MAKE_AUDIO_SYSTEM_METHOD(newAudioRecorderId), - MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;I)I", + MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;IZ)I", android_media_AudioSystem_setDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(getDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(handleDeviceConfigChange), diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index e0cc055a62a6..c4259f41e380 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -266,16 +266,24 @@ class NativeCommandBuffer { } // Picky version of atoi(). No sign or unexpected characters allowed. Return -1 on failure. static int digitsVal(char* start, char* end) { + constexpr int vmax = std::numeric_limits<int>::max(); int result = 0; - if (end - start > 6) { - return -1; - } for (char* dp = start; dp < end; ++dp) { if (*dp < '0' || *dp > '9') { - ALOGW("Argument failed integer format check"); + ALOGW("Argument contains non-integer characters"); + return -1; + } + int digit = *dp - '0'; + if (result > vmax / 10) { + ALOGW("Argument exceeds int limit"); + return -1; + } + result *= 10; + if (result > vmax - digit) { + ALOGW("Argument exceeds int limit"); return -1; } - result = 10 * result + (*dp - '0'); + result += digit; } return result; } diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index 75bd244cbbf4..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -29,7 +29,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" + android:textSize="16sp" android:singleLine="true" android:layout_weight="1" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index 054583297d37..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -102,7 +102,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 9959b666b3bf..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -104,7 +104,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index 85ca124de8ff..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -130,7 +130,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml index 11fc48668ad7..52bc7b8ea3bb 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml @@ -69,7 +69,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml index bf70a5eff47e..cf9ff6bef6f8 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml @@ -90,7 +90,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1a311d572e0b..2188469bdf03 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2643,6 +2643,15 @@ <!-- MMS user agent prolfile url --> <string name="config_mms_user_agent_profile_url" translatable="false"></string> + <!-- The default list of possible CMF Names|Style|ColorSource. This array can be + overridden device-specific resources. A wildcard (fallback) must be supplied. + Name - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required. + Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java + Color - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) --> + <string-array name="theming_defaults"> + <item>*|TONAL_SPOT|home_wallpaper</item> + </string-array> + <!-- National Language Identifier codes for the following two config items. (from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1): 0 - reserved diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index e9d87e4b5f5b..9acb2427aaab 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -580,9 +580,6 @@ <dimen name="notification_text_size">14sp</dimen> <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) --> <dimen name="notification_title_text_size">14sp</dimen> - <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) --> - <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title --> - <dimen name="notification_2025_title_text_size">16sp</dimen> <!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) --> <dimen name="notification_big_title_text_size">16sp</dimen> <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c62732d36038..ffcfce9c420e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,7 +575,6 @@ <java-symbol type="dimen" name="notification_text_size" /> <java-symbol type="dimen" name="notification_title_text_size" /> <java-symbol type="dimen" name="notification_subtext_size" /> - <java-symbol type="dimen" name="notification_2025_title_text_size" /> <java-symbol type="dimen" name="notification_top_pad" /> <java-symbol type="dimen" name="notification_top_pad_narrow" /> <java-symbol type="dimen" name="notification_top_pad_large_text" /> @@ -5904,6 +5903,9 @@ <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> + <!-- Device CMF Theming Settings --> + <java-symbol type="array" name="theming_defaults" /> + <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java index 7d236d203201..3b8f46630344 100644 --- a/graphics/java/android/graphics/FrameInfo.java +++ b/graphics/java/android/graphics/FrameInfo.java @@ -93,10 +93,12 @@ public final class FrameInfo { // Interval between two consecutive frames public static final int FRAME_INTERVAL = 11; + // Workload target deadline for a frame + public static final int WORKLOAD_TARGET = 12; + // Must be the last one // This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h - // In calculating size, + 1 for Flags, and + 1 for WorkloadTarget from FrameInfo.h - private static final int FRAME_INFO_SIZE = FRAME_INTERVAL + 2; + private static final int FRAME_INFO_SIZE = WORKLOAD_TARGET + 1; /** checkstyle */ public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId, @@ -108,6 +110,7 @@ public final class FrameInfo { frameInfo[FRAME_DEADLINE] = frameDeadline; frameInfo[FRAME_START_TIME] = frameStartTime; frameInfo[FRAME_INTERVAL] = frameInterval; + frameInfo[WORKLOAD_TARGET] = frameDeadline - intendedVsync; } /** checkstyle */ diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java index 9016724b765e..3543e991924e 100644 --- a/graphics/java/android/graphics/RuntimeShader.java +++ b/graphics/java/android/graphics/RuntimeShader.java @@ -20,7 +20,6 @@ import android.annotation.ColorInt; import android.annotation.ColorLong; import android.annotation.FlaggedApi; import android.annotation.NonNull; -import android.annotation.Nullable; import android.util.ArrayMap; import android.view.Window; @@ -77,7 +76,6 @@ import libcore.util.NativeAllocationRegistry; * Additionally, if the shader is invoked by another using {@link #setInputShader(String, Shader)}, * then that parent shader may modify the input coordinates arbitrarily.</p> * - * <a id="agsl-and-color-spaces"/> * <h3>AGSL and Color Spaces</h3> * <p>Android Graphics and by extension {@link RuntimeShader} are color managed. The working * {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which @@ -269,8 +267,6 @@ public class RuntimeShader extends Shader { private ArrayMap<String, ColorFilter> mColorFilterUniforms = new ArrayMap<>(); private ArrayMap<String, RuntimeXfermode> mXfermodeUniforms = new ArrayMap<>(); - private ColorSpace mWorkingColorSpace = null; - /** * Creates a new RuntimeShader. @@ -290,35 +286,6 @@ public class RuntimeShader extends Shader { } /** - * Sets the working color space for this shader. That is, the shader will be evaluated - * in the given colorspace before being converted to the output destination's colorspace. - * - * <p>By default the RuntimeShader is evaluated in the context of the - * <a href="#agsl-and-color-spaces">destination colorspace</a>. By calling this method - * that can be overridden to force the shader to be evaluated in the given colorspace first - * before then being color converted to the destination colorspace.</p> - * - * @param colorSpace The ColorSpace to evaluate in. Must be an {@link ColorSpace#getModel() RGB} - * ColorSpace. Passing null restores default behavior of working in the - * destination colorspace. - * @throws IllegalArgumentException If the colorspace is not RGB - */ - @FlaggedApi(Flags.FLAG_SHADER_COLOR_SPACE) - public void setWorkingColorSpace(@Nullable ColorSpace colorSpace) { - if (colorSpace != null && colorSpace.getModel() != ColorSpace.Model.RGB) { - throw new IllegalArgumentException("ColorSpace must be RGB, given " + colorSpace); - } - if (mWorkingColorSpace != colorSpace) { - mWorkingColorSpace = colorSpace; - if (mWorkingColorSpace != null) { - // Just to enforce this can be resolved instead of erroring out later - mWorkingColorSpace.getNativeInstance(); - } - discardNativeInstance(); - } - } - - /** * Sets the uniform color value corresponding to this shader. If the shader does not have a * uniform with that name or if the uniform is declared with a type other than vec3 or vec4 and * corresponding layout(color) annotation then an IllegalArgumentException is thrown. @@ -611,8 +578,7 @@ public class RuntimeShader extends Shader { /** @hide */ @Override protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) { - return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix, - mWorkingColorSpace != null ? mWorkingColorSpace.getNativeInstance() : 0); + return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix); } /** @hide */ @@ -622,8 +588,7 @@ public class RuntimeShader extends Shader { private static native long nativeGetFinalizer(); private static native long nativeCreateBuilder(String agsl); - private static native long nativeCreateShader(long shaderBuilder, long matrix, - long colorSpacePtr); + private static native long nativeCreateShader(long shaderBuilder, long matrix); private static native void nativeUpdateUniforms( long shaderBuilder, String uniformName, float[] uniforms, boolean isColor); private static native void nativeUpdateUniforms( diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 8d7e5fd95957..d50a14cf5dae 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -18,23 +18,28 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout android:id="@+id/container" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="16dp" + android:paddingHorizontal="12dp" + android:paddingVertical="16dp" + android:measureWithLargestChild="true" android:gravity="center"> <LinearLayout android:id="@+id/maximize_menu_immersive_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -44,21 +49,22 @@ android:stateListAnimator="@null" android:importantForAccessibility="yes" android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text" - android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" android:alpha="0"/> <TextView android:id="@+id/maximize_menu_immersive_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_immersive_button_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -66,7 +72,11 @@ android:id="@+id/maximize_menu_size_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -81,15 +91,17 @@ <TextView android:id="@+id/maximize_menu_size_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_maximize_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -97,7 +109,11 @@ android:id="@+id/maximize_menu_snap_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <LinearLayout android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" @@ -106,7 +122,6 @@ android:padding="4dp" android:background="@drawable/desktop_mode_maximize_menu_layout_background" android:layout_marginBottom="4dp" - android:layout_marginStart="8dp" android:alpha="0"> <Button android:id="@+id/maximize_menu_snap_left_button" @@ -131,16 +146,17 @@ </LinearLayout> <TextView android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" + android:lineHeight="16sp" android:gravity="center" android:importantForAccessibility="no" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:text="@string/desktop_mode_maximize_menu_snap_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> </LinearLayout> @@ -150,6 +166,6 @@ <View android:id="@+id/maximize_menu_overlay" android:layout_width="match_parent" - android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> + android:layout_height="match_parent"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index e395341a5792..f5f3f0fe52eb 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -498,14 +498,6 @@ <!-- The default minimum allowed window height when resizing a window in desktop mode. --> <dimen name="desktop_mode_minimum_window_height">352dp</dimen> - <!-- The width of the maximize menu in desktop mode, depending on the number of options --> - <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen> - - <!-- The height of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_height">114dp</dimen> - <!-- The padding of the maximize menu in desktop mode. --> <dimen name="desktop_mode_menu_padding">16dp</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 4d00c74155a8..851987269c10 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -21,6 +21,7 @@ import static android.view.RemoteAnimationTarget.MODE_CHANGING; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -55,9 +56,15 @@ import java.util.function.Predicate; public class TransitionUtil { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1; /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */ - public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1; + public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2; + + /** + * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher. + */ + public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1; /** @return true if the transition was triggered by opening something vs closing something */ public static boolean isOpeningType(@WindowManager.TransitionType int type) { @@ -117,6 +124,11 @@ public class TransitionUtil { return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR); } + /** Returns `true` if `change` is an app's dim layer. */ + public static boolean isDimLayer(TransitionInfo.Change change) { + return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER); + } + /** Returns `true` if `change` is only re-ordering. */ public static boolean isOrderOnly(TransitionInfo.Change change) { return change.getMode() == TRANSIT_CHANGE @@ -231,6 +243,14 @@ public class TransitionUtil { t.setLayer(leash, Integer.MAX_VALUE); return; } + if (isDimLayer(change)) { + // When a dim layer gets reparented onto the transition root, we need to zero out its + // position so that it's in line with everything else on the transition root. Also, + // we need to set a crop because we don't want it applying MATCH_PARENT on the whole + // root surface. + t.setPosition(leash, 0, 0); + t.setCrop(leash, change.getEndAbsBounds()); + } // Put all the OPEN/SHOW on top if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { @@ -284,14 +304,19 @@ public class TransitionUtil { // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); t.reparent(change.getLeash(), leashSurface); - t.setAlpha(change.getLeash(), 1.0f); - t.show(change.getLeash()); + if (!isDimLayer(change)) { + // Most leashes going onto the transition root should have their alpha set here to make + // them visible. But dim layers should be left untouched (their alpha value is their + // actual dim value). + t.setAlpha(change.getLeash(), 1.0f); + } if (!isDividerBar(change)) { // For divider, don't modify its inner leash position when creating the outer leash // for the transition. In case the position being wrong after the transition finished. t.setPosition(change.getLeash(), 0, 0); } t.setLayer(change.getLeash(), 0); + t.show(change.getLeash()); return leashSurface; } @@ -333,6 +358,9 @@ public class TransitionUtil { if (isDividerBar(change)) { return getDividerTarget(change, leash); } + if (isDimLayer(change)) { + return getDimLayerTarget(change, leash); + } int taskId; boolean isNotInRecents; @@ -439,6 +467,17 @@ public class TransitionUtil { TYPE_DOCK_DIVIDER); } + private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change, + SurfaceControl leash) { + return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), + leash, false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), + change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */, + null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */, + TYPE_SPLIT_SCREEN_DIM_LAYER); + } + /** * Finds the "correct" root idx for a change. The change's end display is prioritized, then * the start display. If there is no display, it will fallback on the 0th root in the diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java index f45dc3a1e892..e92c1eb81e89 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java @@ -93,10 +93,21 @@ public class Interpolators { public static final PathInterpolator SLOWDOWN_INTERPOLATOR = new PathInterpolator(0.5f, 1f, 0.5f, 1f); + /** + * An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss + * point. A sharp rise, followed by a steady middle, and ending with another sharp rise. + */ public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); /** + * An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp + * rises" of {@link #DIM_INTERPOLATOR}. + */ + public static final PathInterpolator FAST_DIM_INTERPOLATOR = + new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f); + + /** * Use this interpolator for animating progress values coming from the back callback to get * the predictive-back-typical decelerate motion. * diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index b48296f5f76a..759e711100c3 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -262,6 +262,7 @@ public class SplitScreenConstants { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR; + public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER; public static final String splitPositionToString(@SplitPosition int pos) { switch (pos) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 4c3bde9b2b3a..97184c859d4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -67,6 +67,7 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); private final Map<Integer, RectF> mUnpopulatedDisplayBounds = new HashMap<>(); + private DisplayTopology mDisplayTopology; public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, ShellExecutor mainExecutor, DisplayManager displayManager) { @@ -157,6 +158,7 @@ public class DisplayController { for (int i = 0; i < mDisplays.size(); ++i) { listener.onDisplayAdded(mDisplays.keyAt(i)); } + listener.onTopologyChanged(mDisplayTopology); } } @@ -245,6 +247,7 @@ public class DisplayController { if (topology == null) { return; } + mDisplayTopology = topology; SparseArray<RectF> absoluteBounds = topology.getAbsoluteBounds(); mUnpopulatedDisplayBounds.clear(); for (int i = 0; i < absoluteBounds.size(); ++i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt new file mode 100644 index 000000000000..a1d700af5569 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.window.WindowContainerTransaction +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default + * parameters values [builder = ::WindowContainerTransaction] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [WindowContainerTransaction]s. + */ +@WMSingleton +class WindowContainerTransactionSupplier @Inject constructor( +) : Supplier<WindowContainerTransaction> { + override fun get(): WindowContainerTransaction = WindowContainerTransaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java new file mode 100644 index 000000000000..fb2a324375b6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_ALIGN_CENTER} is the desired + * parallax effect. + */ +public class CenterParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = (retreatingSurface.width() - retreatingContent.width()) / 2; + } else { + retreatingOut.y = (retreatingSurface.height() - retreatingContent.height()) / 2; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java new file mode 100644 index 000000000000..39ecbb379d7d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; + +import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.WindowManager; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_DISMISSING} is the desired parallax + * effect. + */ +public class DismissingParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (dimmingSide == DOCKED_INVALID) { + return; + } + + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + int totalDismissingDistance = 0; + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getDismissStartTarget().getPosition() + - snapAlgorithm.getFirstSplitTarget().getPosition(); + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getLastSplitTarget().getPosition() + - snapAlgorithm.getDismissEndTarget().getPosition(); + } + + float parallaxFraction = + calculateParallaxDismissingFraction(progressTowardScreenEdge, dimmingSide); + if (isLeftRightSplit) { + retreatingOut.x = (int) (parallaxFraction * totalDismissingDistance); + } else { + retreatingOut.y = (int) (parallaxFraction * totalDismissingDistance); + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index 2f5afcaa907b..5b2dd97a338f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -465,5 +465,9 @@ public class DividerSnapAlgorithm { this.snapPosition = snapPosition; this.distanceMultiplier = distanceMultiplier; } + + public int getPosition() { + return position; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java new file mode 100644 index 000000000000..9fa162164e0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX} + * is the desired parallax effect. + */ +public class FlexParallaxSpec implements ParallaxSpec { + final Rect mTempRect = new Rect(); + + @Override + public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** + * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split. + * In flexible split, there are two dimming "behaviors". + * 1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or + * 90%, we dim the app slightly as it moves partially offscreen. + * 2) "fast dim": when moving the divider from a side snap target further toward the screen + * edge, we dim the app rapidly as it approaches the dismiss point. + * @return 0f = no dim applied. 1f = full black. + */ + public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition(); + int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition(); + int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition(); + int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition(); + int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition(); + float progress; + + if (startDismissPos <= position && position < firstTargetPos) { + // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves + // toward the screen edge + progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos); + return fastDim(progress); + } else if (firstTargetPos <= position && position < middleTargetPos) { + // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target + progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos); + return slowDim(progress); + } else if (middleTargetPos <= position && position < lastTargetPos) { + // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target + progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos); + return slowDim(progress); + } else if (lastTargetPos <= position && position <= endDismissPos) { + // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it + // moves toward screen edge + progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos); + return fastDim(progress); + } + return 0f; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps + * up to the default amount of dimming for an offscreen app, + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}. + */ + private float slowDim(float progress) { + return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black). + */ + private float fastDim(float progress) { + return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress) + * (1 - DEFAULT_OFFSCREEN_DIM)); + } + + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // Whether an app is getting pushed offscreen by the divider. + boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface); + // Whether an app was getting pulled onscreen at the beginning of the drag. + boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent); + + // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) + if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { + // On the left side, we use parallax to simulate the contents sticking to the + // divider. This is because surfaces naturally expand to the bottom and right, + // so when a surface's area expands, the contents stick to the left. This is + // correct behavior on the right-side surface, but not the left. + if (topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = retreatingSurface.width() - retreatingContent.width(); + } else { + retreatingOut.y = retreatingSurface.height() - retreatingContent.height(); + } + } + // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) + } else { + mTempRect.set(retreatingSurface); + Point rootOffset = new Point(); + // 10:90 -> 50:50, 10:90, or dismiss right + if (advancingSideStartedOffscreen) { + // We have to handle a complicated case here to keep the parallax smooth. + // When the divider crosses the 50% mark, the retreating-side app surface + // will start expanding offscreen. This is expected and unavoidable, but + // makes the parallax look disjointed. In order to preserve the illusion, + // we add another offset (rootOffset) to simulate the surface staying + // onscreen. + if (mTempRect.intersect(displayBounds)) { + if (retreatingSurface.left < displayBounds.left) { + rootOffset.x = displayBounds.left - retreatingSurface.left; + } + if (retreatingSurface.top < displayBounds.top) { + rootOffset.y = displayBounds.top - retreatingSurface.top; + } + } + + // On the left side, we again have to simulate the contents sticking to the + // divider. + if (!topLeftShrink) { + if (isLeftRightSplit) { + advancingOut.x = advancingSurface.width() - advancingContent.width(); + } else { + advancingOut.y = advancingSurface.height() - advancingContent.height(); + } + } + } + + // In all these cases, the shrinking app also receives a center parallax. + if (isLeftRightSplit) { + retreatingOut.x = rootOffset.x + + ((mTempRect.width() - retreatingContent.width()) / 2); + } else { + retreatingOut.y = rootOffset.y + + ((mTempRect.height() - retreatingContent.height()) / 2); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java new file mode 100644 index 000000000000..043b2880f28b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_NONE} + * is the desired parallax effect. + */ +public class NoParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // no-op + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java new file mode 100644 index 000000000000..84d849b3c1f9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Default interface for a set of calculation classes, used for calculating various parallax and + * dimming effects in split screen. + */ +public interface ParallaxSpec { + /** Returns an int indicating which side of the screen is being dimmed (if any). */ + default int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** Returns the dim amount that we'll apply to the app surface. 0f = no dim, 1f = full black */ + default float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + return DIM_INTERPOLATOR.getInterpolation(progressTowardScreenEdge); + } + + /** + * Calculates the amount to offset app surfaces to create nice parallax effects. Writes to + * {@link ResizingEffectPolicy#mRetreatingSideParallax} and + * {@link ResizingEffectPolicy#mAdvancingSideParallax}. + */ + void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java index 3f76fd0220ff..e2e1f9698a90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java @@ -26,27 +26,32 @@ import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTE import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE; -import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; -import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import android.graphics.Point; import android.graphics.Rect; import android.view.SurfaceControl; -import android.view.WindowManager; /** * This class governs how and when parallax and dimming effects are applied to task surfaces, * usually when the divider is being moved around by the user (or during an animation). */ class ResizingEffectPolicy { + /** The default amount to dim an app that is partially offscreen. */ + public static float DEFAULT_OFFSCREEN_DIM = 0.32f; + private final SplitLayout mSplitLayout; /** The parallax algorithm we are currently using. */ private final int mParallaxType; + /** + * A convenience class, corresponding to {@link #mParallaxType}, that performs all the + * calculations for parallax and dimming values. + */ + private final ParallaxSpec mParallaxSpec; int mShrinkSide = DOCKED_INVALID; // The current dismissing side. - int mDismissingSide = DOCKED_INVALID; + int mDimmingSide = DOCKED_INVALID; /** * A {@link Point} that stores a single x and y value, representing the parallax translation @@ -62,7 +67,7 @@ class ResizingEffectPolicy { final Point mAdvancingSideParallax = new Point(); // The dimming value to hint the dismissing side and progress. - float mDismissingDimValue = 0.0f; + float mDimValue = 0.0f; /** * Content bounds for the app that the divider is moving toward. This is the content that is @@ -95,35 +100,38 @@ class ResizingEffectPolicy { ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) { mParallaxType = parallaxType; mSplitLayout = splitLayout; + switch (mParallaxType) { + case PARALLAX_DISMISSING: + mParallaxSpec = new DismissingParallaxSpec(); + break; + case PARALLAX_ALIGN_CENTER: + mParallaxSpec = new CenterParallaxSpec(); + break; + case PARALLAX_FLEX: + mParallaxSpec = new FlexParallaxSpec(); + break; + case PARALLAX_NONE: + default: + mParallaxSpec = new NoParallaxSpec(); + break; + } } /** - * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax} - * and {@link #mAdvancingSideParallax}. These values will be then be applied in - * {@link #adjustRootSurface}. - * - * @param position The divider's position on the screen (x-coordinate in left-right split, - * y-coordinate in top-bottom split). + * Calculates the desired parallax and dimming values for a task surface and stores them in + * {@link #mRetreatingSideParallax}, {@link #mAdvancingSideParallax}, and + * {@link #mDimValue} These values will be then be applied in + * {@link #adjustRootSurface} and {@link #adjustDimSurface} respectively. */ void applyDividerPosition( int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) { - mDismissingSide = DOCKED_INVALID; + mDimmingSide = DOCKED_INVALID; mRetreatingSideParallax.set(0, 0); mAdvancingSideParallax.set(0, 0); - mDismissingDimValue = 0; + mDimValue = 0; Rect displayBounds = mSplitLayout.getRootBounds(); - int totalDismissingDistance = 0; - if (position < snapAlgorithm.getFirstSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; - totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position - - snapAlgorithm.getFirstSplitTarget().position; - } else if (position > snapAlgorithm.getLastSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; - totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position - - snapAlgorithm.getDismissEndTarget().position; - } - + // Figure out which side is shrinking, and assign retreating/advancing bounds final boolean topLeftShrink = isLeftRightSplit ? position < mSplitLayout.getTopLeftContentBounds().right : position < mSplitLayout.getTopLeftContentBounds().bottom; @@ -141,106 +149,20 @@ class ResizingEffectPolicy { mAdvancingSurface.set(mSplitLayout.getTopLeftBounds()); } - if (mDismissingSide != DOCKED_INVALID) { - float fraction = - Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); - mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); - if (mParallaxType == PARALLAX_DISMISSING) { - fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); - if (isLeftRightSplit) { - mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance); - } else { - mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance); - } - } - } - - if (mParallaxType == PARALLAX_ALIGN_CENTER) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - (mRetreatingSurface.width() - mRetreatingContent.width()) / 2; - } else { - mRetreatingSideParallax.y = - (mRetreatingSurface.height() - mRetreatingContent.height()) / 2; - } - } else if (mParallaxType == PARALLAX_FLEX) { - // Whether an app is getting pushed offscreen by the divider. - boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface); - // Whether an app was getting pulled onscreen at the beginning of the drag. - boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent); + // Figure out if we should be dimming one side + mDimmingSide = mParallaxSpec.getDimmingSide(position, snapAlgorithm, isLeftRightSplit); - // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) - if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { - // On the left side, we use parallax to simulate the contents sticking to the - // divider. This is because surfaces naturally expand to the bottom and right, - // so when a surface's area expands, the contents stick to the left. This is - // correct behavior on the right-side surface, but not the left. - if (topLeftShrink) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - mRetreatingSurface.width() - mRetreatingContent.width(); - } else { - mRetreatingSideParallax.y = - mRetreatingSurface.height() - mRetreatingContent.height(); - } - } - // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) - } else { - mTempRect.set(mRetreatingSurface); - Point rootOffset = new Point(); - // 10:90 -> 50:50, 10:90, or dismiss right - if (advancingSideStartedOffscreen) { - // We have to handle a complicated case here to keep the parallax smooth. - // When the divider crosses the 50% mark, the retreating-side app surface - // will start expanding offscreen. This is expected and unavoidable, but - // makes the parallax look disjointed. In order to preserve the illusion, - // we add another offset (rootOffset) to simulate the surface staying - // onscreen. - mTempRect.intersect(displayBounds); - if (mRetreatingSurface.left < displayBounds.left) { - rootOffset.x = displayBounds.left - mRetreatingSurface.left; - } - if (mRetreatingSurface.top < displayBounds.top) { - rootOffset.y = displayBounds.top - mRetreatingSurface.top; - } - - // On the left side, we again have to simulate the contents sticking to the - // divider. - if (!topLeftShrink) { - if (isLeftRightSplit) { - mAdvancingSideParallax.x = - mAdvancingSurface.width() - mAdvancingContent.width(); - } else { - mAdvancingSideParallax.y = - mAdvancingSurface.height() - mAdvancingContent.height(); - } - } - } - - // In all these cases, the shrinking app also receives a center parallax. - if (isLeftRightSplit) { - mRetreatingSideParallax.x = rootOffset.x - + ((mTempRect.width() - mRetreatingContent.width()) / 2); - } else { - mRetreatingSideParallax.y = rootOffset.y - + ((mTempRect.height() - mRetreatingContent.height()) / 2); - } - } + // If so, calculate dimming + if (mDimmingSide != DOCKED_INVALID) { + mDimValue = mParallaxSpec.getDimValue(position, snapAlgorithm); } - } - /** - * @return for a specified {@code fraction}, this returns an adjusted value that simulates a - * slowing down parallax effect - */ - private float calculateParallaxDismissingFraction(float fraction, int dockSide) { - float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; - - // Less parallax at the top, just because. - if (dockSide == WindowManager.DOCKED_TOP) { - result /= 2f; - } - return result; + // Calculate parallax and modify mRetreatingSideParallax and mAdvancingSideParallax, for use + // in adjustRootSurface(). + mParallaxSpec.getParallax(mRetreatingSideParallax, mAdvancingSideParallax, position, + snapAlgorithm, isLeftRightSplit, displayBounds, mRetreatingSurface, + mRetreatingContent, mAdvancingSurface, mAdvancingContent, mDimmingSide, + topLeftShrink); } /** Applies the calculated parallax and dimming values to task surfaces. */ @@ -250,7 +172,7 @@ class ResizingEffectPolicy { SurfaceControl advancingLeash = null; if (mParallaxType == PARALLAX_DISMISSING) { - switch (mDismissingSide) { + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: retreatingLeash = leash1; @@ -303,14 +225,17 @@ class ResizingEffectPolicy { void adjustDimSurface(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { SurfaceControl targetDimLayer; - switch (mDismissingSide) { + SurfaceControl oppositeDimLayer; + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: targetDimLayer = dimLayer1; + oppositeDimLayer = dimLayer2; break; case DOCKED_BOTTOM: case DOCKED_RIGHT: targetDimLayer = dimLayer2; + oppositeDimLayer = dimLayer1; break; case DOCKED_INVALID: default: @@ -318,7 +243,9 @@ class ResizingEffectPolicy { t.setAlpha(dimLayer2, 0).hide(dimLayer2); return; } - t.setAlpha(targetDimLayer, mDismissingDimValue) - .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); + t.setAlpha(targetDimLayer, mDimValue) + .setVisibility(targetDimLayer, mDimValue > 0.001f); + t.setAlpha(oppositeDimLayer, 0f) + .setVisibility(oppositeDimLayer, false); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index bd89f5cf45f6..708e26cc5546 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -128,6 +128,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // The touch layer is on a stage root, and is sibling with things like the app activity itself // and the app veil. We want it to be above all those. public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; + // The dim layer is also on the stage root, and stays under the touch layer. + public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1; // Animation specs for the swap animation private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; @@ -1201,6 +1203,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); } + if (dimLayer1 != null) { + t.setLayer(dimLayer1, RESTING_DIM_LAYER); + } + if (dimLayer2 != null) { + t.setLayer(dimLayer2, RESTING_DIM_LAYER); + } copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java index d1d133d16ae4..ad0e7fc187e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java @@ -57,4 +57,9 @@ public class SplitState { public List<RectF> getLayout(@SplitScreenState int state) { return mSplitSpec.getSpec(state); } + + /** Returns the layout associated with the current split state. */ + public List<RectF> getCurrentLayout() { + return getLayout(mState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt new file mode 100644 index 000000000000..bdffcf51e7d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.view.GestureDetector +import android.view.MotionEvent +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY + +/** + * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the + * Letterbox Input surface, understands the type of event and filter them based on the current + * letterbox position. + */ +class ReachabilityGestureListener( + private val taskId: Int, + private val token: WindowContainerToken?, + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) : GestureDetector.SimpleOnGestureListener() { + + // The current letterbox bounds. Double tap events are ignored when happening in these bounds. + private val activityBounds = Rect() + + override fun onDoubleTap(e: MotionEvent): Boolean { + val x = e.rawX.toInt() + val y = e.rawY.toInt() + if (!activityBounds.contains(x, y)) { + val wct = wctSupplier.get().apply { + setReachabilityOffset(token!!, taskId, x, y) + } + transitions.startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + return true + } + return false + } + + /** + * Updates the bounds for the letterboxed activity. + */ + fun updateActivityBounds(newActivityBounds: Rect) { + activityBounds.set(newActivityBounds) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt new file mode 100644 index 000000000000..5e9fe09bc840 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.transition.Transitions +import javax.inject.Inject + +/** + * A Factory for [ReachabilityGestureListener]. + */ +@WMSingleton +class ReachabilityGestureListenerFactory @Inject constructor( + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) { + /** + * @return a [ReachabilityGestureListener] implementation to listen to double tap events and + * creating the related [WindowContainerTransaction] to handle the transition. + */ + fun createReachabilityGestureListener( + taskId: Int, + token: WindowContainerToken? + ): ReachabilityGestureListener = + ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 4dc82b66b916..eba1be517147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -821,7 +821,7 @@ class DesktopRepository( /** Removes the given task from the given desk. */ fun removeTaskFromDesk(deskId: Int, taskId: Int) { - logD("removeTaskFromDesk: deskId=%d, taskId=%d", taskId, deskId) + logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId) // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving // it between desks. It might be better to allow restoring to the previous bounds as long // as they're valid (probably valid if in the same display). diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 5eb86506d876..0afceac8a861 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -153,6 +153,16 @@ import java.util.concurrent.TimeUnit import java.util.function.Consumer import kotlin.jvm.optionals.getOrNull +/** + * A callback to be invoked when a transition is started via |Transitions.startTransition| with the + * transition binder token that it produces. + * + * Useful when multiple components are appending WCT operations to a single transition that is + * started outside of their control, and each of them wants to track the transition lifecycle + * independently by cross-referencing the transition token with future ready-transitions. + */ +typealias RunOnTransitStart = (IBinder) -> Unit + /** Handles moving tasks in and out of desktop */ class DesktopTasksController( private val context: Context, @@ -481,7 +491,7 @@ class DesktopTasksController( ): Boolean { val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) if (runningTask != null) { - moveRunningTaskToDesk( + return moveRunningTaskToDesk( task = runningTask, deskId = deskId, wct = wct, @@ -563,10 +573,10 @@ class DesktopTasksController( transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, - ) { + ): Boolean { if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) { logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) - return + return false } val displayId = taskRepository.getDisplayForDesk(deskId) logV( @@ -621,6 +631,7 @@ class DesktopTasksController( } else { taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) } + return true } /** @@ -789,24 +800,44 @@ class DesktopTasksController( wct: WindowContainerTransaction, displayId: Int, taskInfo: RunningTaskInfo, - ): ((IBinder) -> Unit)? { + ): ((IBinder) -> Unit) { val taskId = taskInfo.taskId + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) snapEventHandler.removeTaskIfTiled(displayId, taskId) - // TODO: b/394268248 - desk needs to be deactivated when closing the last task and going - // home. - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + val shouldExitDesktop = + willExitDesktop( + triggerTaskId = taskInfo.taskId, + displayId = displayId, + forceToFullscreen = false, + ) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = shouldExitDesktop, + shouldEndUpAtHome = true, + ) + taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) ) - return desktopImmersiveController - .exitImmersiveIfApplicable( - wct = wct, - taskInfo = taskInfo, - reason = DesktopImmersiveController.ExitReason.CLOSED, - ) - .asExit() - ?.runOnTransitionStart + + val immersiveRunnable = + desktopImmersiveController + .exitImmersiveIfApplicable( + wct = wct, + taskInfo = taskInfo, + reason = DesktopImmersiveController.ExitReason.CLOSED, + ) + .asExit() + ?.runOnTransitionStart + return { transitionToken -> + immersiveRunnable?.invoke(transitionToken) + desktopExitRunnable?.invoke(transitionToken) + } } fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { @@ -840,12 +871,20 @@ class DesktopTasksController( private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val taskId = taskInfo.taskId + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) val displayId = taskInfo.displayId val wct = WindowContainerTransaction() + snapEventHandler.removeTaskIfTiled(displayId, taskId) - // TODO: b/394268248 - desk needs to be deactivated when minimizing the last task and going - // home. - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = willExitDesktop, + ) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -867,6 +906,7 @@ class DesktopTasksController( ) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + desktopExitRunnable?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ @@ -915,7 +955,7 @@ class DesktopTasksController( logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true) - val deactivatingDeskId = addMoveToFullscreenChanges(wct, task, willExitDesktop) + val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop) // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. if (!forceEnterDesktop(task.displayId)) { @@ -930,11 +970,7 @@ class DesktopTasksController( position, mOnAnimationFinishedCallback, ) - if (deactivatingDeskId != null) { - desksTransitionObserver.addPendingTransition( - DeskTransition.DeactivateDesk(token = transition, deskId = deactivatingDeskId) - ) - } + deactivationRunnable?.invoke(transition) // handles case where we are moving to full screen without closing all DW tasks. if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) { @@ -1770,19 +1806,30 @@ class DesktopTasksController( wct: WindowContainerTransaction, forceToFullscreen: Boolean, shouldEndUpAtHome: Boolean = true, - ) { + ): RunOnTransitStart? { taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen) if (!willExitDesktop(taskId, displayId, forceToFullscreen)) { - return + return null } - performDesktopExitCleanUp(wct, displayId, shouldEndUpAtHome) + // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the + // |RunOnTransitStart| when the transition is started. + return performDesktopExitCleanUp( + wct = wct, + deskId = null, + displayId = displayId, + willExitDesktop = true, + shouldEndUpAtHome = shouldEndUpAtHome, + ) } private fun performDesktopExitCleanUp( wct: WindowContainerTransaction, + deskId: Int?, displayId: Int, + willExitDesktop: Boolean, shouldEndUpAtHome: Boolean = true, - ) { + ): RunOnTransitStart? { + if (!willExitDesktop) return null desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) @@ -1792,6 +1839,7 @@ class DesktopTasksController( // intent. addLaunchHomePendingIntent(wct, displayId) } + return prepareDeskDeactivationIfNeeded(wct, deskId) } fun releaseVisualIndicator() { @@ -2473,7 +2521,7 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, willExitDesktop: Boolean, - ): Int? { + ): RunOnTransitStart? { val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -2492,15 +2540,14 @@ class DesktopTasksController( wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true) } taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false) - if (willExitDesktop) { - performDesktopExitCleanUp(wct, taskInfo.displayId, shouldEndUpAtHome = false) - val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && deskId != null) { - desksOrganizer.deactivateDesk(wct, deskId) - return deskId - } - } - return null + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + return performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = taskInfo.displayId, + willExitDesktop = willExitDesktop, + shouldEndUpAtHome = false, + ) } private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) { @@ -2661,6 +2708,23 @@ class DesktopTasksController( ) } + /** + * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home. + */ + private fun prepareDeskDeactivationIfNeeded( + wct: WindowContainerTransaction, + deskId: Int?, + ): RunOnTransitStart? { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null + if (deskId == null) return null + desksOrganizer.deactivateDesk(wct, deskId) + return { transition -> + desksTransitionObserver.addPendingTransition( + DeskTransition.DeactivateDesk(token = transition, deskId = deskId) + ) + } + } + /** Removes the default desk in the given display. */ @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) fun removeDefaultDeskInDisplay(displayId: Int) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index aaecf8c2d727..0929ae15e668 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -251,7 +251,8 @@ sealed class DragToDesktopTransitionHandler( (cancelState == CancelState.CANCEL_BUBBLE_LEFT || cancelState == CancelState.CANCEL_BUBBLE_RIGHT) ) { - if (!bubbleController.isPresent) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble startCancelAnimation() } else { // Animation is handled by BubbleController @@ -497,6 +498,11 @@ sealed class DragToDesktopTransitionHandler( state.cancelState == CancelState.CANCEL_BUBBLE_LEFT || state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT ) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble + startCancelDragToDesktopTransition() + return true + } val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.") val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index 2a8a3475c2a5..b5490cb4b595 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -20,7 +20,7 @@ import android.util.SparseArray import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken -import androidx.core.util.forEach +import androidx.core.util.keyIterator import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -45,11 +45,13 @@ class DesktopWallpaperActivityTokenProvider { } fun removeToken(token: WindowContainerToken) { - wallpaperActivityTokenByDisplayId.forEach { displayId, value -> - if (value == token) { - logV("Remove desktop wallpaper activity token for display %s", displayId) - wallpaperActivityTokenByDisplayId.delete(displayId) + val displayId = + wallpaperActivityTokenByDisplayId.keyIterator().asSequence().find { + wallpaperActivityTokenByDisplayId[it] == token } + if (displayId != null) { + logV("Remove desktop wallpaper activity token for display %s", displayId) + wallpaperActivityTokenByDisplayId.delete(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt index d4586abc8ec4..e57b56378fb3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -92,10 +92,11 @@ class DesksTransitionObserver( } } is DeskTransition.DeactivateDesk -> { + var visibleDeactivation = false for (change in info.changes) { val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) if (isDeskChange) { - desktopRepository.setDeskInactive(deskId = deskTransition.deskId) + visibleDeactivation = true continue } val taskId = change.taskInfo?.taskId ?: continue @@ -109,6 +110,14 @@ class DesksTransitionObserver( ) } } + // Always deactivate even if there's no change that confirms the desk was + // deactivated. Some interactions, such as the desk deactivating because it's + // occluded by a fullscreen task result in a transition change, but others, such + // as transitioning from an empty desk to home may not. + if (!visibleDeactivation) { + logD("Deactivating desk without transition change") + } + desktopRepository.setDeskInactive(deskId = deskTransition.deskId) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index a969845fb8e8..847a0383e7d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -796,7 +796,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " unhandled root taskId=%d", taskInfo.taskId); } - } else if (TransitionUtil.isDividerBar(change)) { + } else if (TransitionUtil.isDividerBar(change) + || TransitionUtil.isDimLayer(change)) { final RemoteAnimationTarget target = TransitionUtil.newTarget(change, belowLayers - i, info, t, mLeashMap); // Add this as a app and we will separate them on launcher side by window type. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index a799b7f2580e..73b42d6f007c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -40,11 +40,13 @@ import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; +import static com.android.wm.shell.common.split.SplitLayout.RESTING_DIM_LAYER; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIM_LAYER; import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; @@ -1824,6 +1826,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Ensure divider surface are re-parented back into the hierarchy at the end of the // transition. See Transition#buildFinishTransaction for more detail. finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.show(mRootTaskLeash); @@ -3540,6 +3550,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, true /* show */); + } return true; } @@ -3790,6 +3803,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } addDividerBarToTransition(info, false /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, false /* show */); + } } /** Call this when the recents animation canceled during split-screen. */ @@ -3836,6 +3852,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, returnToApp); mPausingTasks.clear(); if (returnToApp) { + // Reparent auxiliary surfaces (divider bar and dim layers) back onto their + // original roots. + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + finishT.setLayer(stage.mDimLayer, RESTING_DIM_LAYER); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + finishT.setLayer(mMainStage.mDimLayer, RESTING_DIM_LAYER); + finishT.setLayer(mSideStage.mDimLayer, RESTING_DIM_LAYER); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); @@ -3902,6 +3931,39 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, info.addChange(barChange); } + /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */ + private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) { + if (Flags.enableFlexibleSplit()) { + List<StageTaskListener> stages = mStageOrderOperator.getActiveStages(); + for (int i = 0; i < stages.size(); i++) { + final StageTaskListener stage = stages.get(i); + mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1); + addDimLayerToTransition(info, show, stage, mTempRect1); + } + } else { + addDimLayerToTransition(info, show, mMainStage, getMainStageBounds()); + addDimLayerToTransition(info, show, mSideStage, getSideStageBounds()); + } + } + + /** Adds a single dim layer to the given TransitionInfo. */ + private void addDimLayerToTransition(@NonNull TransitionInfo info, boolean show, + StageTaskListener stage, Rect bounds) { + final SurfaceControl dimLayer = stage.mDimLayer; + if (dimLayer == null || !dimLayer.isValid()) { + Slog.w(TAG, "addDimLayerToTransition but leash was released or not created"); + } else { + final TransitionInfo.Change change = + new TransitionInfo.Change(null /* token */, dimLayer); + change.setParent(mRootTaskInfo.token); + change.setStartAbsBounds(bounds); + change.setEndAbsBounds(bounds); + change.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); + change.setFlags(FLAG_IS_DIM_LAYER); + info.addChange(change); + } + } + @NeverCompile @Override public void dump(@NonNull PrintWriter pw, String prefix) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index add2c54f0e29..a25289d0ea79 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -984,7 +984,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksController.onDesktopWindowClose( wct, mDisplayId, decoration.mTaskInfo); final IBinder transition = mTaskOperations.closeTask(mTaskToken, wct); - if (transition != null && runOnTransitionStart != null) { + if (transition != null) { runOnTransitionStart.invoke(transition); } } @@ -1476,16 +1476,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); - if (DesktopModeStatus.canEnterDesktopMode(mContext)) { + if (DesktopModeStatus.canEnterDesktopMode(mContext) + || BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { // In proto2 any full screen or multi-window task can be dragged to // freeform. dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { - // TODO(b/388851898): add support for split screen (multi-window wm mode) - dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN; - } final boolean shouldStartTransitionDrag = relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 3fb94630eab3..271dead467b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -803,8 +803,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isVisible()) { closeMaximizeMenu(); } else { - final int menuWidth = calculateMaximizeMenuWidth(); - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT); + mMaximizeMenu.positionMenu(startT); } } @@ -1069,27 +1068,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Resources.ID_NULL; } - private int calculateMaximizeMenuWidth() { - final boolean showImmersive = DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() - && TaskInfoKt.getRequestingImmersive(mTaskInfo); - final boolean showMaximize = true; - final boolean showSnaps = mTaskInfo.isResizeable; - int showCount = 0; - if (showImmersive) showCount++; - if (showMaximize) showCount++; - if (showSnaps) showCount++; - return switch (showCount) { - case 1 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_one_options); - case 2 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_two_options); - case 3 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_three_options); - default -> throw new IllegalArgumentException(""); - }; - } - - private PointF calculateMaximizeMenuPosition(int menuWidth) { + private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) { final PointF position = new PointF(); final Resources resources = mContext.getResources(); final DisplayLayout displayLayout = @@ -1105,9 +1084,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int[] maximizeButtonLocation = new int[2]; maximizeWindowButton.getLocationInWindow(maximizeButtonLocation); - final int menuHeight = loadDimensionPixelSize( - resources, R.dimen.desktop_mode_maximize_menu_height); - float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth - maximizeWindowButton.getWidth()) / 2)); float menuTop = (mPositionInParent.y + captionHeight); @@ -1294,17 +1270,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - final int menuWidth = calculateMaximizeMenuWidth(); mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, mDisplayController, mTaskInfo, mContext, - calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier); + (width, height) -> calculateMaximizeMenuPosition(width, height), + mSurfaceControlTransactionSupplier); mMaximizeMenu.show( /* isTaskInImmersiveMode= */ DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && mDesktopUserRepositories.getProfile(mTaskInfo.userId) .isTaskInFullImmersiveState(mTaskInfo.taskId), - /* menuWidth= */ menuWidth, /* showImmersiveOption= */ DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && TaskInfoKt.getRequestingImmersive(mTaskInfo), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 38accce82999..ad3525af3f94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -90,7 +90,7 @@ class MaximizeMenu( private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, private val decorWindowContext: Context, - private val menuPosition: PointF, + private val positionSupplier: (Int, Int) -> PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { private var maximizeMenu: AdditionalViewHostViewContainer? = null @@ -100,19 +100,19 @@ class MaximizeMenu( private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() - private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) + private lateinit var menuPosition: PointF private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) /** Position the menu relative to the caption's position. */ - fun positionMenu(position: PointF, t: Transaction) { - menuPosition.set(position) + fun positionMenu(t: Transaction) { + menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0, + maximizeMenuView?.measureHeight() ?: 0) t.setPosition(leash, menuPosition.x, menuPosition.y) } /** Creates and shows the maximize window. */ fun show( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeOrRestoreClickListener: () -> Unit, @@ -125,7 +125,6 @@ class MaximizeMenu( if (maximizeMenu != null) return createMaximizeMenu( isTaskInImmersiveMode = isTaskInImmersiveMode, - menuWidth = menuWidth, showImmersiveOption = showImmersiveOption, showSnapOptions = showSnapOptions, onMaximizeClickListener = onMaximizeOrRestoreClickListener, @@ -161,7 +160,6 @@ class MaximizeMenu( /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeClickListener: () -> Unit, @@ -178,16 +176,6 @@ class MaximizeMenu( .setName("Maximize Menu") .setContainerLayer() .build() - val lp = WindowManager.LayoutParams( - menuWidth, - menuHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, - PixelFormat.TRANSPARENT - ) - lp.title = "Maximize Menu for Task=" + taskInfo.taskId - lp.setTrustedOverlay() val windowManager = WindowlessWindowManager( taskInfo.configuration, leash, @@ -207,7 +195,6 @@ class MaximizeMenu( MaximizeMenuView.ImmersiveConfig.Hidden }, showSnapOptions = showSnapOptions, - menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> menuView.bind(taskInfo) @@ -217,6 +204,19 @@ class MaximizeMenu( menuView.onRightSnapClickListener = onRightSnapClickListener menuView.onMenuHoverListener = onHoverListener menuView.onOutsideTouchListener = onOutsideTouchListener + val menuWidth = menuView.measureWidth() + val menuHeight = menuView.measureHeight() + menuPosition = positionSupplier(menuWidth, menuHeight) + val lp = WindowManager.LayoutParams( + menuWidth.toInt(), + menuHeight.toInt(), + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSPARENT + ) + lp.title = "Maximize Menu for Task=" + taskInfo.taskId + lp.setTrustedOverlay() viewHost.setView(menuView.rootView, lp) } @@ -268,7 +268,6 @@ class MaximizeMenu( private val sizeToggleDirection: SizeToggleDirection, immersiveConfig: ImmersiveConfig, showSnapOptions: Boolean, - private val menuHeight: Int, private val menuPadding: Int ) { val rootView = LayoutInflater.from(context) @@ -583,7 +582,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -604,7 +603,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply { duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, @@ -667,7 +666,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -688,7 +687,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply { + 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply { duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = FAST_OUT_LINEAR_IN }, @@ -792,6 +791,18 @@ class MaximizeMenu( ) } + /** Measure width of the root view of this menu. */ + fun measureWidth() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredWidth() + } + + /** Measure height of the root view of this menu. */ + fun measureHeight() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredHeight() + } + private fun deactivateSnapOptions() { // TODO(b/346440693): the background/colorStateList set on these buttons is overridden // to a static resource & color on manually tracked hover events, which defeats the @@ -1036,7 +1047,7 @@ interface MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu } @@ -1049,7 +1060,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu { return MaximizeMenu( @@ -1058,7 +1069,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController, taskInfo, decorWindowContext, - menuPosition, + positionSupplier, transactionSupplier ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 25dadfde274d..4002dc572897 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -361,6 +361,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } outResult.mRootView = rootView; + final boolean fontScaleChanged = mWindowDecorConfig != null + && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale; final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -375,7 +377,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode - || mDecorWindowContext == null) { + || mDecorWindowContext == null + || fontScaleChanged) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index d3de0f7c09b4..3d5e9495e29d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -16,26 +16,52 @@ package com.android.wm.shell.common; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import android.content.Context; +import android.content.res.Configuration; +import android.graphics.RectF; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; +import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; +import android.testing.TestableContext; +import android.util.SparseArray; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.IDisplayWindowListener; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestSyncExecutor; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.quality.Strictness; + +import java.util.function.Consumer; /** * Tests for the display controller. @@ -46,23 +72,163 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class DisplayControllerTests extends ShellTestCase { - - private @Mock Context mContext; - private @Mock IWindowManager mWM; - private @Mock ShellInit mShellInit; - private @Mock ShellExecutor mMainExecutor; - private @Mock DisplayManager mDisplayManager; + @Mock private IWindowManager mWM; + @Mock private ShellInit mShellInit; + @Mock private DisplayManager mDisplayManager; + @Mock private DisplayTopology mMockTopology; + @Mock private DisplayController.OnDisplaysChangedListener mListener; + private StaticMockitoSession mMockitoSession; + private TestSyncExecutor mMainExecutor; + private IDisplayWindowListener mDisplayContainerListener; + private Consumer<DisplayTopology> mCapturedTopologyListener; + private Display mMockDisplay; private DisplayController mController; + private static final int DISPLAY_ID_0 = 0; + private static final int DISPLAY_ID_1 = 1; + private static final RectF DISPLAY_ABS_BOUNDS_0 = new RectF(10, 10, 20, 20); + private static final RectF DISPLAY_ABS_BOUNDS_1 = new RectF(11, 11, 22, 22); @Before - public void setUp() { - MockitoAnnotations.initMocks(this); + public void setUp() throws RemoteException { + mMockitoSession = + ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(DesktopModeStatus.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + mContext = spy(new TestableContext( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() + .getContext(), null)); + + mMainExecutor = new TestSyncExecutor(); mController = new DisplayController( mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); + + mMockDisplay = mock(Display.class); + when(mMockDisplay.getDisplayAdjustments()).thenReturn( + new DisplayAdjustments(new Configuration())); + when(mDisplayManager.getDisplay(anyInt())).thenReturn(mMockDisplay); + when(mDisplayManager.getDisplayTopology()).thenReturn(mMockTopology); + doAnswer(invocation -> { + mDisplayContainerListener = invocation.getArgument(0); + return new int[]{DISPLAY_ID_0}; + }).when(mWM).registerDisplayWindowListener(any()); + doAnswer(invocation -> { + mCapturedTopologyListener = invocation.getArgument(1); + return null; + }).when(mDisplayManager).registerTopologyListener(any(), any()); + SparseArray<RectF> absoluteBounds = new SparseArray<>(); + absoluteBounds.put(DISPLAY_ID_0, DISPLAY_ABS_BOUNDS_0); + absoluteBounds.put(DISPLAY_ID_1, DISPLAY_ABS_BOUNDS_1); + when(mMockTopology.getAbsoluteBounds()).thenReturn(absoluteBounds); + } + + @After + public void tearDown() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), eq(mController)); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canEnterDesktopMode_registerListeners() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager).registerTopologyListener(eq(mMainExecutor), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canNotEnterDesktopMode_onlyRegisterDisplayWindowListener() + throws RemoteException { + ExtendedMockito.doReturn(false) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager, never()).registerTopologyListener(any(), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void addDisplayWindowListener_notifiesExistingDisplaysAndTopology() { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onTopologyChanged(eq(mMockTopology)); + } + + @Test + public void onDisplayAddedAndRemoved_updatesDisplayContexts() throws RemoteException { + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_1)); + assertNotNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mContext).createDisplayContext(eq(mMockDisplay)); + + mDisplayContainerListener.onDisplayRemoved(DISPLAY_ID_1); + + assertNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mListener).onDisplayRemoved(eq(DISPLAY_ID_1)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_updateDisplayLayout() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + mCapturedTopologyListener.accept(mMockTopology); + + assertEquals(DISPLAY_ABS_BOUNDS_0, mController.getDisplayLayout(DISPLAY_ID_0) + .globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1) + .globalBoundsDp()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_topologyBeforeDisplayAdded_appliesBoundsOnAdd() + throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mCapturedTopologyListener.accept(mMockTopology); + + assertNull(mController.getDisplayLayout(DISPLAY_ID_1)); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + assertEquals(DISPLAY_ABS_BOUNDS_0, + mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, + mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt new file mode 100644 index 000000000000..c91ef5e6b868 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowContainerTransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowContainerTransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowContainerTransactionSupplierTest { + + @Test + fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() { + val supplier = WindowContainerTransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is WindowContainerTransaction + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java index e3798e92c092..a6c35f1bd93c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -29,9 +29,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java index 85f1da5322ea..737735c9efcd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.mockito.Mockito.when; @@ -27,9 +27,6 @@ import android.view.DisplayInfo; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java index 080b0ae006ea..6bda2259b44c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -32,13 +32,6 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java index 304da75f870c..ad664acfdc37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -36,10 +36,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.function.TriConsumer; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java index b583acda1c9a..1756aad8fc9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; @@ -29,8 +29,6 @@ import android.graphics.Rect; import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDoubleTapHelper; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java index ac13d7ffcd61..3e71ab3e1ad4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; @@ -25,8 +25,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java new file mode 100644 index 000000000000..22a85fc49a4b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class FlexParallaxSpecTests { + ParallaxSpec mFlexSpec = new FlexParallaxSpec(); + + Rect mDisplayBounds = new Rect(0, 0, 1000, 1000); + Rect mRetreatingSurface = new Rect(0, 0, 1000, 1000); + Rect mRetreatingContent = new Rect(0, 0, 1000, 1000); + Rect mAdvancingSurface = new Rect(0, 0, 1000, 1000); + Rect mAdvancingContent = new Rect(0, 0, 1000, 1000); + boolean mIsLeftRightSplit; + boolean mTopLeftShrink; + + int mDimmingSide; + float mDimValue; + Point mRetreatingParallax = new Point(0, 0); + Point mAdvancingParallax = new Point(0, 0); + + @Mock DividerSnapAlgorithm mockSnapAlgorithm; + @Mock SnapTarget mockStartEdge; + @Mock SnapTarget mockFirstTarget; + @Mock SnapTarget mockMiddleTarget; + @Mock SnapTarget mockLastTarget; + @Mock SnapTarget mockEndEdge; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mockSnapAlgorithm.getDismissStartTarget()).thenReturn(mockStartEdge); + when(mockSnapAlgorithm.getFirstSplitTarget()).thenReturn(mockFirstTarget); + when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget); + when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget); + when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge); + + when(mockStartEdge.getPosition()).thenReturn(0); + when(mockFirstTarget.getPosition()).thenReturn(250); + when(mockMiddleTarget.getPosition()).thenReturn(500); + when(mockLastTarget.getPosition()).thenReturn(750); + when(mockEndEdge.getPosition()).thenReturn(1000); + } + + @Test + public void testHorizontalDragFromCenter() { + mIsLeftRightSplit = true; + simulateDragFromCenterToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromLeft() { + mIsLeftRightSplit = true; + simulateDragFromLeftToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isGreaterThan(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromRight() { + mIsLeftRightSplit = true; + + simulateDragFromRightToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + private void simulateDragFromCenterToLeft(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromCenterToRight(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToLeft(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = fullOffscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToCenter(int to) { + int from = 250; + + mRetreatingSurface = onscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToRight(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToLeft(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToCenter(int to) { + int from = 750; + + mRetreatingSurface = onscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToRight(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = fullOffscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private Rect flexOffscreenAppLeft(int pos) { + return new Rect(pos - (1000 - pos), 0, pos, 1000); + } + + private Rect onscreenAppLeft(int pos) { + return new Rect(0, 0, pos, 1000); + } + + private Rect fullOffscreenAppLeft(int pos) { + return new Rect(Math.min(0, pos - 750), 0, pos, 1000); + } + + private Rect flexOffscreenAppRight(int pos) { + return new Rect(pos, 0, pos * 2, 1000); + } + + private Rect onscreenAppRight(int pos) { + return new Rect(pos, 0, 1000, 1000); + } + + private Rect fullOffscreenAppRight(int pos) { + return new Rect(pos, 0, Math.max(pos + 750, 1000), 1000); + } + + private void calculateDimAndParallax(int from, int to) { + resetParallax(); + mTopLeftShrink = to < from; + mDimmingSide = mFlexSpec.getDimmingSide(to, mockSnapAlgorithm, mIsLeftRightSplit); + mDimValue = mFlexSpec.getDimValue(to, mockSnapAlgorithm); + mFlexSpec.getParallax(mRetreatingParallax, mAdvancingParallax, to, mockSnapAlgorithm, + mIsLeftRightSplit, mDisplayBounds, mRetreatingSurface, mRetreatingContent, + mAdvancingSurface, mAdvancingContent, mDimmingSide, mTopLeftShrink); + } + + private void resetParallax() { + mRetreatingParallax.set(0, 0); + mAdvancingParallax.set(0, 0); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt new file mode 100644 index 000000000000..a5f6ced20dc0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListenerFactory]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerFactoryTest : ShellTestCase() { + + @Test + fun `When invoked a ReachabilityGestureListenerFactory is created`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkReachabilityGestureListenerCreated() + } + } + + @Test + fun `Right parameters are used for creation`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkRightParamsAreUsed() + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) { + val robot = ReachabilityGestureListenerFactoryRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerFactoryRobotTest { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val factory: ReachabilityGestureListenerFactory + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private lateinit var obtainedResult: Any + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier) + } + + fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + obtainedResult = factory.createReachabilityGestureListener(taskId, token) + } + + fun checkReachabilityGestureListenerCreated(expected: Boolean = true) { + assertEquals(expected, obtainedResult is ReachabilityGestureListener) + } + + fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + with(obtainedResult as ReachabilityGestureListener) { + // Click outside the bounds + updateActivityBounds(Rect(0, 0, 10, 20)) + onDoubleTap(motionEventAt(50f, 100f)) + // WindowContainerTransactionSupplier is invoked to create a + // WindowContainerTransaction + verify(wctSupplier).get() + // Verify the right params are passed to startAppCompatReachability() + verify(wct).setReachabilityOffset( + token!!, + taskId, + 50, + 100 + ) + // startTransition() is invoked on Transitions with the right parameters + verify(transitions).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt new file mode 100644 index 000000000000..bc10ea578ffb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.compatui.letterbox.asMode +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListener]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerTest : ShellTestCase() { + + @Test + fun `Only events outside the bounds are handled`() { + runTestScenario { r -> + r.updateActivityBounds(Rect(0, 0, 100, 200)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = false, 50, 100) + r.verifyReachabilityTransitionStarted(expected = false) + r.verifyEventIsHandled(expected = false) + + r.updateActivityBounds(Rect(0, 0, 10, 50)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = true, 50, 100) + r.verifyReachabilityTransitionStarted(expected = true) + r.verifyEventIsHandled(expected = true) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) { + val robot = ReachabilityGestureListenerRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerRobotTest( + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val reachabilityListener: ReachabilityGestureListener + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private var eventHandled = false + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + reachabilityListener = + ReachabilityGestureListener( + taskId, + token, + transitions, + animationHandler, + wctSupplier + ) + } + + fun updateActivityBounds(activityBounds: Rect) { + reachabilityListener.updateActivityBounds(activityBounds) + } + + fun sendMotionEvent(x: Int, y: Int) { + eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat())) + } + + fun verifyReachabilityTransitionCreated( + expected: Boolean, + x: Int, + y: Int, + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + verify(wct, expected.asMode()).setReachabilityOffset( + token!!, + taskId, + x, + y + ) + } + + fun verifyReachabilityTransitionStarted(expected: Boolean = true) { + verify(transitions, expected.asMode()).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + + fun verifyEventIsHandled(expected: Boolean) { + assertEquals(expected, eventHandled) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 77cd1e56853d..e9f92cfd7c56 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -127,26 +127,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test - fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() { - val wct = WindowContainerTransaction() - val taskId = 1 - val isLastTask = false - whenever( - freeformTaskTransitionHandler.startMinimizedModeTransition( - any(), - anyInt(), - anyBoolean(), - ) - ) - .thenReturn(mock()) - - mixedHandler.startMinimizedModeTransition(wct, taskId, isLastTask) - - verify(freeformTaskTransitionHandler) - .startMinimizedModeTransition(eq(wct), eq(taskId), eq(isLastTask)) - } - - @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) fun startRemoveTransition_callsFreeformTaskTransitionHandler() { val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index ed40acfdb705..e2c3dda0d927 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -2922,6 +2922,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + val transition = Binder() + val runOnTransitStart = + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + runOnTransitStart(transition) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId = 0)) + } + + @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() @@ -2945,6 +2971,48 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) + verify(desksOrganizer).deactivateDesk(captor.firstValue, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) + } + + @Test fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt new file mode 100644 index 000000000000..aa4e9aaf248e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode.desktopwallpaperactivity + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.wm.shell.MockToken +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopWallpaperActivityTokenProvider] + * + * Usage: atest WMShellUnitTests:DesktopWallpaperActivityTokenProviderTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopWallpaperActivityTokenProviderTest : ShellTestCase() { + + private lateinit var provider: DesktopWallpaperActivityTokenProvider + private val DEFAULT_DISPLAY = 0 + private val SECONDARY_DISPLAY = 1 + + @Before + fun setUp() { + provider = DesktopWallpaperActivityTokenProvider() + } + + @Test + fun setToken_setsTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token) + } + + @Test + fun setToken_overwritesExistingTokenForDisplay() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token2) + } + + @Test + fun getToken_returnsNullForNonExistentDisplay() { + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_withToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(token) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_doesNothingForNonExistentDisplay() { + provider.removeToken(SECONDARY_DISPLAY) + + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_withNonExistentToken_doesNothing() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.removeToken(token2) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + } + + @Test + fun multipleDisplays_tokensAreIndependent() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, SECONDARY_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt index 79310c9ce6c2..4dcf669f4d25 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -227,4 +227,21 @@ class DesksTransitionObserverTest : ShellTestCase() { assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isFalse() } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithoutVisibleChange_updatesRepository() { + val transition = Binder() + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0), + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..2e389b7dd151 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone; + +import static android.view.MotionEvent.ACTION_BUTTON_PRESS; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PipTouchStateTest extends ShellTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + mMainExecutor = new TestShellExecutor(); + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + mDoubleTapCallbackTriggeredLatch::countDown, + mHoverExitCallbackTriggeredLatch::countDown, + mMainExecutor); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + } + + @Test + public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertTrue(mTouchState.isWaitingForDoubleTap()); + + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10); + mTouchState.scheduleDoubleTapTimeoutCallback(); + + mMainExecutor.flushAll(); + assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testDoubleTapDrag_doubleTapCanceled() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500)); + assertTrue(mTouchState.isDragging()); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTap_doubleTapRegistered() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertTrue(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index 741a0fdcf63c..4082ffd4ac0a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -22,8 +22,6 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES import android.window.DesktopModeFlags import androidx.test.filters.SmallTest import com.android.internal.R @@ -63,14 +61,12 @@ class DesktopModeStatusTest : ShellTestCase() { doReturn(context.contentResolver).whenever(mockContext).contentResolver resetDesktopModeFlagsCache() resetEnforceDeviceRestriction() - resetFlagOverride() } @After fun tearDown() { resetDesktopModeFlagsCache() resetEnforceDeviceRestriction() - resetFlagOverride() } @DisableFlags( @@ -246,18 +242,11 @@ class DesktopModeStatusTest : ShellTestCase() { cachedToggleOverride.set(null, null) } - private fun resetFlagOverride() { - Settings.Global.putString( - context.contentResolver, - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null - ) - } - private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) { - Settings.Global.putInt( - context.contentResolver, - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting - ) + val cachedToggleOverride = + DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride") + cachedToggleOverride.isAccessible = true + cachedToggleOverride.set(null, override) } private fun setDeviceEligibleForDesktopMode(eligible: Boolean) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 71c821dd9b71..c4f70ac2297f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -120,6 +120,7 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.MainCoroutineDispatcher; @@ -998,8 +999,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); assertTrue(decoration.isMaximizeMenuActive()); } @@ -1011,8 +1012,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new FakeMaximizeMenuFactory(menu)); decoration.setAppHeaderMaximizeButtonHovered(false); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(false); @@ -1050,8 +1051,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(true); @@ -1065,8 +1066,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); decoration.setAppHeaderMaximizeButtonHovered(true); @@ -1086,7 +1087,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(true), anyBoolean(), any(), @@ -1111,7 +1111,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(false), anyBoolean(), any(), @@ -1136,7 +1135,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(true), any(), @@ -1161,7 +1159,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(false), any(), @@ -1766,7 +1763,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, @NonNull DisplayController displayController, @NonNull ActivityManager.RunningTaskInfo taskInfo, - @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Context decorWindowContext, + @NonNull Function2<? super Integer,? super Integer,? extends PointF> + positionSupplier, @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { return mMaximizeMenu; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index aa1f82e3e4d8..af0162334440 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; @@ -386,6 +387,49 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any(), any()); } + + @Test + public void testReinflateViewsOnFontScaleChange() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo2.configuration.fontScale = taskInfo.configuration.fontScale + 1; + windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since the font scale has changed. + verify(windowDecor).releaseViews(any()); + } + + @Test + public void testViewNotReinflatedWhenFontScaleNotChanged() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since task info (and therefore the + // fontScale) has not changed. + verify(windowDecor, never()).releaseViews(any()); + } + @Test public void testAddViewHostViewContainer() { final Display defaultDisplay = mock(Display.class); diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp index ea9e9a2d4280..9aacdcb9ca92 100644 --- a/libs/androidfw/LocaleDataLookup.cpp +++ b/libs/androidfw/LocaleDataLookup.cpp @@ -14871,12 +14871,22 @@ static uint32_t findLatnParent(uint32_t packed_lang_region) { case 0x656E4154u: // en-AT -> en-150 case 0x656E4245u: // en-BE -> en-150 case 0x656E4348u: // en-CH -> en-150 + case 0x656E435Au: // en-CZ -> en-150 case 0x656E4445u: // en-DE -> en-150 case 0x656E444Bu: // en-DK -> en-150 + case 0x656E4553u: // en-ES -> en-150 case 0x656E4649u: // en-FI -> en-150 + case 0x656E4652u: // en-FR -> en-150 + case 0x656E4855u: // en-HU -> en-150 + case 0x656E4954u: // en-IT -> en-150 case 0x656E4E4Cu: // en-NL -> en-150 + case 0x656E4E4Fu: // en-NO -> en-150 + case 0x656E504Cu: // en-PL -> en-150 + case 0x656E5054u: // en-PT -> en-150 + case 0x656E524Fu: // en-RO -> en-150 case 0x656E5345u: // en-SE -> en-150 case 0x656E5349u: // en-SI -> en-150 + case 0x656E534Bu: // en-SK -> en-150 return 0x656E80A1u; case 0x65734152u: // es-AR -> es-419 case 0x6573424Fu: // es-BO -> es-419 diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index d3fc91b65829..7e1f2e2a3490 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -137,14 +137,6 @@ flag { } flag { - name: "shader_color_space" - is_exported: true - namespace: "core_graphics" - description: "API to set the working colorspace of a Shader or ColorFilter" - bug: "299670828" -} - -flag { name: "query_global_priority" namespace: "core_graphics" description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation." diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index c02508977eba..eadb9dea566f 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -266,17 +266,11 @@ static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete)); } -static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr, - jlong colorSpacePtr) { +static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); - auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr); sk_sp<SkShader> shader = builder->makeShader(matrix); ThrowIAE_IfNull(env, shader); - if (colorSpace) { - shader = shader->makeWithWorkingColorSpace(colorSpace); - ThrowIAE_IfNull(env, shader); - } return reinterpret_cast<jlong>(shader.release()); } @@ -385,7 +379,7 @@ static const JNINativeMethod gComposeShaderMethods[] = { static const JNINativeMethod gRuntimeShaderMethods[] = { {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer}, - {"nativeCreateShader", "(JJJ)J", (void*)RuntimeShader_create}, + {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create}, {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder}, {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", (void*)RuntimeShader_updateFloatArrayUniforms}, diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java index e1fbfea19235..892a8612d74a 100644 --- a/media/java/android/media/AudioDeviceVolumeManager.java +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -86,10 +86,10 @@ public class AudioDeviceVolumeManager { /** * @hide * Interface to receive volume changes on a device that behaves in absolute volume mode. - * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor, - * OnAudioDeviceVolumeChangeListener) - * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor, - * OnAudioDeviceVolumeChangeListener) + * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, boolean, Executor, + * OnAudioDeviceVolumeChangedListener) + * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, boolean, Executor, + * OnAudioDeviceVolumeChangedListener) */ public interface OnAudioDeviceVolumeChangedListener { /** @@ -203,6 +203,9 @@ public class AudioDeviceVolumeManager { * volume updates to apply on that device * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -211,13 +214,13 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { final ArrayList<VolumeInfo> volumes = new ArrayList<>(1); volumes.add(volume); - setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, - handlesVolumeAdjustment); + setDeviceAbsoluteMultiVolumeBehavior(device, volumes, handlesVolumeAdjustment, executor, + vclistener); } /** @@ -226,20 +229,20 @@ public class AudioDeviceVolumeManager { * registers a listener for receiving volume updates to apply on that device * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates - * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately - * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} - * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteMultiVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull List<VolumeInfo> volumes, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } @@ -249,11 +252,14 @@ public class AudioDeviceVolumeManager { * Configures a device to use absolute volume model, and registers a listener for receiving * volume updates to apply on that device. * - * Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no reliable - * way to set the device's volume to a percentage. + * <p>Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no + * reliable way to set the device's volume to a percentage. * * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -262,13 +268,13 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { final ArrayList<VolumeInfo> volumes = new ArrayList<>(1); volumes.add(volume); - setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, executor, vclistener, - handlesVolumeAdjustment); + setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, handlesVolumeAdjustment, + executor, vclistener); } /** @@ -276,11 +282,14 @@ public class AudioDeviceVolumeManager { * Configures a device to use absolute volume model applied to different volume types, and * registers a listener for receiving volume updates to apply on that device. * - * Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is + * <p>Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is * no reliable way to set the device's volume to a percentage. * * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -289,16 +298,16 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull List<VolumeInfo> volumes, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } /** * Base method for configuring a device to use absolute volume behavior, or one of its variants. - * See {@link AudioManager#AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors. + * See {@link AudioManager.AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors. * * @param behavior the variant of absolute device volume behavior to adopt */ diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 12d7f33a0d51..e01cb928e369 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1754,13 +1754,21 @@ public class AudioSystem @UnsupportedAppUsage public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, int codecFormat) { + return setDeviceConnectionState(attributes, state, codecFormat, false /*deviceSwitch*/); + } + + /** + * @hide + */ + public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, + int codecFormat, boolean deviceSwitch) { android.media.audio.common.AudioPort port = AidlConversion.api2aidl_AudioDeviceAttributes_AudioPort(attributes); Parcel parcel = Parcel.obtain(); port.writeToParcel(parcel, 0); parcel.setDataPosition(0); try { - return setDeviceConnectionState(state, parcel, codecFormat); + return setDeviceConnectionState(state, parcel, codecFormat, deviceSwitch); } finally { parcel.recycle(); } @@ -1769,7 +1777,10 @@ public class AudioSystem * @hide */ @UnsupportedAppUsage - public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat); + public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat, + boolean deviceSwitch); + + /** @hide */ @UnsupportedAppUsage public static native int getDeviceConnectionState(int device, String device_address); diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 0f24654879cd..021348153bb8 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -60,6 +60,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * RingtoneManager provides access to ringtones, notification, and other types @@ -810,9 +811,7 @@ public class RingtoneManager { // Don't set the stream type Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, false); - if (Flags.enableRingtoneHapticsCustomization() - && Utils.isRingtoneVibrationSettingsSupported(context) - && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) { + if (muteHapticChannelForVibration(context, ringtoneUri)) { audioAttributes = new AudioAttributes.Builder( audioAttributes).setHapticChannelsMuted(true).build(); } @@ -1305,4 +1304,19 @@ public class RingtoneManager { default: throw new IllegalArgumentException(); } } + + private static boolean muteHapticChannelForVibration(Context context, Uri ringtoneUri) { + final Uri vibrationUri = Utils.getVibrationUri(ringtoneUri); + // No vibration is specified + if (vibrationUri == null) { + return false; + } + // The user specified the synchronized pattern + if (Objects.equals(vibrationUri.toString(), Utils.SYNCHRONIZED_VIBRATION)) { + return false; + } + return Flags.enableRingtoneHapticsCustomization() + && Utils.isRingtoneVibrationSettingsSupported(context) + && hasHapticChannels(ringtoneUri); + } } diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index 11bd221ec696..d6e27b0ffa75 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -66,6 +66,8 @@ public class Utils { public static final String VIBRATION_URI_PARAM = "vibration_uri"; + public static final String SYNCHRONIZED_VIBRATION = "synchronized"; + /** * Sorts distinct (non-intersecting) range array in ascending order. * @throws java.lang.IllegalArgumentException if ranges are not distinct @@ -757,8 +759,8 @@ public class Utils { return null; } String filePath = vibrationUri.getPath(); - if (filePath == null) { - Log.w(TAG, "The file path is null."); + if (filePath == null || filePath.equals(Utils.SYNCHRONIZED_VIBRATION)) { + Log.w(TAG, "Ignore the vibration parsing for file:" + filePath); return null; } File vibrationFile = new File(filePath); diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt index bfaeb42d5a31..8d12f01e24ed 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt @@ -17,7 +17,9 @@ package com.android.settingslib.widget import android.os.Bundle +import android.view.LayoutInflater; import android.view.View +import android.view.ViewGroup; import androidx.annotation.CallSuper import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen @@ -27,6 +29,15 @@ import androidx.recyclerview.widget.RecyclerView abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() { @CallSuper + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return super.onCreateView(inflater, container, savedInstanceState) + } + + @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 4b0400fb3441..91ec83690722 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -889,6 +889,9 @@ <!-- Preference category for monitoring debugging development settings. [CHAR LIMIT=25] --> <string name="debug_monitoring_category">Monitoring</string> + <!-- Preference category to alter window management settings, [CHAR LIMIT=50] --> + <string name="window_management_category">Window Management</string> + <!-- UI debug setting: always enable strict mode? [CHAR LIMIT=25] --> <string name="strict_mode">Strict mode enabled</string> <!-- UI debug setting: show strict mode summary [CHAR LIMIT=50] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 97a345efd566..bb96041739eb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1988,6 +1988,17 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } /** + * @return {@code true} if {@code cachedBluetoothDevice} has member which is LeAudio device + */ + public boolean hasConnectedLeAudioMemberDevice() { + LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); + return leAudio != null && getMemberDevice().stream().anyMatch( + cachedDevice -> cachedDevice != null && cachedDevice.getDevice() != null + && leAudio.getConnectionStatus(cachedDevice.getDevice()) + == BluetoothProfile.STATE_CONNECTED); + } + + /** * @return {@code true} if {@code cachedBluetoothDevice} supports broadcast assistant profile */ public boolean isConnectedLeAudioBroadcastAssistantDevice() { diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 236654deefb5..f5c0233d56b1 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -8,7 +8,6 @@ achalke@google.com acul@google.com adamcohen@google.com aioana@google.com -alexchau@google.com alexflo@google.com andonian@google.com amiko@google.com @@ -91,10 +90,8 @@ rahulbanerjee@google.com rgl@google.com roosa@google.com saff@google.com -samcackett@google.com santie@google.com shanh@google.com -silvajordan@google.com snoeberger@google.com spdonghao@google.com steell@google.com @@ -106,7 +103,6 @@ thiruram@google.com tracyzhou@google.com tsuji@google.com twickham@google.com -uwaisashraf@google.com vadimt@google.com valiiftime@google.com vanjan@google.com @@ -121,3 +117,11 @@ yuandizhou@google.com yurilin@google.com yuzhechen@google.com zakcohen@google.com + +# Overview eng team +alexchau@google.com +samcackett@google.com +silvajordan@google.com +uwaisashraf@google.com +vinayjoglekar@google.com +willosborn@google.com diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 0ccb20ce3e3f..099d6b645a6d 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -974,6 +974,16 @@ flag { } flag { + name: "use_notif_inflation_thread_for_footer" + namespace: "systemui" + description: "use the @NotifInflation thread for FooterView and EmptyShadeView inflation" + bug: "375320642" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notify_power_manager_user_activity_background" namespace: "systemui" description: "Decide whether to notify the user activity to power manager in the background thread." @@ -1977,6 +1987,16 @@ flag { } flag { + name: "hardware_color_styles" + namespace: "systemui" + description: "Enables loading initial colors based ion hardware color" + bug: "347286986" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "shade_launch_accessibility" namespace: "systemui" description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events." @@ -2026,3 +2046,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "skip_hide_sensitive_notif_animation" + namespace: "systemui" + description: "Skip hide sensitive notification animation when the showing layout is not changed." + bug: "390624334" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 65cd3c79cd16..444389fb26ea 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -36,6 +36,7 @@ import android.view.ViewGroupOverlay import android.widget.FrameLayout import com.android.internal.jank.Cuj.CujType import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import java.util.LinkedList import kotlin.math.min import kotlin.math.roundToInt @@ -58,7 +59,7 @@ open class GhostedViewTransitionAnimatorController @JvmOverloads constructor( /** The view that will be ghosted and from which the background will be extracted. */ - private val ghostedView: View, + transitioningView: View, /** The [CujType] associated to this launch animation. */ private val launchCujType: Int? = null, @@ -75,11 +76,24 @@ constructor( private val isEphemeral: Boolean = false, private var interactionJankMonitor: InteractionJankMonitor = InteractionJankMonitor.getInstance(), + + /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */ + private val transitionRegistry: IViewTransitionRegistry? = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionRegistry.instance + } else { + null + } ) : ActivityTransitionAnimator.Controller { override val isLaunching: Boolean = true /** The container to which we will add the ghost view and expanding background. */ - override var transitionContainer = ghostedView.rootView as ViewGroup + override var transitionContainer: ViewGroup + get() = ghostedView.rootView as ViewGroup + set(_) { + // empty, should never be set to avoid memory leak + } + private val transitionContainerOverlay: ViewGroupOverlay get() = transitionContainer.overlay @@ -138,9 +152,33 @@ constructor( } } + /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */ + private val transitionToken = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionToken(transitioningView::class.java) + } else { + null + } + + /** The view that will be ghosted and from which the background will be extracted */ + private val ghostedView: View + get() = + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.getView(transitionToken!!) + } else { + _ghostedView + }!! + + private val _ghostedView = + if (Flags.decoupleViewControllerInAnimlib()) { + null + } else { + transitioningView + } + init { // Make sure the View we launch from implements LaunchableView to avoid visibility issues. - if (ghostedView !is LaunchableView) { + if (transitioningView !is LaunchableView) { throw IllegalArgumentException( "A GhostedViewLaunchAnimatorController was created from a View that does not " + "implement LaunchableView. This can lead to subtle bugs where the visibility " + @@ -148,6 +186,10 @@ constructor( ) } + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.register(transitionToken!!, transitioningView) + } + /** Find the first view with a background in [view] and its children. */ fun findBackground(view: View): Drawable? { if (view.background != null) { @@ -184,6 +226,7 @@ constructor( if (TransitionAnimator.returnAnimationsEnabled()) { ghostedView.removeOnAttachStateChangeListener(detachListener) } + transitionToken?.let { token -> transitionRegistry?.unregister(token) } } /** @@ -237,7 +280,7 @@ constructor( val insets = backgroundInsets val boundCorrections: Rect = if (ghostedView is LaunchableView) { - ghostedView.getPaddingForLaunchAnimation() + (ghostedView as LaunchableView).getPaddingForLaunchAnimation() } else { Rect() } @@ -387,8 +430,8 @@ constructor( if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. - ghostedView.setShouldBlockVisibilityChanges(false) - ghostedView.onActivityLaunchAnimationEnd() + (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false) + (ghostedView as LaunchableView).onActivityLaunchAnimationEnd() } else { // Make the ghosted view visible. We ensure that the view is considered VISIBLE by // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 @@ -398,7 +441,7 @@ constructor( ghostedView.invalidate() } - if (isEphemeral) { + if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { onDispose() } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt new file mode 100644 index 000000000000..af3ca87bf788 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.view.View + +/** Represents a Registry for holding a transitioning view mapped to a token */ +interface IViewTransitionRegistry { + + /** + * Registers the transitioning [view] mapped to a [token] + * + * @param token The token corresponding to the transitioning view + * @param view The view undergoing transition + */ + fun register(token: ViewTransitionToken, view: View) + + /** + * Unregisters the transitioned view from its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun unregister(token: ViewTransitionToken) + + /** + * Extracts a transitioning view from registry using its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun getView(token: ViewTransitionToken): View? + + /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + fun getViewToken(view: View): ViewTransitionToken? + + /** Event call to run on registry update (on both [register] and [unregister]) */ + fun onRegistryUpdate() +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt index 58c2a1c98ec4..86c7f76c6bee 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt @@ -24,14 +24,14 @@ import java.lang.ref.WeakReference * A registry to temporarily store the view being transitioned into a Dialog (using * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator]) */ -class ViewTransitionRegistry { +class ViewTransitionRegistry : IViewTransitionRegistry { /** * A map of a unique token to a WeakReference of the View being transitioned. WeakReference * ensures that Views are garbage collected whenever they become eligible and avoid any * memory leaks */ - private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } + private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } /** * A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to @@ -45,8 +45,7 @@ class ViewTransitionRegistry { } override fun onViewDetachedFromWindow(view: View) { - (view.getTag(R.id.tag_view_transition_token) - as? ViewTransitionToken)?.let { token -> unregister(token) } + getViewToken(view)?.let { token -> unregister(token) } } } } @@ -57,12 +56,12 @@ class ViewTransitionRegistry { * @param token unique token associated with the transitioning view * @param view view undergoing transitions */ - fun register(token: ViewTransitionToken, view: View) { + override fun register(token: ViewTransitionToken, view: View) { // token embedded as a view tag enables to use a single listener for all views view.setTag(R.id.tag_view_transition_token, token) view.addOnAttachStateChangeListener(listener) registry[token] = WeakReference(view) - emitCountForTrace() + onRegistryUpdate() } /** @@ -70,30 +69,51 @@ class ViewTransitionRegistry { * * @param token unique token associated with the transitioning view */ - fun unregister(token: ViewTransitionToken) { + override fun unregister(token: ViewTransitionToken) { registry.remove(token)?.let { it.get()?.let { view -> view.removeOnAttachStateChangeListener(listener) view.setTag(R.id.tag_view_transition_token, null) } it.clear() + onRegistryUpdate() } - emitCountForTrace() } /** * Access a view from registry using unique "token" associated with it * WARNING - this returns a StrongReference to the View stored in the registry */ - fun getView(token: ViewTransitionToken): View? { + override fun getView(token: ViewTransitionToken): View? { return registry[token]?.get() } /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + override fun getViewToken(view: View): ViewTransitionToken? { + return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token -> + getView(token)?.let { token } + } + } + + /** Event call to run on registry update (on both [register] and [unregister]) */ + override fun onRegistryUpdate() { + emitCountForTrace() + } + + /** * Utility function to emit number of non-null views in the registry whenever the registry is * updated (via [register] or [unregister]) */ private fun emitCountForTrace() { Trace.setCounter("transition_registry_view_count", registry.count().toLong()) } + + companion object { + val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() } + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt index c211a8ed1de2..e011df01504f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt @@ -16,17 +16,19 @@ package com.android.systemui.animation +import java.util.UUID + /** * A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be * unique as timestamp is appended to the token string * - * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or - * "ClassName_timestamp" + * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or + * "ClassName_UUID" * * @property token String value of a unique token */ @JvmInline value class ViewTransitionToken private constructor(val token: String) { - constructor() : this(token = System.currentTimeMillis().toString()) - constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}") + constructor() : this(token = UUID.randomUUID().toString()) + constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}") } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 64f3cb13662a..297995becfb2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -23,7 +23,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.dimensionResource import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey @@ -34,6 +34,13 @@ import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.controls.ui.composable.isLandscape +import com.android.systemui.media.controls.ui.controller.MediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED +import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED +import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.res.R @@ -42,10 +49,11 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader -import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.util.Utils import dagger.Lazy import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.flow.Flow @SysUISingleton @@ -58,6 +66,8 @@ constructor( private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, private val keyguardClockViewModel: KeyguardClockViewModel, + private val mediaCarouselController: MediaCarouselController, + @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>, ) : Overlay { override val key = Overlays.NotificationsShade @@ -84,6 +94,11 @@ constructor( viewModel.notificationsPlaceholderViewModelFactory.create() } + val usingCollapsedLandscapeMedia = + Utils.useCollapsedMediaInLandscape(LocalResources.current) + mediaHost.get().expansion = + if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED + OverlayShade( panelElement = NotificationsShade.Elements.Panel, alignmentOnWideScreens = Alignment.TopStart, @@ -96,9 +111,7 @@ constructor( } OverlayShadeHeader( viewModel = headerViewModel, - modifier = - Modifier.element(NotificationsShade.Elements.StatusBar) - .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), + modifier = Modifier.element(NotificationsShade.Elements.StatusBar), ) }, ) { @@ -116,6 +129,19 @@ constructor( } } + MediaCarousel( + isVisible = viewModel.showMedia, + mediaHost = mediaHost.get(), + carouselController = mediaCarouselController, + usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia, + modifier = + Modifier.padding( + top = notificationStackPadding, + start = notificationStackPadding, + end = notificationStackPadding, + ), + ) + NotificationScrollingStack( shadeSession = shadeSession, stackScrollView = stackScrollView.get(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index 8aa5bc7b7c6f..60eaa28e3822 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -21,6 +21,7 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey +import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.observableTransitionState import com.android.systemui.scene.shared.model.SceneDataSource import kotlinx.coroutines.CoroutineScope @@ -103,4 +104,8 @@ class SceneTransitionLayoutDataSource( override fun instantlyHideOverlay(overlay: OverlayKey) { state.snapTo(overlays = state.currentOverlays - overlay) } + + override fun freezeAndAnimateToCurrentState() { + (state.transitionState as? TransitionState.Transition)?.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index bd811814eb24..4140a956182c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -18,9 +18,7 @@ package com.android.keyguard; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -28,8 +26,6 @@ import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; import android.testing.TestableLooper; -import android.text.Editable; -import android.text.TextWatcher; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -99,19 +95,6 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { } @Test - public void textChanged_AnnounceForAccessibility() { - ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass( - TextWatcher.class); - mMessageAreaController.onViewAttached(); - verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture()); - - textWatcherArgumentCaptor.getValue().afterTextChanged( - Editable.Factory.getInstance().newEditable("abc")); - verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class)); - verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong()); - } - - @Test public void testSetBouncerVisible() { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index e492c63d095c..052d520ac92f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt @@ -17,16 +17,20 @@ package com.android.systemui.animation import android.os.HandlerThread +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.View import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.view.LaunchableFrameLayout import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,6 +44,14 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { } private val interactionJankMonitor = FakeInteractionJankMonitor() + private lateinit var transitionRegistry: FakeViewTransitionRegistry + private lateinit var transitioningView: View + + @Before + fun setup() { + transitioningView = LaunchableFrameLayout(mContext) + transitionRegistry = FakeViewTransitionRegistry() + } @Test fun animatingOrphanViewDoesNotCrash() { @@ -67,7 +79,7 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { parent.addView((launchView)) val launchController = GhostedViewTransitionAnimatorController( - launchView, + launchView, launchCujType = LAUNCH_CUJ, returnCujType = RETURN_CUJ, interactionJankMonitor = interactionJankMonitor @@ -96,6 +108,26 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ) } + @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testViewsAreRegisteredInTransitionRegistry() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isNotEmpty() + } + + @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testNotUseRegistryIfDecouplingFlagDisabled() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isEmpty() + } + /** * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and * allows inspection. @@ -117,4 +149,30 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { return true } } + + private class FakeViewTransitionRegistry : IViewTransitionRegistry { + + val registry = mutableMapOf<ViewTransitionToken, View>() + + override fun register(token: ViewTransitionToken, view: View) { + registry[token] = view + view.setTag(R.id.tag_view_transition_token, token) + } + + override fun unregister(token: ViewTransitionToken) { + registry.remove(token)?.setTag(R.id.tag_view_transition_token, null) + } + + override fun getView(token: ViewTransitionToken): View? { + return registry[token] + } + + override fun getViewToken(view: View): ViewTransitionToken? { + return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken + } + + override fun onRegistryUpdate() { + //empty + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt index 1a3606e413cc..da25bcac6c95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos 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 @@ -44,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -65,7 +67,7 @@ class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestC private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val communalSceneRepository = kosmos.fakeCommunalSceneRepository - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val underTest: CommunalTransitionViewModel by lazy { kosmos.communalTransitionViewModel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 329627af8ec2..e36d2455d316 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -61,6 +61,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -73,6 +74,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { @@ -80,21 +82,26 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { private val testScope: TestScope = kosmos.testScope private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor + private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val fakeUserRepository = kosmos.fakeUserRepository private val facePropertyRepository = kosmos.facePropertyRepository - private val fakeDeviceEntryFingerprintAuthInteractor = - kosmos.deviceEntryFingerprintAuthInteractor - private val powerInteractor = kosmos.powerInteractor private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository - private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor + private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor } private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager - private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor + + private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } + private val fakeDeviceEntryFingerprintAuthInteractor by lazy { + kosmos.deviceEntryFingerprintAuthInteractor + } + private val powerInteractor by lazy { kosmos.powerInteractor } + private val deviceEntryFaceAuthStatusInteractor by lazy { + kosmos.deviceEntryFaceAuthStatusInteractor + } @Before fun setup() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 183e4d6f624b..98486a22854a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -541,7 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), + simpleShortcutCategory(MultiTasking, "Split screen", "Use full screen"), simpleShortcutCategory( MultiTasking, "Split screen", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 29e95cd911f8..0b42898d82ae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState @@ -46,20 +47,31 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardTransitionInteractorTest : SysuiTestCase() { - val kosmos = testKosmos() - val underTest = kosmos.keyguardTransitionInteractor - val repository = kosmos.fakeKeyguardTransitionRepository - val testScope = kosmos.testScope + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var underTest: KeyguardTransitionInteractor + + @Before + fun setup() { + repository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.keyguardTransitionInteractor + } @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt index 26fe379f00bf..3cff0fc96af4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -1358,6 +1358,45 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** + * When a transition away from the lockscreen is interrupted by an `Idle(Lockscreen)`, a + * `sceneState` that was set during the transition is consumed and passed to KTF. + */ + @Test + fun transition_from_ls_scene_sceneStateSet_then_interrupted_by_idle_on_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Gone, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false), + ) + progress.value = 0.4f + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + val sceneState = KeyguardState.AOD + underTest.onSceneAboutToChange(toScene = Scenes.Lockscreen, sceneState = sceneState) + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.AOD, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + private fun assertTransition( step: TransitionStep, from: KeyguardState? = null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt deleted file mode 100644 index 052dfd52887f..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.viewmodel - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.kosmos.collectValues -import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class DozingToDreamingTransitionViewModelTest : SysuiTestCase() { - val kosmos = testKosmos() - - val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel } - - @Test - fun notificationShadeAlpha() = - kosmos.runTest { - val values by collectValues(underTest.notificationAlpha) - assertThat(values).isEmpty() - - fakeKeyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.DOZING, - to = KeyguardState.DREAMING, - testScope, - ) - - assertThat(values).isNotEmpty() - values.forEach { assertThat(it).isEqualTo(0) } - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 25c157208513..0b34a01a0fe0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.authController import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -47,6 +48,7 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider @@ -123,6 +125,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun notificationsPlacement_dualShadeSmallClock_below() = kosmos.runTest { setupState( @@ -135,6 +138,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun notificationsPlacement_dualShadeLargeClock_topStart() = kosmos.runTest { setupState( @@ -156,6 +160,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun areNotificationsVisible_dualShadeWideOnLockscreen_true() = kosmos.runTest { setupState( @@ -298,6 +303,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa ) { val isShadeLayoutWide by collectLastValue(kosmos.shadeRepository.isShadeLayoutWide) val collectedClockSize by collectLastValue(kosmos.keyguardClockInteractor.clockSize) + val collectedShadeMode by collectLastValue(kosmos.shadeModeInteractor.shadeMode) when (shadeMode) { ShadeMode.Dual -> kosmos.enableDualShade(wideLayout = shadeLayoutWide) ShadeMode.Single -> kosmos.enableSingleShade() @@ -309,6 +315,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa if (shadeLayoutWide != null) { assertThat(isShadeLayoutWide).isEqualTo(shadeLayoutWide) } + assertThat(collectedShadeMode).isEqualTo(shadeMode) assertThat(collectedClockSize).isEqualTo(clockSize) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java index f5a71113235a..a7a0c24e2163 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java @@ -33,8 +33,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.WallpaperColors; -import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; @@ -68,7 +66,7 @@ import java.util.stream.Collectors; @SmallTest @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class MediaOutputAdapterTest extends SysuiTestCase { +public class MediaOutputAdapterLegacyTest extends SysuiTestCase { private static final String TEST_DEVICE_NAME_1 = "test_device_name_1"; private static final String TEST_DEVICE_NAME_2 = "test_device_name_2"; @@ -92,8 +90,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Captor private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor; - private MediaOutputAdapter mMediaOutputAdapter; - private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder; + private MediaOutputAdapterLegacy mMediaOutputAdapter; + private MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy mViewHolder; private List<MediaDevice> mMediaDevices = new ArrayList<>(); private List<MediaItem> mMediaItems = new ArrayList<>(); MediaOutputSeekbar mSpyMediaOutputSeekbar; @@ -124,9 +122,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true)); mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false)); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar); } @@ -150,9 +148,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindPairNew_verifyView() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); @@ -175,9 +173,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -197,9 +195,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(null); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -225,7 +223,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() { when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -245,7 +243,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -264,7 +262,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -276,7 +274,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() { when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -294,7 +292,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -315,7 +313,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -348,7 +346,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false); when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -498,7 +496,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT); when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -522,7 +520,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -545,7 +543,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_NONE); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -567,7 +565,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT); when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -627,9 +625,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); mMediaOutputAdapter.updateItems(); @@ -645,9 +643,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -663,11 +661,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); - MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder); + MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy( + mViewHolder); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0); @@ -684,11 +683,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); - MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder); + MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy( + mViewHolder); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1); spyMediaDeviceViewHolder.mContainerLayout.performClick(); @@ -715,7 +715,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice2); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -859,7 +859,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectedMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice1)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -899,16 +899,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void updateColorScheme_triggerController() { - WallpaperColors wallpaperColors = WallpaperColors.fromBitmap( - Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)); - - mMediaOutputAdapter.updateColorScheme(wallpaperColors, true); - - verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true); - } - - @Test public void updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() { mMediaOutputAdapter.updateItems(); List<MediaItem> updatedList = new ArrayList<>(); @@ -990,7 +980,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void multipleSelectedDevices_verifySessionView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -1011,7 +1001,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void multipleSelectedDevices_verifyCollapsedView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -1024,13 +1014,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void multipleSelectedDevices_expandIconClicked_verifyInitialView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mEndTouchArea.performClick(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -1047,13 +1037,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mEndTouchArea.performClick(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -1075,7 +1065,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices); when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>()); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 0bba8bba2419..b23cd5e5547f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.notifications.ui.viewmodel +import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,6 +29,8 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData 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.powerInteractor @@ -39,10 +42,13 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -50,6 +56,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -155,6 +162,36 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { assertThat(underTest.showClock).isFalse() } + @Test + fun showMedia_activeMedia_true() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + runCurrent() + + assertThat(underTest.showMedia).isTrue() + } + + @Test + fun showMedia_noActiveMedia_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false)) + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } + + @Test + fun showMedia_qsDisabled_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + kosmos.fakeDisableFlagsRepository.disableFlags.update { + it.copy(disable2 = DISABLE2_QUICK_SETTINGS) + } + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } + private fun TestScope.lockDevice() { val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.powerInteractor.setAsleepForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt index c775bfd75f6e..9e400a6c0a4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -34,6 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class GridLayoutTypeInteractorTest : SysuiTestCase() { val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt index 2e7aeb433e04..9fe783b98046 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.QSColumnsRepository @@ -76,6 +77,7 @@ class QSColumnsInteractorTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun withDualShade_returnsCorrectValue() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt index fdbf42c9afd8..d5e502e99de5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt @@ -21,6 +21,7 @@ import android.content.res.mainResources import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS @@ -36,6 +37,7 @@ import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -43,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(ParameterizedAndroidJunit4::class) @SmallTest class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() { @@ -63,6 +66,7 @@ class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : Sysui } @Test + @EnableSceneContainer fun shouldMediaShowInRow() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt index 241cdbfbef83..4912c319bf2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testCase @@ -88,6 +89,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationNull_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -111,6 +113,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -133,6 +136,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 80c7026b0cea..23a0f6224fb7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -757,4 +757,46 @@ class SceneInteractorTest : SysuiTestCase() { verify(processor, never()).onSceneAboutToChange(any(), any()) } + + @Test + fun changeScene_sameScene_withFreeze() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "test", + sceneState = KeyguardState.AOD, + forceSettleToTargetScene = true, + ) + + verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(1) + } + + @Test + fun changeScene_sameScene_withoutFreeze() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "test", + sceneState = KeyguardState.AOD, + forceSettleToTargetScene = false, + ) + + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt index 668f568d7f46..d26e195d360a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos @@ -31,6 +32,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class ShadeModeInteractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -80,7 +82,7 @@ class ShadeModeInteractorImplTest : SysuiTestCase() { } @Test - fun isDualShade_settingEnabled_returnsTrue() = + fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() = testScope.runTest { // TODO(b/391578667): Add a test case for user switching once the bug is fixed. val shadeMode by collectLastValue(underTest.shadeMode) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt index b8f66acf6413..dde867814159 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.any 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.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -59,6 +60,7 @@ import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -103,6 +105,7 @@ class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableSceneContainer fun hydrateShadeMode_dualShadeEnabled() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index aaa9b58a45df..7cf817a06225 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -49,8 +49,11 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith @@ -286,13 +289,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.shortCriticalText = "Arrived" this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -340,13 +345,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeHiddenIfAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = true this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -370,13 +377,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeShownIfNotAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = false this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -397,18 +406,117 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_basicTime_isShortTimeDelta() = + fun chips_basicTime_timeInFuture_isShortTimeDelta() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 13.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 500, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsNow_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.seconds.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeInPast_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime - 2.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( listOf( activeNotificationModel( @@ -421,6 +529,45 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + // Not necessarily the behavior we *want* to have, but it's the currently implemented behavior. + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsInFuture_thenTimeAdvances_stillShortTimeDelta() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 3.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + + fakeSystemClock.advanceTime(5.minutes.inWholeMilliseconds) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) } @@ -429,12 +576,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countUpTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountUp, ) } @@ -457,12 +606,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountDown, ) } @@ -485,12 +636,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -517,12 +670,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -556,12 +711,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -569,7 +726,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { PromotedNotificationContentModel.Builder("other notif").apply { this.time = PromotedNotificationContentModel.When( - time = 654321L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -610,12 +767,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt index 1a5f57dd43f8..6409a20d5156 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt @@ -17,100 +17,119 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import android.graphics.drawable.Drawable -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.parameterizeSceneContainerFlag +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +@RunWith(ParameterizedAndroidJunit4::class) @SmallTest -@RunWith(AndroidJUnit4::class) -class MediaControlChipInteractorTest : SysuiTestCase() { - +class MediaControlChipInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest = kosmos.mediaControlChipInteractor + private val mediaFilterRepository = kosmos.mediaFilterRepository + private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipInteractor } + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + @Before + fun setUp() { + kosmos.underTest.initialize() + MockitoAnnotations.initMocks(this) + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Test - fun mediaControlModel_noActiveMedia_null() = + fun mediaControlChipModel_noActiveMedia_null() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) assertThat(model).isNull() } @Test - fun mediaControlModel_activeMedia_notNull() = + fun mediaControlChipModel_activeMedia_notNull() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val userMedia = MediaData(active = true) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() } @Test - fun mediaControlModel_mediaRemoved_null() = + fun mediaControlChipModel_mediaRemoved_null() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val userMedia = MediaData(active = true) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() - assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) - .isTrue() - mediaFilterRepository.addMediaDataLoadingState( - MediaDataLoadingModel.Removed(instanceId) - ) + removeMedia(userMedia) assertThat(model).isNull() } @Test - fun mediaControlModel_songNameChanged_emitsUpdatedModel() = + fun mediaControlChipModel_songNameChanged_emitsUpdatedModel() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.songName).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.songName).isEqualTo(newSongName) } @Test - fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() = + fun mediaControlChipModel_playPauseActionChanges_emitsUpdatedModel() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val mockDrawable = mock<Drawable>() @@ -123,9 +142,7 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) @@ -139,15 +156,15 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val updatedMediaButton = MediaButton(playOrPause = newAction) val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.playOrPause).isEqualTo(newAction) } @Test - fun mediaControlModel_playPauseActionRemoved_playPauseNull() = + fun mediaControlChipModel_playPauseActionRemoved_playPauseNull() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val mockDrawable = mock<Drawable>() @@ -160,16 +177,36 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val updatedUserMedia = userMedia.copy(semanticActions = MediaButton()) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.playOrPause).isNull() } + + private fun updateMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + mediaFilterRepository.addSelectedUserMediaEntry(mediaData) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + } else { + kosmos.underTest.updateMediaControlChipModelLegacy(mediaData) + } + } + + private fun removeMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, mediaData) + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Removed(instanceId) + ) + } else { + kosmos.underTest.updateMediaControlChipModelLegacy(null) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt index 8650e4b8cfce..d36dbbe8d36f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt @@ -16,26 +16,57 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.parameterizeSceneContainerFlag +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class MediaControlChipViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest = kosmos.mediaControlChipViewModel + private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor } + private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel } + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mediaControlChipInteractor.initialize() + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Test fun chip_noActiveMedia_IsHidden() = @@ -51,10 +82,7 @@ class MediaControlChipViewModelTest : SysuiTestCase() { val chip by collectLastValue(underTest.chip) val userMedia = MediaData(active = true, song = "test") - val instanceId = userMedia.instanceId - - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) } @@ -67,16 +95,25 @@ class MediaControlChipViewModelTest : SysuiTestCase() { val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) - val instanceId = userMedia.instanceId - - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) - + updateMedia(userMedia) + assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } + + private fun updateMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(mediaData) + kosmos.mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(instanceId) + ) + } else { + mediaControlChipInteractor.updateMediaControlChipModelLegacy(mediaData) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java index d66b010daefd..a58f7f72f08a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java @@ -51,8 +51,10 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.people.NotificationPersonExtractor; import com.android.systemui.util.DeviceConfigProxyFake; +import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java index 77fd06757595..8520508c7611 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java @@ -132,7 +132,7 @@ public class DynamicChildBindControllerTest extends SysuiTestCase { LayoutInflater inflater = LayoutInflater.from(mContext); inflater.setFactory2( new RowInflaterTask.RowAsyncLayoutInflater(entry, new FakeSystemClock(), mock( - RowInflaterTaskLogger.class))); + RowInflaterTaskLogger.class), mContext.getUser())); ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, null); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt new file mode 100644 index 000000000000..426af264da07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class BundleEntryTest : SysuiTestCase() { + private lateinit var underTest: BundleEntry + + @get:Rule + val setFlagsRule = SetFlagsRule() + + @Before + fun setUp() { + underTest = BundleEntry("key") + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getParent_adapter() { + assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isTopLevelEntry_adapter() { + assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getRow_adapter() { + assertThat(underTest.entryAdapter.row).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_adapter() { + assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getKey_adapter() { + assertThat(underTest.entryAdapter.key).isEqualTo("key") + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java index 8e95ac599ce1..76e2d619a4df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java @@ -30,6 +30,9 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -39,8 +42,10 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -57,6 +62,9 @@ public class HighPriorityProviderTest extends SysuiTestCase { @Mock private GroupMembershipManager mGroupMembershipManager; private HighPriorityProvider mHighPriorityProvider; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -210,6 +218,7 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testIsHighPriority_checkChildrenToCalculatePriority_legacy() { // GIVEN: a summary with low priority has a highPriorityChild and a lowPriorityChild final NotificationEntry summary = createNotifEntry(false); @@ -247,20 +256,18 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test - public void testIsHighPriority_checkChildrenToCalculatePriority() { + public void testIsHighPriority_checkChildrenViewsToCalculatePriority() { // GIVEN: // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild // NotificationEntry = highPriorityChild + List<NotificationEntry> children = List.of(createNotifEntry(false), createNotifEntry(true)); final NotificationEntry lowPrioritySummary = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(children) .build(); - when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( - new ArrayList<>( - List.of( - createNotifEntry(false), - createNotifEntry(true)))); + when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(children); // THEN the GroupEntry parentEntry is high priority since it has a high priority child assertTrue(mHighPriorityProvider.isHighPriority(parentEntry)); @@ -272,10 +279,11 @@ public class HighPriorityProviderTest extends SysuiTestCase { // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild final NotificationEntry lowPrioritySummary = createNotifEntry(false); + final NotificationEntry lowPriorityChild = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(List.of(lowPriorityChild)) .build(); - final NotificationEntry lowPriorityChild = createNotifEntry(false); when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( new ArrayList<>(List.of(lowPriorityChild))); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt index e93c74252251..7fa157fa7cb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt @@ -27,14 +27,29 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry import com.android.systemui.statusbar.notification.collection.buildEntry import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.notifPipeline +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -59,7 +74,13 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { fun setup() { allowTestableLooperAsMainThread() - colorizedFgsCoordinator = ColorizedFgsCoordinator() + kosmos.statusBarNotificationChipsInteractor.start() + + colorizedFgsCoordinator = + ColorizedFgsCoordinator( + kosmos.applicationCoroutineScope, + kosmos.promotedNotificationsInteractor, + ) colorizedFgsCoordinator.attach(notifPipeline) sectioner = colorizedFgsCoordinator.sectioner } @@ -178,6 +199,37 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { verify(notifPipeline, never()).addPromoter(any()) } + @Test + @EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, + ) + fun comparatorPutsCallBeforeOther() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + kosmos.renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + + // VERIFY that the comparator puts the call before the ron + assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0) + // VERIFY that the comparator puts the ron before the other + assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0) + } + private fun makeCallStyle(): Notification.CallStyle { val pendingIntent = PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index db5921d8bd36..3dd0982ba2ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -17,23 +17,29 @@ package com.android.systemui.statusbar.notification.collection.render import android.os.Build +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.assertLogsWtf +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Assume import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never @@ -44,6 +50,9 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class GroupExpansionManagerTest : SysuiTestCase() { + @get:Rule + val setFlagsRule = SetFlagsRule() + private lateinit var underTest: GroupExpansionManagerImpl private val dumpManager: DumpManager = mock() @@ -52,8 +61,8 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private lateinit var beforeRenderListListener: OnBeforeRenderListListener - private val summary1 = notificationEntry("foo", 1) - private val summary2 = notificationEntry("bar", 1) + private val summary1 = notificationSummaryEntry("foo", 1) + private val summary2 = notificationSummaryEntry("bar", 1) private val entries = listOf<ListEntry>( GroupEntryBuilder() @@ -82,15 +91,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { private fun notificationEntry(pkg: String, id: Int) = NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() } + private fun notificationSummaryEntry(pkg: String, id: Int) = + NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build() + .apply { row = mock() } + @Before fun setUp() { whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) + whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter)) + .thenReturn(summary1.entryAdapter) + whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter)) + .thenReturn(summary2.entryAdapter) + underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager) } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun notifyOnlyOnChange() { var listenerCalledCount = 0 underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } @@ -108,6 +127,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun notifyOnlyOnChange_withEntryAdapter() { + var listenerCalledCount = 0 + underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } + + underTest.setGroupExpanded(summary1.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(0) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(1) + underTest.setGroupExpanded(summary2.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary2.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(3) + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun expandUnattachedEntry() { // First, expand the entry when it is attached. underTest.setGroupExpanded(summary1, true) @@ -122,6 +160,22 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun expandUnattachedEntryAdapter() { + // First, expand the entry when it is attached. + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue() + + // Un-attach it, and un-expand it. + NotificationEntryBuilder.setNewParent(summary1, null) + underTest.setGroupExpanded(summary1.entryAdapter, false) + + // Expanding again should throw. + assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) } + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun syncWithPipeline() { underTest.attach(pipeline) beforeRenderListListener = withArgCaptor { @@ -143,4 +197,28 @@ class GroupExpansionManagerTest : SysuiTestCase() { verify(listener).onGroupExpansionChange(summary1.row, false) verifyNoMoreInteractions(listener) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun syncWithPipeline_withEntryAdapter() { + underTest.attach(pipeline) + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + val listener: OnGroupExpansionChangeListener = mock() + underTest.registerGroupExpansionChangeListener(listener) + + beforeRenderListListener.onBeforeRenderList(entries) + verify(listener, never()).onGroupExpansionChange(any(), any()) + + // Expand one of the groups. + underTest.setGroupExpanded(summary1.entryAdapter, true) + verify(listener).onGroupExpansionChange(summary1.row, true) + + // Empty the pipeline list and verify that the group is no longer expanded. + beforeRenderListListener.onBeforeRenderList(emptyList()) + verify(listener).onGroupExpansionChange(summary1.row, false) + verifyNoMoreInteractions(listener) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index 2cbcc5a8d925..dcbf44e6e301 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -16,34 +16,46 @@ package com.android.systemui.statusbar.notification.collection.render +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.google.common.truth.Truth.assertThat +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { + + @get:Rule + val setFlagsRule = SetFlagsRule() + private var underTest = GroupMembershipManagerImpl() @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_topLevel() { val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_noParent() { val noParentEntry = NotificationEntryBuilder().setParent(null).build() assertThat(underTest.isChildInGroup(noParentEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_summary() { val groupKey = "group" val summary = @@ -57,12 +69,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_summary() { val groupKey = "group" val summary = @@ -76,6 +90,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_child() { val groupKey = "group" val summary = @@ -90,12 +105,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.getGroupSummary(entry)).isNull() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_summary() { val groupKey = "group" val summary = @@ -109,6 +126,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_child() { val groupKey = "group" val summary = @@ -121,4 +139,104 @@ class GroupMembershipManagerTest : SysuiTestCase() { assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_topLevel() { + val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_noParent() { + val noParentEntry = NotificationEntryBuilder().setParent(null).build() + assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 339f8fac3820..e22acd53e584 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -106,11 +106,15 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME) this.addOverride( R.integer.heads_up_notification_minimum_time, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, ) this.addOverride( R.integer.heads_up_notification_minimum_time_with_throttling, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, + ) + this.addOverride( + R.integer.heads_up_notification_minimum_time_for_user_initiated, + TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED, ) this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME) this.addOverride( @@ -414,7 +418,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun testRemoveNotification_beforeMinimumDisplayTime() { + fun testRemoveNotification_beforeMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) @@ -429,18 +433,22 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(removedImmediately).isFalse() assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() } @Test - fun testRemoveNotification_afterMinimumDisplayTime() { + fun testRemoveNotification_afterMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) underTest.showNotification(entry) - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() @@ -455,6 +463,57 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_beforeMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "beforeMinimumDisplayTime", + ) + assertThat(removedImmediately).isFalse() + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_afterMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "afterMinimumDisplayTime", + ) + + assertThat(removedImmediately).isTrue() + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test fun testRemoveNotification_releaseImmediately() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) @@ -1047,16 +1106,21 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } companion object { - const val TEST_TOUCH_ACCEPTANCE_TIME = 200 - const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 - const val TEST_EXTENSION_TIME = 500 + private const val TEST_TOUCH_ACCEPTANCE_TIME = 200 + private const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 + private const val TEST_EXTENSION_TIME = 500 - const val TEST_MINIMUM_DISPLAY_TIME = 400 - const val TEST_AUTO_DISMISS_TIME = 600 - const val TEST_STICKY_AUTO_DISMISS_TIME = 800 + private const val TEST_MINIMUM_DISPLAY_TIME_DEFAULT = 400 + private const val TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED = 500 + private const val TEST_AUTO_DISMISS_TIME = 600 + private const val TEST_STICKY_AUTO_DISMISS_TIME = 800 init { - assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT) + .isLessThan(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + .isLessThan(TEST_AUTO_DISMISS_TIME) assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME) assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt new file mode 100644 index 000000000000..75f5de0118d4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.people + +import android.app.Notification +import android.app.NotificationChannel +import android.content.pm.ShortcutInfo +import android.service.notification.NotificationListenerService.Ranking +import android.service.notification.StatusBarNotification +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mock + + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PeopleNotificationIdentifierTest : SysuiTestCase() { + + private lateinit var underTest: PeopleNotificationIdentifierImpl + + private val summary1 = notificationEntry("foo", 1, summary = true) + private val summary2 = notificationEntry("bar", 1, summary = true) + private val entries = + listOf<GroupEntry>( + GroupEntryBuilder() + .setSummary(summary1) + .setChildren( + listOf( + notificationEntry("foo", 2), + notificationEntry("foo", 3), + notificationEntry("foo", 4) + ) + ) + .build(), + GroupEntryBuilder() + .setSummary(summary2) + .setChildren( + listOf( + notificationEntry("bar", 2), + notificationEntry("bar", 3), + notificationEntry("bar", 4) + ) + ) + .build() + ) + + private fun notificationEntry( + pkg: String, + id: Int, + summary: Boolean = false + ): NotificationEntry { + val sbn = mock(StatusBarNotification::class.java) + Mockito.`when`(sbn.key).thenReturn("key") + Mockito.`when`(sbn.notification).thenReturn(mock(Notification::class.java)) + if (summary) + Mockito.`when`(sbn.notification.isGroupSummary).thenReturn(true) + return NotificationEntryBuilder().setPkg(pkg) + .setId(id) + .setSbn(sbn) + .build().apply { + row = mock(ExpandableNotificationRow::class.java) + } + } + + private fun personRanking(entry: NotificationEntry, personType: Int): Ranking { + val channel = NotificationChannel("person", "person", 4) + channel.setConversationId("parent", "person") + channel.setImportantConversation(true) + + val br = RankingBuilder(entry.ranking) + + when (personType) { + TYPE_NON_PERSON -> br.setIsConversation(false) + TYPE_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(null) + } + + TYPE_IMPORTANT_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + br.setChannel(channel) + } + + else -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + } + } + + return br.build() + } + + @Before + fun setUp() { + val personExtractor = object : NotificationPersonExtractor { + public override fun isPersonNotification(sbn: StatusBarNotification): Boolean { + return true + } + } + + underTest = PeopleNotificationIdentifierImpl( + personExtractor, + GroupMembershipManagerImpl() + ) + } + + private val Ranking.personTypeInfo + get() = when { + !isConversation -> TYPE_NON_PERSON + conversationShortcutInfo == null -> TYPE_PERSON + channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON + else -> TYPE_FULL_PERSON + } + + @Test + fun getPeopleNotificationType_entryIsImportant() { + summary1.setRanking(personRanking(summary1, TYPE_IMPORTANT_PERSON)) + + assertThat(underTest.getPeopleNotificationType(summary1)).isEqualTo(TYPE_IMPORTANT_PERSON) + } + + @Test + fun getPeopleNotificationType_importantChild() { + entries.get(0).getChildren().get(0).setRanking( + personRanking(entries.get(0).getChildren().get(0), TYPE_IMPORTANT_PERSON) + ) + + assertThat(entries.get(0).summary?.let { underTest.getPeopleNotificationType(it) }) + .isEqualTo(TYPE_IMPORTANT_PERSON) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt new file mode 100644 index 000000000000..aa6e76d08c17 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt @@ -0,0 +1,156 @@ +/* + * 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.promoted.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, +) +class PromotedNotificationsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Fixture { promotedNotificationsInteractor } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } + + @Test + fun orderedChipNotificationKeys_containsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun orderedChipNotificationKeys_containsPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_skipsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a non-promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the ron is first because the call has no content + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_includesPromotedCalls() = + kosmos.runTest { + // GIVEN a promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the call is the top notification + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|call|0") + } + + @Test + fun topPromotedNotificationContent_nullWithNoPromotedNotifications() = + kosmos.runTest { + // GIVEN a a non-promoted call and no promoted ongoing entry + val callEntry = buildOngoingCallEntry(promoted = false) + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList(listOf(callEntry, otherEntry)) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN there is no top promoted notification + assertThat(topPromotedNotificationContent).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 7d406b4b397c..9f35d631bd45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -67,6 +67,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; @@ -248,14 +249,13 @@ public class NotificationContentInflaterTest extends SysuiTestCase { true /* isNewView */, (v, p, r) -> true, new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { countDownLatch.countDown(); throw new RuntimeException("No Exception expected"); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { countDownLatch.countDown(); } }, mRow.getPrivateLayout(), null, null, new HashMap<>(), @@ -539,8 +539,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { inflater.setInflateSynchronously(true); InflationCallback callback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { if (!expectingException) { exceptionHolder.setException(e); } @@ -548,7 +547,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { if (expectingException) { exceptionHolder.setException(new RuntimeException( "Inflation finished even though there should be an error")); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 82eca3735a71..ce3aee1d88d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTIO import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi @@ -223,12 +224,12 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { remoteViewClickHandler = { _, _, _ -> true }, callback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { countDownLatch.countDown() throw RuntimeException("No Exception expected") } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { countDownLatch.countDown() } }, @@ -675,14 +676,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { inflater.setInflateSynchronously(true) val callback: InflationCallback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { if (!expectingException) { exceptionHolder.exception = e } countDownLatch.countDown() } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { if (expectingException) { exceptionHolder.exception = RuntimeException( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index c39b252cd795..f2131da8f0bb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -615,7 +615,7 @@ public class NotificationTestHelper { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, - mRowInflaterTaskLogger)); + mRowInflaterTaskLogger, UserHandle.of(entry.getSbn().getNormalizedUserId()))); mRow = (ExpandableNotificationRow) inflater.inflate( R.layout.status_bar_notification_row, null /* root */, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index d570f18e35d8..6381b4e0fef7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -57,11 +57,12 @@ class NotificationShelfViewModelTest : SysuiTestCase() { statusBarStateController = mock() whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) } - private val underTest = kosmos.notificationShelfViewModel private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val keyguardRepository = kosmos.fakeKeyguardRepository - private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController private val powerRepository = kosmos.fakePowerRepository + private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController } + + private val underTest by lazy { kosmos.notificationShelfViewModel } @Test fun canModifyColorOfNotifications_whenKeyguardNotShowing() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 256da253588c..9c5d65ec12ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.statusbar.notification.stack +import android.os.UserHandle import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper @@ -21,6 +22,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState @@ -978,7 +980,10 @@ open class NotificationShelfTest : SysuiTestCase() { ) { val sbnMock: StatusBarNotification = mock() val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) } - val row = ExpandableNotificationRow(mContext, null, mockEntry) + val row = when (NotificationBundleUi.isEnabled) { + true -> ExpandableNotificationRow(mContext, null, UserHandle.CURRENT) + false -> ExpandableNotificationRow(mContext, null, mockEntry) + } whenever(ambientState.lastVisibleBackgroundChild).thenReturn(row) whenever(ambientState.isExpansionChanging).thenReturn(true) whenever(ambientState.expansionFraction).thenReturn(expansionFraction) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index 8ec17dadcfe7..345ddae42798 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -46,9 +46,11 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -133,9 +135,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -144,7 +148,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -169,7 +177,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -193,7 +202,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -207,9 +217,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -218,7 +230,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -244,6 +260,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -272,7 +289,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -299,7 +317,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -326,7 +345,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -353,6 +373,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java new file mode 100644 index 000000000000..ecd04a47b8ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.theme; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + + +public class HardwareColorRule implements TestRule { + public String color = ""; + public String[] options = {}; + public boolean isTesting = false; + + @Override + public Statement apply(Statement base, Description description) { + HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class); + if (hardwareColors != null) { + color = hardwareColors.color(); + options = hardwareColors.options(); + isTesting = true; + } + return base; + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java new file mode 100644 index 000000000000..0b8df2e2670e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.theme; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface HardwareColors { + String color(); + String[] options(); +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 5cd0846ded7e..9a0b8125fb25 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -64,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.monet.DynamicColors; @@ -77,6 +78,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -98,6 +100,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100); private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101); + @Rule + public HardwareColorRule rule = new HardwareColorRule(); + @Mock private JavaAdapter mJavaAdapter; @Mock @@ -148,13 +153,17 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<ContentObserver> mSettingsObserver; + @Mock + private SystemPropertiesHelper mSystemProperties; + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); when(mUiModeManager.getContrast()).thenReturn(0.5f); - when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); + when(mResources.getColor(eq(android.R.color.system_accent1_500), any())) .thenReturn(Color.RED); when(mResources.getColor(eq(android.R.color.system_accent2_500), any())) @@ -166,11 +175,20 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { when(mResources.getColor(eq(android.R.color.system_neutral2_500), any())) .thenReturn(Color.BLACK); + when(mResources.getStringArray(com.android.internal.R.array.theming_defaults)) + .thenReturn(rule.options); + + // should fallback to `*|TONAL_SPOT|home_wallpaper` + when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color); + // will try set hardware colors as boot ONLY if user is not set yet + when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting); + mThemeOverlayController = new ThemeOverlayController(mContext, mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -214,11 +232,58 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { public void start_checksWallpaper() { ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); verify(mBgExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + } + + @Test + @HardwareColors(color = "BLK", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_checkHardwareColor() { + // getWallpaperColors should not be called + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager, never()).getWallpaperColors(anyInt()); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC); + assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get( + 0).toArgb()).isEqualTo(Color.RED); + } + + @Test + @HardwareColors(color = "", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_wildcardColor() { + // getWallpaperColors will be called because we srt wildcard to `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT); + } + + @Test + @HardwareColors(color = "NONEXISTENT", options = {}) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_fallbackColor() { + // getWallpaperColors will be called because we default color source is `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); registrationRunnable.getValue().run(); verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT); } + @Test public void onWallpaperColorsChanged_setsTheme_whenForeground() { // Should ask for a new theme when wallpaper colors change @@ -287,9 +352,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"preset\"}"; + String jsonString = createJsonString(TestColorSource.preset, "override.package.name", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -313,11 +378,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -348,11 +409,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -381,11 +438,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -404,11 +457,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -455,8 +504,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Test public void onSettingChanged_invalidStyle() { when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true); - String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00", + "some_invalid_name"); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -473,11 +522,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -506,11 +551,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -537,11 +578,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -570,11 +607,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -599,7 +632,6 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } - @Test @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI) public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() { @@ -608,11 +640,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -642,11 +670,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -676,11 +700,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -711,11 +731,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -745,11 +761,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -886,7 +898,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -926,7 +939,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -992,7 +1006,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1018,7 +1032,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1034,8 +1048,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"00FF00\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -1115,4 +1130,25 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { + DynamicColors.getCustomColorsMapped(false).size() * 2) ).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null)); } + + private enum TestColorSource { + preset, + home_wallpaper, + lock_wallpaper + } + + private String createJsonString(TestColorSource colorSource, String seedColorHex, + String style) { + return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\"," + + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.color_index\":\"2\"," + + "\"android.theme.customization.theme_style\":\"" + style + "\"}"; + } + + private String createJsonString(TestColorSource colorSource) { + return createJsonString(colorSource, "A16B00", "TONAL_SPOT"); + } + + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt index 7c166de81502..cc6a7b93eef3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt @@ -42,7 +42,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -53,7 +52,6 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class WallpaperRepositoryImplTest : SysuiTestCase() { - private var isWallpaperSupported = true private val kosmos = testKosmos().apply { @@ -293,12 +291,9 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { Intent(Intent.ACTION_WALLPAPER_CHANGED), ) assertThat(latest).isTrue() - assertThat(underTest.sendLockscreenLayoutJob).isNotNull() - assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true) } @Test - @Ignore("ag/31591766") @EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS) fun shouldSendNotificationLayout_setNotExtendedEffectsWallpaper_cancelSendLayoutJob() = testScope.runTest { @@ -315,8 +310,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { Intent(Intent.ACTION_WALLPAPER_CHANGED), ) assertThat(latest).isTrue() - assertThat(underTest.sendLockscreenLayoutJob).isNotNull() - assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true) whenever(kosmos.wallpaperManager.getWallpaperInfoForUser(any())) .thenReturn(UNSUPPORTED_WP) @@ -327,7 +320,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { runCurrent() assertThat(latest).isFalse() - assertThat(underTest.sendLockscreenLayoutJob?.isCancelled).isEqualTo(true) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt index 31afc298951b..31a611cc984b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt @@ -30,11 +30,8 @@ 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.data.repository.shadeRepository -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.testKosmos -import com.android.systemui.wallpapers.data.repository.fakeWallpaperFocalAreaRepository import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.wallpaperRepository import com.android.systemui.wallpapers.ui.viewmodel.wallpaperFocalAreaViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -75,36 +72,22 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { .thenReturn(2f) underTest = WallpaperFocalAreaInteractor( - applicationScope = testScope.backgroundScope, context = kosmos.mockedContext, - wallpaperFocalAreaRepository = kosmos.fakeWallpaperFocalAreaRepository, + wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository, shadeRepository = kosmos.shadeRepository, - activeNotificationsInteractor = kosmos.activeNotificationsInteractor, - wallpaperRepository = kosmos.wallpaperRepository, ) } - private fun overrideMockedResources(overrideResources: OverrideResources) { - val displayMetrics = - DisplayMetrics().apply { - widthPixels = overrideResources.screenWidth - heightPixels = overrideResources.screenHeight - density = 2f - } - whenever(mockedResources.displayMetrics).thenReturn(displayMetrics) - whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape)) - .thenReturn(overrideResources.centerAlignFocalArea) - } - @Test fun focalAreaBounds_withoutNotifications_inHandheldDevices() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) @@ -120,11 +103,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_withNotifications_inHandheldDevices() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) @@ -139,11 +123,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_inUnfoldLandscape() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 2000, screenHeight = 1600, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(true) @@ -158,11 +143,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_withNotifications_inUnfoldPortrait() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) @@ -177,11 +163,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_withoutNotifications_inUnfoldPortrait() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) @@ -196,11 +183,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_inTabletLandscape() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 3000, screenHeight = 2000, centerAlignFocalArea = true, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(true) @@ -216,11 +204,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F)) overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( RectF(250f, 700F, 750F, 1400F) @@ -240,11 +229,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F)) overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) kosmos.wallpaperFocalAreaViewModel = mock() kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( @@ -262,4 +252,21 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { val screenHeight: Int, val centerAlignFocalArea: Boolean, ) + + companion object { + fun overrideMockedResources( + mockedResources: Resources, + overrideResources: OverrideResources, + ) { + val displayMetrics = + DisplayMetrics().apply { + widthPixels = overrideResources.screenWidth + heightPixels = overrideResources.screenHeight + density = 2f + } + whenever(mockedResources.displayMetrics).thenReturn(displayMetrics) + whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape)) + .thenReturn(overrideResources.centerAlignFocalArea) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt new file mode 100644 index 000000000000..3cd20721a15b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wallpapers.ui.viewmodel + +import android.content.mockedContext +import android.content.res.Resources +import android.graphics.RectF +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs +import com.android.systemui.testKosmos +import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.Companion.overrideMockedResources +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.OverrideResources +import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class WallpaperFocalAreaViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var mockedResources: Resources + lateinit var underTest: WallpaperFocalAreaViewModel + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mockedResources = mock<Resources>() + overrideMockedResources( + mockedResources, + OverrideResources(screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false), + ) + whenever(kosmos.mockedContext.resources).thenReturn(mockedResources) + whenever( + mockedResources.getFloat( + Resources.getSystem() + .getIdentifier( + /* name= */ "config_wallpaperMaxScale", + /* defType= */ "dimen", + /* defPackage= */ "android", + ) + ) + ) + .thenReturn(2f) + kosmos.wallpaperFocalAreaInteractor = + WallpaperFocalAreaInteractor( + context = kosmos.mockedContext, + wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository, + shadeRepository = kosmos.shadeRepository, + ) + underTest = + WallpaperFocalAreaViewModel( + wallpaperFocalAreaInteractor = kosmos.wallpaperFocalAreaInteractor, + keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, + ) + } + + @Test + fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() = + testScope.runTest { + overrideMockedResources( + mockedResources, + OverrideResources( + screenWidth = 1600, + screenHeight = 2000, + centerAlignFocalArea = false, + ), + ) + val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN), + TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN), + ), + testScope, + ) + + setTestFocalAreaBounds() + + assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F)) + } + + @Test + fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() = + testScope.runTest { + overrideMockedResources( + mockedResources, + OverrideResources( + screenWidth = 1600, + screenHeight = 2000, + centerAlignFocalArea = false, + ), + ) + val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), + testScope, + ) + setTestFocalAreaBounds() + + assertThat(bounds).isEqualTo(null) + } + + private fun setTestFocalAreaBounds() { + kosmos.shadeRepository.setShadeLayoutWide(false) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F) + kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F) + kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F) + } +} diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml index 61d6a9046144..a27e29f1beb6 100644 --- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml @@ -19,12 +19,13 @@ android:height="40dp" android:viewportHeight="40" android:viewportWidth="40"> + <group> + <clip-path android:pathData="M8,12h24.5v15.5h-24.5z" /> <path - android:fillColor="#F7DAEE" - android:fillType="evenOdd" - android:pathData="M20.76,19C21.65,19 22.096,17.924 21.467,17.294L19.284,15.105C18.895,14.716 18.895,14.085 19.285,13.695C19.674,13.306 20.306,13.306 20.695,13.695L26.293,19.293C26.683,19.683 26.683,20.317 26.293,20.707L20.705,26.295C20.315,26.685 19.683,26.686 19.292,26.298C18.9,25.907 18.898,25.272 19.29,24.88L21.463,22.707C22.093,22.077 21.647,21 20.756,21H10C9.448,21 9,20.552 9,20C9,19.448 9.448,19 10,19H20.76ZM32,26C32,26.552 31.552,27 31,27C30.448,27 30,26.552 30,26V14C30,13.448 30.448,13 31,13C31.552,13 32,13.448 32,14V26Z" - android:strokeColor="#F7DAEE" - android:strokeLineCap="round" - android:strokeLineJoin="round" - android:strokeWidth="2" /> + android:fillColor="#000000" + android:pathData="M30.75,12C29.79,12 29,12.79 29,13.75V25.75C29,26.71 29.79,27.5 30.75,27.5C31.71,27.5 32.5,26.71 32.5,25.75V13.75C32.5,12.79 31.71,12 30.75,12Z" /> + <path + android:fillColor="#000000" + android:pathData="M20.98,12.92C20.3,12.24 19.19,12.24 18.51,12.92C17.83,13.6 17.83,14.71 18.51,15.39L21.12,18H9.75C8.79,18 8,18.79 8,19.75C8,20.71 8.79,21.5 9.75,21.5H21.11L18.51,24.1C18.18,24.43 18,24.87 18,25.34C18,25.81 18.18,26.25 18.52,26.58C18.86,26.92 19.31,27.09 19.75,27.09C20.19,27.09 20.65,26.92 20.99,26.58L26.61,20.96C27.28,20.29 27.28,19.21 26.61,18.55L20.98,12.92Z" /> + </group> </vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml deleted file mode 100644 index 044656d6fc7d..000000000000 --- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml +++ /dev/null @@ -1,25 +0,0 @@ -<!-- - ~ Copyright (C) 2025 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="40dp" - android:height="40dp" - android:viewportHeight="40" - android:viewportWidth="40"> - <path - android:fillColor="#ECDFE5" - android:pathData="M18.792,26.5L23.333,21.958L27.875,26.5L29.875,24.542L25.292,20L29.792,15.458L27.833,13.5L23.333,18.042L18.792,13.5L16.792,15.458L21.375,20L16.792,24.542L18.792,26.5ZM14.708,33.333C14.292,33.333 13.875,33.236 13.458,33.042C13.069,32.847 12.75,32.569 12.5,32.208L3.333,20L12.458,7.792C12.708,7.431 13.028,7.153 13.417,6.958C13.833,6.764 14.264,6.667 14.708,6.667H33.917C34.694,6.667 35.347,6.944 35.875,7.5C36.431,8.028 36.708,8.681 36.708,9.458V30.542C36.708,31.319 36.431,31.986 35.875,32.542C35.347,33.069 34.694,33.333 33.917,33.333H14.708Z" /> -</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml new file mode 100644 index 000000000000..86f95bc97169 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <path + android:fillColor="#000000" + android:pathData="M22.167,21.9L25.531,25.265C25.795,25.502 26.112,25.621 26.481,25.621C26.851,25.621 27.167,25.502 27.431,25.265C27.669,25.001 27.788,24.684 27.788,24.315C27.788,23.919 27.669,23.589 27.431,23.325L24.067,20L27.392,16.675C27.656,16.411 27.788,16.094 27.788,15.725C27.788,15.356 27.656,15.039 27.392,14.775C27.128,14.511 26.798,14.379 26.402,14.379C26.033,14.379 25.729,14.511 25.492,14.775L22.167,18.1L18.802,14.735C18.538,14.498 18.222,14.379 17.852,14.379C17.483,14.379 17.166,14.498 16.902,14.735C16.665,14.999 16.546,15.329 16.546,15.725C16.546,16.094 16.665,16.411 16.902,16.675L20.267,20L16.902,23.325C16.665,23.589 16.546,23.906 16.546,24.275C16.546,24.644 16.665,24.961 16.902,25.225C17.166,25.489 17.483,25.621 17.852,25.621C18.248,25.621 18.578,25.489 18.842,25.225L22.167,21.9ZM14.012,32.667C13.59,32.667 13.181,32.574 12.785,32.39C12.416,32.179 12.099,31.915 11.835,31.598L4.394,21.623C4.024,21.148 3.84,20.607 3.84,20C3.84,19.393 4.024,18.852 4.394,18.377L11.835,8.402C12.073,8.085 12.39,7.835 12.785,7.65C13.181,7.439 13.59,7.333 14.012,7.333H32.142C32.907,7.333 33.554,7.597 34.081,8.125C34.609,8.653 34.873,9.286 34.873,10.025V29.975C34.873,30.714 34.609,31.347 34.081,31.875C33.554,32.403 32.907,32.667 32.142,32.667H14.012Z" /> +</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml new file mode 100644 index 000000000000..7f551f4d3c60 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml @@ -0,0 +1,31 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <group> + <clip-path android:pathData="M5,7h29.89v25h-29.89z" /> + <path + android:fillColor="#000000" + android:pathData="M30.96,32H15.59C14.21,32 12.89,31.34 12.06,30.24L5.78,21.86C4.74,20.47 4.74,18.54 5.78,17.15L12.06,8.77C12.89,7.67 14.21,7 15.59,7H30.96C33.13,7 34.89,8.76 34.89,10.93V28.08C34.89,30.25 33.13,32.01 30.96,32.01V32ZM14.46,28.44C14.73,28.79 15.15,29 15.59,29H30.96C31.47,29 31.89,28.58 31.89,28.07V10.93C31.89,10.42 31.47,10 30.96,10H15.59C15.15,10 14.73,10.21 14.46,10.56L8.18,18.94C7.93,19.27 7.93,19.72 8.18,20.05L14.46,28.43V28.44Z" /> + <path + android:fillColor="#000000" + android:pathData="M22.46,21.27L25.36,24.17C25.6,24.43 25.89,24.56 26.25,24.56C26.61,24.56 26.9,24.43 27.14,24.17C27.4,23.93 27.53,23.64 27.53,23.28C27.53,22.92 27.4,22.63 27.14,22.39L24.24,19.49L27.14,16.59C27.38,16.35 27.49,16.06 27.49,15.7C27.49,15.34 27.37,15.05 27.14,14.81C26.91,14.57 26.61,14.46 26.25,14.46C25.89,14.46 25.59,14.58 25.33,14.81L22.46,17.71L19.56,14.81C19.32,14.55 19.03,14.42 18.67,14.42C18.31,14.42 18.02,14.55 17.78,14.81C17.52,15.05 17.39,15.34 17.39,15.7C17.39,16.06 17.52,16.35 17.78,16.59L20.68,19.49L17.78,22.39C17.52,22.63 17.39,22.92 17.39,23.28C17.39,23.64 17.52,23.93 17.78,24.17C18.02,24.41 18.31,24.52 18.67,24.52C19.03,24.52 19.32,24.4 19.56,24.17L22.46,21.27Z" /> + </group> +</vector> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 0b35559148af..87d06bfde743 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyguard_lock_padding" - android:importantForAccessibility="no" + android:accessibilityLiveRegion="polite" android:ellipsize="marquee" android:focusable="false" android:gravity="center" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index f231df2f1a10..c7f320c69113 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -67,6 +67,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 04457229d573..9359838238af 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -32,6 +32,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml index b184344f2f24..6cbe96a8cb50 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml @@ -68,6 +68,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 0e15ff66f3ee..cf388875a174 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -36,6 +36,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml index f6ac02aee657..33eab179c3f7 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml @@ -75,6 +75,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index ba4da794d777..fd5eeb8b9408 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -33,6 +33,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml index 8fd10fb3ddb8..8c34cd4165e0 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml @@ -29,7 +29,6 @@ android:layout_height="wrap_content" android:singleLine="true" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" android:paddingEnd="4dp" /> <TextView diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml index 35f2ef901bdd..a338e4c70cfa 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml @@ -54,7 +54,6 @@ android:singleLine="true" android:paddingEnd="4dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" /> <TextView diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 09aa2241e42b..8d10e393b5ca 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -119,7 +119,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes + internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects </string> <!-- The tiles to display in QuickSettings --> @@ -175,6 +175,9 @@ <!-- Minimum display time for a heads up notification if throttling is enabled, in milliseconds. --> <integer name="heads_up_notification_minimum_time_with_throttling">500</integer> + <!-- Minimum display time for a heads up notification that was shown from a user action (like tapping on a different part of the UI), in milliseconds. --> + <integer name="heads_up_notification_minimum_time_for_user_initiated">3000</integer> + <!-- Display time for a sticky heads up notification, in milliseconds. --> <integer name="sticky_heads_up_notification_time">60000</integer> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e077b41a6f59..c0eea15b043b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2341,7 +2341,9 @@ <!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] --> <string name="system_multitasking_lhs">Use split screen with app on the left</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> - <string name="system_multitasking_full_screen">Switch to full screen</string> + <string name="system_multitasking_full_screen">Use full screen</string> + <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] --> + <string name="system_multitasking_desktop_view">Use desktop view</string> <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml index d885e00fbe82..faf06f3d39f0 100644 --- a/packages/SystemUI/res/values/tiles_states_strings.xml +++ b/packages/SystemUI/res/values/tiles_states_strings.xml @@ -358,4 +358,14 @@ <item>Off</item> <item>On</item> </string-array> + + <!-- State names for desktop effects tile: unavailable, off, on. + This subtitle is shown when the tile is in that particular state but does not set its own + subtitle, so some of these may never appear on screen. They should still be translated as + if they could appear. [CHAR LIMIT=32] --> + <string-array name="tile_states_desktopeffects"> + <item>Unavailable</item> + <item>Off</item> + <item>On</item> + </string-array> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index f528ec8af134..860a496ef18b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -20,22 +20,16 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.hardware.biometrics.BiometricSourceType; import android.os.SystemClock; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; import android.util.Log; import android.util.Pair; import android.view.View; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.util.ViewController; -import java.lang.ref.WeakReference; - import javax.inject.Inject; /** @@ -54,39 +48,8 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> private Pair<BiometricSourceType, Long> mMessageBiometricSource = null; private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L; - /** - * Delay before speaking an accessibility announcement. Used to prevent - * lift-to-type from interrupting itself. - */ - private static final long ANNOUNCEMENT_DELAY = 250; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; - private final AnnounceRunnable mAnnounceRunnable; - private final TextWatcher mTextWatcher = new TextWatcher() { - @Override - public void afterTextChanged(Editable editable) { - CharSequence msg = editable; - if (!TextUtils.isEmpty(msg)) { - mView.removeCallbacks(mAnnounceRunnable); - mAnnounceRunnable.setTextToAnnounce(msg); - mView.postDelayed(() -> { - if (msg == mView.getText()) { - mAnnounceRunnable.run(); - } - }, ANNOUNCEMENT_DELAY); - } - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - }; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { public void onFinishedGoingToSleep(int why) { @@ -122,7 +85,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor = keyguardUpdateMonitor; mConfigurationController = configurationController; - mAnnounceRunnable = new AnnounceRunnable(mView); } @Override @@ -131,14 +93,12 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor.registerCallback(mInfoCallback); mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive()); mView.onThemeChanged(); - mView.addTextChangedListener(mTextWatcher); } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); mKeyguardUpdateMonitor.removeCallback(mInfoCallback); - mView.removeTextChangedListener(mTextWatcher); } /** @@ -232,30 +192,4 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> view, mKeyguardUpdateMonitor, mConfigurationController); } } - - /** - * Runnable used to delay accessibility announcements. - */ - @VisibleForTesting - public static class AnnounceRunnable implements Runnable { - private final WeakReference<View> mHost; - private CharSequence mTextToAnnounce; - - AnnounceRunnable(View host) { - mHost = new WeakReference<>(host); - } - - /** Sets the text to announce. */ - public void setTextToAnnounce(CharSequence textToAnnounce) { - mTextToAnnounce = textToAnnounce; - } - - @Override - public void run() { - final View host = mHost.get(); - if (host != null && host.isVisibleToUser()) { - host.announceForAccessibility(mTextToAnnounce); - } - } - } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index 245283da75ab..04d4c2a3cdf9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -184,7 +184,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView } mDeleteButton = findViewById(R.id.delete_button); if (Flags.bouncerUiRevamp2()) { - mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete); + mDeleteButton.setDrawableForTransparentMode(R.drawable.pin_bouncer_delete_filled); + mDeleteButton.setDefaultDrawable(R.drawable.pin_bouncer_delete_outline); + mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete_outline); } mDeleteButton.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 0ff93236a856..584ebb50520a 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -25,6 +25,7 @@ import android.util.AttributeSet; import android.view.MotionEvent; import android.view.accessibility.AccessibilityNodeInfo; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.systemui.Flags; @@ -42,6 +43,12 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni private int mStyleAttr; private boolean mIsTransparentMode; + @DrawableRes + private int mDrawableForTransparentMode = 0; + + @DrawableRes + private int mDefaultDrawable = 0; + public NumPadButton(Context context, AttributeSet attrs) { super(context, attrs); mStyleAttr = attrs.getStyleAttribute(); @@ -123,8 +130,14 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni mIsTransparentMode = isTransparentMode; if (isTransparentMode) { + if (mDrawableForTransparentMode != 0) { + setImageResource(mDrawableForTransparentMode); + } setBackgroundColor(getResources().getColor(android.R.color.transparent)); } else { + if (mDefaultDrawable != 0) { + setImageResource(mDefaultDrawable); + } Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background); if (Flags.bouncerUiRevamp2() && bgDrawable != null) { bgDrawable.setTint(Color.actionBg); @@ -154,4 +167,19 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni super.onInitializeAccessibilityNodeInfo(info); info.setTextEntryKey(true); } + + /** + * Drawable to use when transparent mode is enabled + */ + public void setDrawableForTransparentMode(@DrawableRes int drawableResId) { + mDrawableForTransparentMode = drawableResId; + } + + /** + * Drawable to use when transparent mode is not enabled. + */ + public void setDefaultDrawable(@DrawableRes int drawableResId) { + mDefaultDrawable = drawableResId; + setImageResource(mDefaultDrawable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt index 434a9ce58c3b..7d8945a5b4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt @@ -191,7 +191,6 @@ object KeyguardBouncerViewBinder { .filter { it == EXPANSION_VISIBLE } .collect { securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON) - view.announceForAccessibility(securityContainerController.title) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index 0a9bd4214a12..bf4445ba18db 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -151,5 +151,7 @@ constructor( override fun instantlyShowOverlay(overlay: OverlayKey) = Unit override fun instantlyHideOverlay(overlay: OverlayKey) = Unit + + override fun freezeAndAnimateToCurrentState() = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt index 464201f6ec12..b787fc2a2b17 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyboard.shortcut.data.source import android.content.Context import android.content.res.Resources import android.view.KeyEvent.KEYCODE_D +import android.view.KeyEvent.KEYCODE_DPAD_DOWN import android.view.KeyEvent.KEYCODE_DPAD_LEFT import android.view.KeyEvent.KEYCODE_DPAD_RIGHT import android.view.KeyEvent.KEYCODE_DPAD_UP @@ -73,6 +74,15 @@ constructor(@Main private val resources: Resources, @Application private val con command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_UP) } ) + if (DesktopModeStatus.canEnterDesktopMode(context)) { + // Switch to desktop view + // - Meta + Ctrl + Down arrow + add( + shortcutInfo(resources.getString(R.string.system_multitasking_desktop_view)) { + command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_DOWN) + } + ) + } if (enableMoveToNextDisplayShortcut()) { // Move a window to the next display: // - Meta + Ctrl + D diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt index 80bdc65f9b97..f69229213690 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt @@ -25,11 +25,17 @@ import kotlinx.coroutines.flow.MutableStateFlow class LockscreenSceneTransitionRepository @Inject constructor() { /** - * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the - * next transition into the Lockscreen scene is started. It will be consumed exactly once and - * after that the state will be set back to [DEFAULT_STATE]. + * This [KeyguardState] will indicate which sub-state within KTF should be navigated to next. + * + * This can be either starting a transition to the `Lockscreen` scene or cancelling a transition + * from the `Lockscreen` scene and returning back to it. + * + * A `null` value means that no explicit target state was set and therefore the [DEFAULT_STATE] + * should be used. + * + * Once consumed, this state should be reset to `null`. */ - val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE) + val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null) companion object { val DEFAULT_STATE = KeyguardState.LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 0700ec639153..6f5f662d6fa3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -159,7 +159,6 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value - val canStartDreaming = dreamManager.canStartDreaming(false) if (!deviceEntryInteractor.isLockscreenEnabled()) { if (!SceneContainerFlag.isEnabled) { @@ -192,13 +191,6 @@ constructor( if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } - } else if (canStartDreaming) { - // If we're waking up to dream, transition directly to dreaming without - // showing the lockscreen. - startTransitionTo( - KeyguardState.DREAMING, - ownerReason = "moving from doze to dream", - ) } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index 5f821022d580..1b70ff84f09d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -119,7 +119,8 @@ constructor( } else { val targetState = if (idle.currentScene == Scenes.Lockscreen) { - transitionInteractor.startedKeyguardTransitionStep.value.from + repository.nextLockscreenTargetState.value + ?: transitionInteractor.startedKeyguardTransitionStep.value.from } else { UNDEFINED } @@ -197,11 +198,11 @@ constructor( TransitionInfo( ownerName = this::class.java.simpleName, from = UNDEFINED, - to = repository.nextLockscreenTargetState.value, + to = repository.nextLockscreenTargetState.value ?: DEFAULT_STATE, animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) - repository.nextLockscreenTargetState.value = DEFAULT_STATE + repository.nextLockscreenTargetState.value = null startTransition(newTransition) } @@ -215,7 +216,7 @@ constructor( animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) - repository.nextLockscreenTargetState.value = DEFAULT_STATE + repository.nextLockscreenTargetState.value = null startTransition(newTransition) } 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 8e385385b8c4..da87e38daa9b 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 @@ -370,6 +370,14 @@ object KeyguardRootViewBinder { repeatOnLifecycle(Lifecycle.State.STARTED) { if (wallpaperFocalAreaViewModel.hasFocalArea.value) { launch { + wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect { + wallpaperFocalAreaBounds -> + wallpaperFocalAreaViewModel.setFocalAreaBounds( + wallpaperFocalAreaBounds + ) + } + } + launch { wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds .filterNotNull() .collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt index 9018c58a7e36..e6a85c6860c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt @@ -39,6 +39,4 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) { ) val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) - // Notifications should not be shown while transitioning to dream. - val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt index c3182bf7a320..1466d8b4288e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt @@ -143,6 +143,12 @@ interface MediaDataManager { * place immediately. */ override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {} + + /** + * Called whenever the current active media notification changes. Should only be used if + * [SceneContainerFlag] is disabled + */ + override fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 1464849156dc..59f98d83e149 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -1434,6 +1434,9 @@ class MediaDataProcessor( * place immediately. */ fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + + /** Called whenever the current active media notification changes */ + fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {} } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 173b600de06b..93c4bafe4273 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -87,6 +87,7 @@ import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTE import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE +import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -155,6 +156,7 @@ constructor( private val mediaCarouselViewModel: MediaCarouselViewModel, private val mediaViewControllerFactory: Provider<MediaViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, + private val mediaControlChipInteractor: MediaControlChipInteractor, ) : Dumpable { /** The current width of the carousel */ var currentCarouselWidth: Int = 0 @@ -957,6 +959,9 @@ constructor( } } mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) MediaPlayerData.updateVisibleMediaPlayers() // Automatically scroll to the active player if needed if (shouldScrollToKey) { @@ -1015,6 +1020,9 @@ constructor( ) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1023,6 +1031,9 @@ constructor( updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1036,6 +1047,9 @@ constructor( } updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1194,6 +1208,9 @@ constructor( mediaContent.removeView(removed.recommendationViewHolder?.recommendations) removed.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) updatePageIndicator() if (dismissMediaData) { @@ -1928,6 +1945,16 @@ internal object MediaPlayerData { fun visiblePlayerKeys() = visibleMediaPlayers.values + /** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */ + fun getFirstActiveMediaData(): MediaData? { + mediaPlayers.entries.forEach { entry -> + if (!entry.key.isSsMediaRec && entry.key.data.active) { + return entry.key.data + } + } + return null + } + /** Returns the index of the first non-timeout media. */ fun firstActiveMediaIndex(): Int { mediaPlayers.entries.forEachIndexed { index, e -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt index 709723fa9480..6022b7b1fc13 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt @@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.util import androidx.recyclerview.widget.ListUpdateCallback import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import kotlin.math.min /** A [ListUpdateCallback] to apply media events needed to reach the new state. */ class MediaViewModelListUpdateCallback( @@ -46,7 +47,7 @@ class MediaViewModelListUpdateCallback( } override fun onChanged(position: Int, count: Int, payload: Any?) { - for (i in position until position + count) { + for (i in position until min(position + count, new.size)) { onUpdated(new[i], position) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index f5e62323e769..c58ba377fb68 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -20,23 +20,16 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; -import android.annotation.DrawableRes; -import android.annotation.StringRes; import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.Log; import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import androidx.core.widget.CompoundButtonCompat; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.annotations.VisibleForTesting; @@ -49,23 +42,64 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** - * Adapter for media output dialog. + * A parent RecyclerView adapter for the media output dialog device list. This class doesn't + * manipulate the layout directly. */ -public class MediaOutputAdapter extends MediaOutputBaseAdapter { +public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + record OngoingSessionStatus(boolean host) {} - private static final String TAG = "MediaOutputAdapter"; + record GroupStatus(Boolean selected, Boolean deselectable) {} + + enum ConnectionState { + CONNECTED, + CONNECTING, + DISCONNECTED, + } + + protected final MediaSwitchingController mController; + private int mCurrentActivePosition; + private boolean mIsDragging; + private static final String TAG = "MediaOutputAdapterBase"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final float DEVICE_DISABLED_ALPHA = 0.5f; - private static final float DEVICE_ACTIVE_ALPHA = 1f; - protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); + protected final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping(); - public MediaOutputAdapter(MediaSwitchingController controller) { - super(controller); + public MediaOutputAdapterBase(MediaSwitchingController controller) { + mController = controller; + mCurrentActivePosition = -1; + mIsDragging = false; setHasStableIds(true); } - @Override + boolean isCurrentlyConnected(MediaDevice device) { + return TextUtils.equals(device.getId(), + mController.getCurrentConnectedMediaDevice().getId()) + || (mController.getSelectedMediaDevice().size() == 1 + && isDeviceIncluded(mController.getSelectedMediaDevice(), device)); + } + + boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { + for (MediaDevice device : deviceList) { + if (TextUtils.equals(device.getId(), targetDevice.getId())) { + return true; + } + } + return false; + } + + boolean isDragging() { + return mIsDragging; + } + + void setIsDragging(boolean isDragging) { + mIsDragging = isDragging; + } + + int getCurrentActivePosition() { + return mCurrentActivePosition; + } + + /** Refreshes the RecyclerView dataset and forces re-render. */ public void updateItems() { mMediaItemList.clear(); mMediaItemList.addAll(mController.getMediaItemList()); @@ -79,47 +113,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, - int viewType) { - super.onCreateViewHolder(viewGroup, viewType); - switch (viewType) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - return new MediaGroupDividerViewHolder(mHolderView); - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - case MediaItem.MediaItemType.TYPE_DEVICE: - default: - return new MediaDeviceViewHolder(mHolderView); - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (position >= mMediaItemList.size()) { - if (DEBUG) { - Log.d(TAG, "Incorrect position: " + position + " list size: " - + mMediaItemList.size()); - } - return; - } - MediaItem currentMediaItem = mMediaItemList.get(position); - switch (currentMediaItem.getMediaItemType()) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle()); - break; - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBindPairNewDevice(); - break; - case MediaItem.MediaItemType.TYPE_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBind( - currentMediaItem, - position); - break; - default: - Log.d(TAG, "Incorrect position: " + position); - } - } - - @Override public long getItemId(int position) { if (position >= mMediaItemList.size()) { Log.d(TAG, "Incorrect position for item id: " + position); @@ -145,15 +138,17 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return mMediaItemList.size(); } - class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder { + abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { + + Context mContext; - MediaDeviceViewHolder(View view) { + MediaDeviceViewHolderBase(View view, Context context) { super(view); + mContext = context; } - void onBind(MediaItem mediaItem, int position) { + void renderItem(MediaItem mediaItem, int position) { MediaDevice device = mediaItem.getMediaDevice().get(); - super.onBind(device, position); boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice(); final boolean currentlyConnected = isCurrentlyConnected(device); boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device); @@ -198,7 +193,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { if (mCurrentActivePosition == position) { mCurrentActivePosition = -1; } - mItemLayout.setVisibility(View.VISIBLE); if (mController.isAnyDeviceTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING) { @@ -265,37 +259,15 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - private void renderDeviceItem(boolean hideGroupItem, MediaDevice device, + protected abstract void renderDeviceItem(boolean hideGroupItem, MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment, GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, - Drawable deviceStatusIcon) { - if (hideGroupItem) { - mItemLayout.setVisibility(View.GONE); - return; - } - updateTitle(device.getName()); - updateTitleIcon(device, connectionState, restrictVolumeAdjustment); - updateSeekBar(device, connectionState, restrictVolumeAdjustment, - getDeviceItemContentDescription(device)); - updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus); - updateLoadingIndicator(connectionState); - updateFullItemClickListener(clickListener); - updateContentAlpha(deviceDisabled); - updateSubtitle(subtitle); - updateDeviceStatusIcon(deviceStatusIcon); - updateItemBackground(connectionState); - } + Drawable deviceStatusIcon); - private void renderDeviceGroupItem() { - String sessionName = mController.getSessionName() == null ? "" - : mController.getSessionName().toString(); - updateTitle(sessionName); - updateUnmutedVolumeIcon(null /* device */); - updateGroupSeekBar(getGroupItemContentDescription(sessionName)); - updateEndAreaForDeviceGroup(); - updateItemBackground(ConnectionState.CONNECTED); - } + protected abstract void renderDeviceGroupItem(); + + protected abstract void disableSeekBar(); private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) { return device.hasOngoingSession() ? new OngoingSessionStatus( @@ -322,95 +294,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return !mController.getSelectableMediaDevice().isEmpty(); } - /** Renders the right side round pill button / checkbox. */ - private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState, - @Nullable GroupStatus groupStatus, - @Nullable OngoingSessionStatus ongoingSessionStatus) { - boolean showEndArea = false; - boolean isCheckbox = false; - // If both group status and the ongoing session status are present, only the ongoing - // session controls are displayed. The current layout design doesn't allow both group - // and ongoing session controls to be rendered simultaneously. - if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) { - showEndArea = true; - updateEndAreaForOngoingSession(device, ongoingSessionStatus.host()); - } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { - showEndArea = true; - isCheckbox = true; - updateEndAreaForGroupCheckBox(device, groupStatus); - } - updateEndAreaVisibility(showEndArea, isCheckbox); - } - - private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) { - if (Flags.enableOutputSwitcherDeviceGrouping()) { - return isGroupCheckboxEnabled(groupStatus); - } - return true; - } - - private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) { - boolean disabled = groupStatus.selected() && !groupStatus.deselectable(); - return !disabled; - } - - public void setCheckBoxColor(CheckBox checkBox, int color) { - int[][] states = {{android.R.attr.state_checked}, {}}; - int[] colors = {color, color}; - CompoundButtonCompat.setButtonTintList(checkBox, new - ColorStateList(states, colors)); - } - - private void updateContentAlpha(boolean deviceDisabled) { - float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA; - mTitleIcon.setAlpha(alphaValue); - mTitleText.setAlpha(alphaValue); - mSubTitleText.setAlpha(alphaValue); - mStatusIcon.setAlpha(alphaValue); - } - - private void updateEndAreaForDeviceGroup() { - updateEndAreaWithIcon( - v -> { - mShouldGroupSelectedMediaItems = false; - notifyDataSetChanged(); - }, - R.drawable.media_output_item_expand_group, - R.string.accessibility_expand_group); - updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */); - } - - private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) { - updateEndAreaWithIcon( - v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v), - isHost ? R.drawable.media_output_status_edit_session - : R.drawable.ic_sound_bars_anim, - R.string.accessibility_open_application); - } - - private void updateEndAreaWithIcon(View.OnClickListener clickListener, - @DrawableRes int iconDrawableId, - @StringRes int accessibilityStringId) { - updateEndAreaColor(mController.getColorSeekbarProgress()); - mEndClickIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - mEndClickIcon.setOnClickListener(clickListener); - mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick()); - Drawable drawable = mContext.getDrawable(iconDrawableId); - mEndClickIcon.setImageDrawable(drawable); - if (drawable instanceof AnimatedVectorDrawable) { - ((AnimatedVectorDrawable) drawable).start(); - } - if (Flags.enableOutputSwitcherDeviceGrouping()) { - mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId)); - } - } - - public void updateEndAreaColor(int color) { - mEndTouchArea.setBackgroundTintList( - ColorStateList.valueOf(color)); - } - @Nullable private View.OnClickListener getClickListenerBasedOnSelectionBehavior( @NonNull MediaDevice device) { @@ -427,57 +310,12 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) { - if (deviceStatusIcon == null) { - mStatusIcon.setVisibility(View.GONE); - } else { - mStatusIcon.setImageDrawable(deviceStatusIcon); - mStatusIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - if (deviceStatusIcon instanceof AnimatedVectorDrawable) { - ((AnimatedVectorDrawable) deviceStatusIcon).start(); - } - mStatusIcon.setVisibility(View.VISIBLE); - } - } - - public void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device, - @NonNull GroupStatus groupStatus) { - boolean isEnabled = isGroupCheckboxEnabled(groupStatus); - mEndTouchArea.setOnClickListener( - isEnabled ? (v) -> mCheckBox.performClick() : null); - mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress() - : mController.getColorItemBackground()); - mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device)); - mCheckBox.setOnCheckedChangeListener(null); - mCheckBox.setChecked(groupStatus.selected()); - mCheckBox.setOnCheckedChangeListener( - isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered( - !groupStatus.selected(), device) : null); - mCheckBox.setEnabled(isEnabled); - setCheckBoxColor(mCheckBox, mController.getColorItemContent()); - } - - private void updateFullItemClickListener(@Nullable View.OnClickListener listener) { - mContainerLayout.setOnClickListener(listener); - updateIconAreaClickListener(listener); - } - - /** Binds a ViewHolder for a "Connect a device" item. */ - void onBindPairNewDevice() { - mTitleText.setTextColor(mController.getColorItemContent()); - mCheckBox.setVisibility(View.GONE); - updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new)); - updateItemBackground(ConnectionState.DISCONNECTED); - final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); - mTitleIcon.setImageDrawable(addDrawable); - mTitleIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); + protected void onExpandGroupButtonClicked() { + mShouldGroupSelectedMediaItems = false; + notifyDataSetChanged(); } - private void onGroupActionTriggered(boolean isChecked, MediaDevice device) { + protected void onGroupActionTriggered(boolean isChecked, MediaDevice device) { disableSeekBar(); if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { mController.addDeviceToPlayMedia(device); @@ -523,32 +361,18 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { notifyDataSetChanged(); } - private String getDeviceItemContentDescription(@NonNull MediaDevice device) { + protected String getDeviceItemContentDescription(@NonNull MediaDevice device) { return mContext.getString( device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE ? R.string.accessibility_bluetooth_name : R.string.accessibility_cast_name, device.getName()); } - private String getGroupItemContentDescription(String sessionName) { + protected String getGroupItemContentDescription(String sessionName) { return mContext.getString(R.string.accessibility_cast_name, sessionName); } } - class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder { - final TextView mTitleText; - - MediaGroupDividerViewHolder(@NonNull View itemView) { - super(itemView); - mTitleText = itemView.requireViewById(R.id.title); - } - - void onBind(String groupDividerTitle) { - mTitleText.setTextColor(mController.getColorItemContent()); - mTitleText.setText(groupDividerTitle); - } - } - @RequiresApi(34) private static class Api34Impl { @DoNotInline diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java index f97b3d3d5e38..565b2e41f75a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java @@ -18,14 +18,18 @@ package com.android.systemui.media.dialog; import android.animation.Animator; import android.animation.ValueAnimator; -import android.app.WallpaperColors; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.ClipDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.LayerDrawable; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -40,6 +44,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.widget.CompoundButtonCompat; import androidx.recyclerview.widget.RecyclerView; import com.android.media.flags.Flags; @@ -48,82 +53,67 @@ import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.res.R; -import java.util.List; - /** - * Base adapter for media output dialog. + * A RecyclerView adapter for the legacy UI media output dialog device list. */ -public abstract class MediaOutputBaseAdapter extends - RecyclerView.Adapter<RecyclerView.ViewHolder> { - - record OngoingSessionStatus(boolean host) {} - - record GroupStatus(Boolean selected, Boolean deselectable) {} - - enum ConnectionState { - CONNECTED, - CONNECTING, - DISCONNECTED, - } - - protected final MediaSwitchingController mController; +public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { + private static final String TAG = "MediaOutputAdapterL"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int UNMUTE_DEFAULT_VOLUME = 2; - - Context mContext; + private static final float DEVICE_DISABLED_ALPHA = 0.5f; + private static final float DEVICE_ACTIVE_ALPHA = 1f; View mHolderView; - boolean mIsDragging; - int mCurrentActivePosition; private boolean mIsInitVolumeFirstTime; - public MediaOutputBaseAdapter(MediaSwitchingController controller) { - mController = controller; - mIsDragging = false; - mCurrentActivePosition = -1; + public MediaOutputAdapterLegacy(MediaSwitchingController controller) { + super(controller); mIsInitVolumeFirstTime = true; } - /** - * Refresh current dataset - */ - public abstract void updateItems(); - @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { - mContext = viewGroup.getContext(); - mHolderView = LayoutInflater.from(mContext).inflate(MediaItem.getMediaLayoutId(viewType), - viewGroup, false); - return null; - } - - void updateColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { - mController.setCurrentColorScheme(wallpaperColors, isDarkTheme); - } + Context context = viewGroup.getContext(); + mHolderView = LayoutInflater.from(viewGroup.getContext()).inflate( + MediaItem.getMediaLayoutId(viewType), + viewGroup, false); - boolean isCurrentlyConnected(MediaDevice device) { - return TextUtils.equals(device.getId(), - mController.getCurrentConnectedMediaDevice().getId()) - || (mController.getSelectedMediaDevice().size() == 1 - && isDeviceIncluded(mController.getSelectedMediaDevice(), device)); + switch (viewType) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + return new MediaGroupDividerViewHolderLegacy(mHolderView); + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: + case MediaItem.MediaItemType.TYPE_DEVICE: + default: + return new MediaDeviceViewHolderLegacy(mHolderView, context); + } } - boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { - for (MediaDevice device : deviceList) { - if (TextUtils.equals(device.getId(), targetDevice.getId())) { - return true; + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (position >= getItemCount()) { + if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position + " list size: " + + getItemCount()); } + return; + } + MediaItem currentMediaItem = mMediaItemList.get(position); + switch (currentMediaItem.getMediaItemType()) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + ((MediaGroupDividerViewHolderLegacy) viewHolder).onBind( + currentMediaItem.getTitle()); + break; + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: + ((MediaDeviceViewHolderLegacy) viewHolder).onBindPairNewDevice(); + break; + case MediaItem.MediaItemType.TYPE_DEVICE: + ((MediaDeviceViewHolderLegacy) viewHolder).onBindDevice(currentMediaItem, position); + break; + default: + Log.d(TAG, "Incorrect position: " + position); } - return false; - } - - boolean isDragging() { - return mIsDragging; - } - - int getCurrentActivePosition() { - return mCurrentActivePosition; } public MediaSwitchingController getController() { @@ -133,7 +123,7 @@ public abstract class MediaOutputBaseAdapter extends /** * ViewHolder for binding device view. */ - abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder { + class MediaDeviceViewHolderLegacy extends MediaDeviceViewHolderBase { private static final int ANIM_DURATION = 500; @@ -158,8 +148,8 @@ public abstract class MediaOutputBaseAdapter extends private ValueAnimator mVolumeAnimator; private int mLatestUpdateVolume = -1; - MediaDeviceBaseViewHolder(View view) { - super(view); + MediaDeviceViewHolderLegacy(View view, Context context) { + super(view, context); mContainerLayout = view.requireViewById(R.id.device_container); mItemLayout = view.requireViewById(R.id.item_layout); mTitleText = view.requireViewById(R.id.title); @@ -180,8 +170,10 @@ public abstract class MediaOutputBaseAdapter extends initAnimator(); } - void onBind(MediaDevice device, int position) { + void onBindDevice(MediaItem mediaItem, int position) { + MediaDevice device = mediaItem.getMediaDevice().get(); mDeviceId = device.getId(); + mItemLayout.setVisibility(View.VISIBLE); mCheckBox.setVisibility(View.GONE); mStatusIcon.setVisibility(View.GONE); mEndTouchArea.setVisibility(View.GONE); @@ -196,6 +188,54 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setProgressTintList( ColorStateList.valueOf(mController.getColorSeekbarProgress())); enableFocusPropertyForView(mContainerLayout); + renderItem(mediaItem, position); + } + + /** Binds a ViewHolder for a "Connect a device" item. */ + void onBindPairNewDevice() { + mTitleText.setTextColor(mController.getColorItemContent()); + mCheckBox.setVisibility(View.GONE); + updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new)); + updateItemBackground(ConnectionState.DISCONNECTED); + final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); + mTitleIcon.setImageDrawable(addDrawable); + mTitleIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); + } + + @Override + protected void renderDeviceItem(boolean hideGroupItem, MediaDevice device, + ConnectionState connectionState, boolean restrictVolumeAdjustment, + GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, + View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, + Drawable deviceStatusIcon) { + if (hideGroupItem) { + mItemLayout.setVisibility(View.GONE); + return; + } + updateTitle(device.getName()); + updateTitleIcon(device, connectionState, restrictVolumeAdjustment); + updateSeekBar(device, connectionState, restrictVolumeAdjustment, + getDeviceItemContentDescription(device)); + updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus); + updateLoadingIndicator(connectionState); + updateFullItemClickListener(clickListener); + updateContentAlpha(deviceDisabled); + updateSubtitle(subtitle); + updateDeviceStatusIcon(deviceStatusIcon); + updateItemBackground(connectionState); + } + + @Override + protected void renderDeviceGroupItem() { + String sessionName = mController.getSessionName() == null ? "" + : mController.getSessionName().toString(); + updateTitle(sessionName); + updateUnmutedVolumeIcon(null /* device */); + updateGroupSeekBar(getGroupItemContentDescription(sessionName)); + updateEndAreaForDeviceGroup(); + updateItemBackground(ConnectionState.CONNECTED); } void updateTitle(CharSequence title) { @@ -303,7 +343,7 @@ public abstract class MediaOutputBaseAdapter extends private void initializeSeekbarVolume( @Nullable MediaDevice device, int currentVolume, boolean isCurrentSeekbarInvisible) { - if (!mIsDragging) { + if (!isDragging()) { if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1 || currentVolume == mLatestUpdateVolume)) { // Update only if volume of device and value of volume bar doesn't match. @@ -459,6 +499,132 @@ public abstract class MediaOutputBaseAdapter extends : R.drawable.media_output_icon_volume; } + private void updateContentAlpha(boolean deviceDisabled) { + float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA; + mTitleIcon.setAlpha(alphaValue); + mTitleText.setAlpha(alphaValue); + mSubTitleText.setAlpha(alphaValue); + mStatusIcon.setAlpha(alphaValue); + } + + private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) { + if (deviceStatusIcon == null) { + mStatusIcon.setVisibility(View.GONE); + } else { + mStatusIcon.setImageDrawable(deviceStatusIcon); + mStatusIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + if (deviceStatusIcon instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) deviceStatusIcon).start(); + } + mStatusIcon.setVisibility(View.VISIBLE); + } + } + + + /** Renders the right side round pill button / checkbox. */ + private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState, + @Nullable GroupStatus groupStatus, + @Nullable OngoingSessionStatus ongoingSessionStatus) { + boolean showEndArea = false; + boolean isCheckbox = false; + // If both group status and the ongoing session status are present, only the ongoing + // session controls are displayed. The current layout design doesn't allow both group + // and ongoing session controls to be rendered simultaneously. + if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) { + showEndArea = true; + updateEndAreaForOngoingSession(device, ongoingSessionStatus.host()); + } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { + showEndArea = true; + isCheckbox = true; + updateEndAreaForGroupCheckBox(device, groupStatus); + } + updateEndAreaVisibility(showEndArea, isCheckbox); + } + + private void updateEndAreaForDeviceGroup() { + updateEndAreaWithIcon( + v -> { + onExpandGroupButtonClicked(); + }, + R.drawable.media_output_item_expand_group, + R.string.accessibility_expand_group); + updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */); + } + + private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) { + updateEndAreaWithIcon( + v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v), + isHost ? R.drawable.media_output_status_edit_session + : R.drawable.ic_sound_bars_anim, + R.string.accessibility_open_application); + } + + private void updateEndAreaWithIcon(View.OnClickListener clickListener, + @DrawableRes int iconDrawableId, + @StringRes int accessibilityStringId) { + updateEndAreaColor(mController.getColorSeekbarProgress()); + mEndClickIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + mEndClickIcon.setOnClickListener(clickListener); + mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick()); + Drawable drawable = mContext.getDrawable(iconDrawableId); + mEndClickIcon.setImageDrawable(drawable); + if (drawable instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) drawable).start(); + } + if (Flags.enableOutputSwitcherDeviceGrouping()) { + mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId)); + } + } + + private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device, + @NonNull GroupStatus groupStatus) { + boolean isEnabled = isGroupCheckboxEnabled(groupStatus); + mEndTouchArea.setOnClickListener( + isEnabled ? (v) -> mCheckBox.performClick() : null); + mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress() + : mController.getColorItemBackground()); + mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device)); + mCheckBox.setOnCheckedChangeListener(null); + mCheckBox.setChecked(groupStatus.selected()); + mCheckBox.setOnCheckedChangeListener( + isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered( + !groupStatus.selected(), device) : null); + mCheckBox.setEnabled(isEnabled); + setCheckBoxColor(mCheckBox, mController.getColorItemContent()); + } + + private void setCheckBoxColor(CheckBox checkBox, int color) { + int[][] states = {{android.R.attr.state_checked}, {}}; + int[] colors = {color, color}; + CompoundButtonCompat.setButtonTintList(checkBox, new + ColorStateList(states, colors)); + } + + private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) { + if (Flags.enableOutputSwitcherDeviceGrouping()) { + return isGroupCheckboxEnabled(groupStatus); + } + return true; + } + + private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) { + boolean disabled = groupStatus.selected() && !groupStatus.deselectable(); + return !disabled; + } + + private void updateEndAreaColor(int color) { + mEndTouchArea.setBackgroundTintList( + ColorStateList.valueOf(color)); + } + + private void updateFullItemClickListener(@Nullable View.OnClickListener listener) { + mContainerLayout.setOnClickListener(listener); + updateIconAreaClickListener(listener); + } + void updateIconAreaClickListener(@Nullable View.OnClickListener listener) { mIconAreaLayout.setOnClickListener(listener); } @@ -498,6 +664,7 @@ public abstract class MediaOutputBaseAdapter extends }); } + @Override protected void disableSeekBar() { mSeekBar.setEnabled(false); mSeekBar.setOnTouchListener((v, event) -> true); @@ -589,7 +756,7 @@ public abstract class MediaOutputBaseAdapter extends int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( seekBar.getProgress()); mStartFromMute = (currentVolume == 0); - mIsDragging = true; + setIsDragging(true); } @Override @@ -604,11 +771,25 @@ public abstract class MediaOutputBaseAdapter extends } mTitleIcon.setVisibility(View.VISIBLE); mVolumeValueText.setVisibility(View.GONE); - mIsDragging = false; + setIsDragging(false); } protected boolean shouldHandleProgressChanged() { return mMediaDevice != null; } }; } + + class MediaGroupDividerViewHolderLegacy extends RecyclerView.ViewHolder { + final TextView mTitleText; + + MediaGroupDividerViewHolderLegacy(@NonNull View itemView) { + super(itemView); + mTitleText = itemView.requireViewById(R.id.title); + } + + void onBind(String groupDividerTitle) { + mTitleText.setTextColor(mController.getColorItemContent()); + mTitleText.setText(groupDividerTitle); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 64256f97fd78..d791361d555f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -105,7 +105,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private boolean mIsLeBroadcastCallbackRegistered; private boolean mDismissing; - MediaOutputBaseAdapter mAdapter; + MediaOutputAdapterBase mAdapter; protected Executor mExecutor; @@ -342,7 +342,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap()); colorSetUpdated = !wallpaperColors.equals(mWallpaperColors); if (colorSetUpdated) { - mAdapter.updateColorScheme(wallpaperColors, isDarkThemeOn); + mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn); updateButtonBackgroundColorFilter(); updateDialogBackgroundColor(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index 9b5b872a00db..9ade9e275ca1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -245,7 +245,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { broadcastSender, mediaSwitchingController, /* includePlaybackAndAppMetadata */ true); - mAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class // that extends MediaOutputBaseDialog if (!aboveStatusbar) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index c9af7b322811..2e602be4556e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -53,7 +53,7 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index c7b165415aea..c43c1a999fcb 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -20,12 +20,15 @@ import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation @@ -33,6 +36,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOf /** * Models UI state used to render the content of the notifications shade overlay. @@ -47,6 +51,8 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, + disableFlagsInteractor: DisableFlagsInteractor, + mediaCarouselInteractor: MediaCarouselInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { @@ -69,6 +75,22 @@ constructor( ), ) + val showMedia: Boolean by + hydrator.hydratedStateOf( + traceName = "showMedia", + initialValue = + disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled() && + mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, + source = + disableFlagsInteractor.disableFlags.flatMapLatestConflated { + if (it.isQuickSettingsEnabled()) { + mediaCarouselInteractor.hasActiveMediaOrRecommendation + } else { + flowOf(false) + } + }, + ) + override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt index c9a0635021da..61a8fa3d2a6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt @@ -55,6 +55,7 @@ object SubtitleArrayMapping { subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling subtitleIdsMap["hearing_devices"] = R.array.tile_states_hearing_devices subtitleIdsMap["notes"] = R.array.tile_states_notes + subtitleIdsMap["desktopeffects"] = R.array.tile_states_desktopeffects } /** Get the subtitle resource id of the given tile */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index caa7bbae0420..e357f63479dc 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -163,4 +163,12 @@ constructor( fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + /** + * If currently in a transition between contents, cancel that transition and go back to the + * pre-transition state. + */ + fun freezeAndAnimateToCurrentState() { + dataSource.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index e9e7deca0abf..01180859b1d2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -234,6 +234,10 @@ constructor( * The change is animated. Therefore, it will be some time before the UI will switch to the * desired scene. Once enough of the transition has occurred, the [currentScene] will become * [toScene] (unless the transition is canceled by user action or another call to this method). + * + * If [forceSettleToTargetScene] is `true` and the target scene is the same as the current + * scene, any current transition will be canceled and an animation to the target scene will be + * started. */ @JvmOverloads fun changeScene( @@ -241,9 +245,19 @@ constructor( loggingReason: String, transitionKey: TransitionKey? = null, sceneState: Any? = null, + forceSettleToTargetScene: Boolean = false, ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene + + if (resolvedScene == currentSceneKey && forceSettleToTargetScene) { + logger.logSceneChangeCancellation(scene = resolvedScene, sceneState = sceneState) + onSceneAboutToChangeListener.forEach { + it.onSceneAboutToChange(resolvedScene, sceneState) + } + repository.freezeAndAnimateToCurrentState() + } + if ( !validateSceneChange( from = currentSceneKey, @@ -523,14 +537,32 @@ constructor( } if (from == to) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is the same as ${to.debugName}", + ) return false } if (to !in repository.allContentKeys) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} isn't present in allContentKeys", + ) return false } if (disabledContentInteractor.isDisabled(to)) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is currently disabled", + ) return false } @@ -580,14 +612,58 @@ constructor( } if (to != null && disabledContentInteractor.isDisabled(to)) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is currently disabled", + ) return false } - val isFromValid = (from == null) || (from in currentOverlays.value) - val isToValid = - (to == null) || (to !in currentOverlays.value && to in repository.allContentKeys) + return when { + to != null && from != null && to == from -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is the same as ${to.debugName}", + ) + false + } - return isFromValid && isToValid && from != to + to != null && to !in repository.allContentKeys -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is not in allContentKeys", + ) + false + } + + from != null && from !in currentOverlays.value -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is not a current overlay", + ) + false + } + + to != null && to in currentOverlays.value -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is already a current overlay", + ) + false + } + + else -> true + } } /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 16adf5ef976e..218ad477c45e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -554,6 +554,7 @@ constructor( targetSceneKey = Scenes.Lockscreen, loggingReason = "device is starting to sleep", sceneState = keyguardInteractor.asleepKeyguardState.value, + freezeAndAnimateToCurrentState = true, ) } else { val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value @@ -933,11 +934,13 @@ constructor( targetSceneKey: SceneKey, loggingReason: String, sceneState: Any? = null, + freezeAndAnimateToCurrentState: Boolean = false, ) { sceneInteractor.changeScene( toScene = targetSceneKey, loggingReason = loggingReason, sceneState = sceneState, + forceSettleToTargetScene = freezeAndAnimateToCurrentState, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index d00585858ccb..73c71f6088e1 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene.shared.logger +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey @@ -74,6 +75,50 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } + fun logSceneChangeCancellation(scene: SceneKey, sceneState: Any?) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { + str1 = scene.debugName + str2 = sceneState?.toString() + }, + messagePrinter = { "CANCELED scene change. scene: $str1, sceneState: $str2" }, + ) + } + + fun logSceneChangeRejection( + from: ContentKey?, + to: ContentKey?, + originalChangeReason: String, + rejectionReason: String, + ) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { + str1 = "${from?.debugName ?: "<none>"} → ${to?.debugName ?: "<none>"}" + str2 = rejectionReason + str3 = originalChangeReason + bool1 = to is OverlayKey + }, + messagePrinter = { + buildString { + append("REJECTED ") + append( + if (bool1) { + "overlay " + } else { + "scene " + } + ) + append("change $str1 because \"$str2\" ") + append("(original change reason: \"$str3\")") + } + }, + ) + } + fun logSceneTransition(transitionState: ObservableTransitionState) { when (transitionState) { is ObservableTransitionState.Transition -> { diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt index daf2d7f698b6..42c4b24a72d3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt @@ -83,4 +83,10 @@ interface SceneDataSource { /** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */ fun instantlyHideOverlay(overlay: OverlayKey) + + /** + * If currently in a transition between contents, cancel that transition and go back to the + * pre-transition state. + */ + fun freezeAndAnimateToCurrentState() } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt index dcb699539760..d6dce38d0bbf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt @@ -82,6 +82,10 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo delegateMutable.value.instantlyHideOverlay(overlay) } + override fun freezeAndAnimateToCurrentState() { + delegateMutable.value.freezeAndAnimateToCurrentState() + } + /** * Binds the current, dependency injection provided [SceneDataSource] to the given object. * @@ -120,5 +124,7 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo override fun instantlyShowOverlay(overlay: OverlayKey) = Unit override fun instantlyHideOverlay(overlay: OverlayKey) = Unit + + override fun freezeAndAnimateToCurrentState() = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index 8f4e8701cad8..1ab0b93da175 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.domain.SceneFrameworkTableLog +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn /** @@ -89,10 +91,14 @@ constructor( ) : ShadeModeInteractor { private val isDualShadeEnabled: Flow<Boolean> = - secureSettingsRepository.boolSetting( - Settings.Secure.DUAL_SHADE, - defaultValue = DUAL_SHADE_ENABLED_DEFAULT, - ) + if (SceneContainerFlag.isEnabled) { + secureSettingsRepository.boolSetting( + Settings.Secure.DUAL_SHADE, + defaultValue = DUAL_SHADE_ENABLED_DEFAULT, + ) + } else { + flowOf(false) + } override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt index c23ff5302b3c..dc444ffc2a34 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.shared.flag +import android.window.DesktopExperienceFlags import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils @@ -30,10 +31,26 @@ object ShadeWindowGoesAround { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) + /** + * This is defined as [DesktopExperienceFlags] to make it possible to enable it together with + * all the other desktop experience flags from the dev settings. + * + * Alternatively, using adb: + * ```bash + * adb shell aflags enable com.android.window.flags.show_desktop_experience_dev_option && \ + * adb shell setprop persist.wm.debug.desktop_experience_devopts 1 + * ``` + */ + val FLAG = + DesktopExperienceFlags.DesktopExperienceFlag( + Flags::shadeWindowGoesAround, + /* shouldOverrideByDevOption= */ true, + ) + /** Is the refactor enabled */ @JvmStatic inline val isEnabled: Boolean - get() = Flags.shadeWindowGoesAround() + get() = FLAG.isTrue /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index d20a2d18a7e7..edb44185459c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -145,7 +145,7 @@ constructor( * Emits all notifications that are eligible to show as chips in the status bar. This is * different from which chips will *actually* show, see [shownNotificationChips] for that. */ - private val allNotificationChips: Flow<List<NotificationChipModel>> = + val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { // For all our current interactors... // TODO(b/364653005): When a promoted notification is added or removed, each individual diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 3ecbdf82f2cb..11e9fd56288f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -51,6 +52,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, + private val systemClock: SystemClock, ) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are @@ -158,16 +160,38 @@ constructor( clickBehavior, ) } + when (this.promotedContent.time.mode) { PromotedNotificationContentModel.When.Mode.BasicTime -> { - return OngoingActivityChipModel.Active.ShortTimeDelta( - this.key, - icon, - colors, - time = this.promotedContent.time.time, - onClickListenerLegacy, - clickBehavior, - ) + return if ( + this.promotedContent.time.time >= + systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS + ) { + OngoingActivityChipModel.Active.ShortTimeDelta( + this.key, + icon, + colors, + time = this.promotedContent.time.time, + onClickListenerLegacy, + clickBehavior, + ) + } else { + // Don't show a `when` time that's close to now or in the past because it's + // likely that the app didn't intentionally set the `when` time to be shown in + // the status bar chip. + // TODO(b/393369213): If a notification sets a `when` time in the future and + // then that time comes and goes, the chip *will* start showing times in the + // past. Not going to fix this right now because the Compose implementation + // automatically handles this for us and we're hoping to launch the notification + // chips at the same time as the Compose chips. + return OngoingActivityChipModel.Active.IconOnly( + this.key, + icon, + colors, + onClickListenerLegacy, + clickBehavior, + ) + } } PromotedNotificationContentModel.When.Mode.CountUp -> { return OngoingActivityChipModel.Active.Timer( @@ -204,4 +228,12 @@ constructor( ) ) } + + companion object { + /** + * Notifications must have a `when` time of at least 1 minute in the future in order for the + * status bar chip to show the time. + */ + private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt new file mode 100644 index 000000000000..e7bc052114eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.media + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * A [CoreStartable] that initializes and starts the media control chip functionality. The media + * chip is limited to large screen devices currently. Therefore, this [CoreStartable] should not be + * used for phones or smaller form factor devices. + */ +@SysUISingleton +class MediaControlChipStartable +@Inject +constructor( + @Background val bgScope: CoroutineScope, + private val mediaControlChipInteractor: MediaControlChipInteractor, +) : CoreStartable { + + override fun start() { + bgScope.launch { mediaControlChipInteractor.initialize() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt index e3e77e16be6d..f439bb297de0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt @@ -22,13 +22,15 @@ import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -37,6 +39,8 @@ import kotlinx.coroutines.flow.stateIn * Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media * control chip. Emits a new [MediaControlChipModel] when there is an active media session and the * corresponding user preference is found, otherwise emits null. + * + * This functionality is only enabled on large screen devices. */ @SysUISingleton class MediaControlChipInteractor @@ -45,30 +49,57 @@ constructor( @Background private val backgroundScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { - private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = - mediaFilterRepository.currentMedia - .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } - .stateIn( - scope = backgroundScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList(), - ) + private val isEnabled = MutableStateFlow(false) + + private val mediaControlChipModelForScene: Flow<MediaControlChipModel?> = + combine(mediaFilterRepository.currentMedia, mediaFilterRepository.selectedUserEntries) { + mediaList, + userEntries -> + mediaList + .filterIsInstance<MediaCommonModel.MediaControl>() + .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } + .firstOrNull { it.active } + ?.toMediaControlChipModel() + } + + /** + * A flow of [MediaControlChipModel] representing the current state of the media controls chip. + * This flow emits null when no active media is playing or when playback information is + * unavailable. This flow is only active when [SceneContainerFlag] is disabled. + */ + private val mediaControlChipModelLegacy = MutableStateFlow<MediaControlChipModel?>(null) + + fun updateMediaControlChipModelLegacy(mediaData: MediaData?) { + if (!SceneContainerFlag.isEnabled) { + mediaControlChipModelLegacy.value = mediaData?.toMediaControlChipModel() + } + } + + private val _mediaControlChipModel: Flow<MediaControlChipModel?> = + if (SceneContainerFlag.isEnabled) { + mediaControlChipModelForScene + } else { + mediaControlChipModelLegacy + } /** The currently active [MediaControlChipModel] */ - val mediaControlModel: StateFlow<MediaControlChipModel?> = - combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) { - mediaControls, - userEntries -> - mediaControls - .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } - .firstOrNull { it.active } - ?.toMediaControlChipModel() + val mediaControlChipModel: StateFlow<MediaControlChipModel?> = + combine(_mediaControlChipModel, isEnabled) { mediaControlChipModel, isEnabled -> + if (isEnabled) { + mediaControlChipModel + } else { + null + } } - .stateIn( - scope = backgroundScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) + .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) + + /** + * The media control chip may not be enabled on all form factors, so only the relevant form + * factors should initialize the interactor. This must be called from a CoreStartable. + */ + fun initialize() { + isEnabled.value = true + } } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt index 19acb2e9839c..7f0f6078f391 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -55,8 +55,8 @@ constructor( * whenever the underlying [MediaControlChipModel] changes. */ override val chip: StateFlow<PopupChipModel> = - mediaControlChipInteractor.mediaControlModel - .map { mediaControlModel -> toPopupChipModel(mediaControlModel) } + mediaControlChipInteractor.mediaControlChipModel + .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 37485feed792..0e3f103c152e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -16,11 +16,74 @@ package com.android.systemui.statusbar.notification.collection; +import static android.app.NotificationChannel.NEWS_ID; +import static android.app.NotificationChannel.PROMOTIONS_ID; +import static android.app.NotificationChannel.RECS_ID; +import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + +import java.util.List; + /** * Abstract class to represent notification section bundled by AI. */ public class BundleEntry extends PipelineEntry { + private final String mKey; + private final BundleEntryAdapter mEntryAdapter; + + // TODO (b/389839319): implement the row + private ExpandableNotificationRow mRow; + + public BundleEntry(String key) { + mKey = key; + mEntryAdapter = new BundleEntryAdapter(); + } + + @VisibleForTesting + public BundleEntryAdapter getEntryAdapter() { + return mEntryAdapter; + } + public class BundleEntryAdapter implements EntryAdapter { + + /** + * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? + */ + @Override + public GroupEntry getParent() { + return GroupEntry.ROOT_ENTRY; + } + + @Override + public boolean isTopLevelEntry() { + return true; + } + + @Override + public String getKey() { + return mKey; + } + + @Override + public ExpandableNotificationRow getRow() { + return mRow; + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + return this; + } } + + public static final List<BundleEntry> ROOT_BUNDLES = List.of( + new BundleEntry(PROMOTIONS_ID), + new BundleEntry(SOCIAL_MEDIA_ID), + new BundleEntry(NEWS_ID), + new BundleEntry(RECS_ID)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index b12b1c538a32..4df81c97e21e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -16,8 +16,52 @@ package com.android.systemui.statusbar.notification.collection; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + /** * Adapter interface for UI to get relevant info. */ public interface EntryAdapter { + + /** + * Gets the parent of this entry, or null if the entry's view is not attached + */ + @Nullable PipelineEntry getParent(); + + /** + * Returns whether the entry is attached and appears at the top level of the shade + */ + boolean isTopLevelEntry(); + + /** + * @return the unique identifier for this entry + */ + @NonNull String getKey(); + + /** + * Gets the view that this entry is backing. + */ + @NonNull + ExpandableNotificationRow getRow(); + + /** + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. + */ + @Nullable + EntryAdapter getGroupRoot(); + + /** + * Returns whether the entry is attached to the current shade list + */ + default boolean isAttached() { + return getParent() != null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java index fc47dc1ed81a..8f3c357a277a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -78,7 +79,7 @@ public class NotifInflaterImpl implements NotifInflater { requireBinder().inflateViews( entry, params, - wrapInflationCallback(callback)); + wrapInflationCallback(entry, callback)); } catch (InflationException e) { mLogger.logInflationException(entry, e); mNotifErrorManager.setInflationError(entry, e); @@ -101,17 +102,26 @@ public class NotifInflaterImpl implements NotifInflater { } private NotificationRowContentBinder.InflationCallback wrapInflationCallback( + final NotificationEntry entry, InflationCallback callback) { return new NotificationRowContentBinder.InflationCallback() { @Override public void handleInflationException( NotificationEntry entry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + handleInflationException(e); + } else { + mNotifErrorManager.setInflationError(entry, e); + } + } + @Override + public void handleInflationException(Exception e) { mNotifErrorManager.setInflationError(entry, e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mNotifErrorManager.clearInflationError(entry); if (callback != null) { callback.onInflationFinished(entry, entry.getRowController()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 7dd82a6b5198..90f9525c7683 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -29,6 +29,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES; +import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING; @@ -107,6 +109,7 @@ public final class NotificationEntry extends ListEntry { private final String mKey; private StatusBarNotification mSbn; private Ranking mRanking; + private final NotifEntryAdapter mEntryAdapter; /* * Bookkeeping members @@ -268,9 +271,48 @@ public final class NotificationEntry extends ListEntry { mKey = sbn.getKey(); setSbn(sbn); setRanking(ranking); + mEntryAdapter = new NotifEntryAdapter(); } public class NotifEntryAdapter implements EntryAdapter { + @Override + public PipelineEntry getParent() { + return NotificationEntry.this.getParent(); + } + + @Override + public boolean isTopLevelEntry() { + return getParent() != null + && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent())); + } + + @Override + public String getKey() { + return NotificationEntry.this.getKey(); + } + + @Override + public ExpandableNotificationRow getRow() { + return NotificationEntry.this.getRow(); + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + // TODO (b/395857098): for backwards compatibility this will return null if called + // on a group summary that's not in a bundles, but it should return itself. + if (isTopLevelEntry() || getParent() == null) { + return null; + } + if (NotificationEntry.this.getParent().getSummary() != null) { + return NotificationEntry.this.getParent().getSummary().mEntryAdapter; + } + return null; + } + } + + public EntryAdapter getEntryAdapter() { + return mEntryAdapter; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java index efedfef5cbe9..c5a479180329 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java @@ -19,5 +19,5 @@ package com.android.systemui.statusbar.notification.collection; /** * Class to represent a notification, group, or bundle in the pipeline. */ -public class PipelineEntry { +public abstract class PipelineEntry { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java index 733b986b5422..9df4bf4af4e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java @@ -23,6 +23,7 @@ import android.app.Notification; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -31,29 +32,50 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.promoted.domain.interactor.PromotedNotificationsInteractor; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; +import com.android.systemui.util.kotlin.JavaAdapterKt; -import com.google.common.primitives.Booleans; +import kotlinx.coroutines.CoroutineScope; + +import java.util.Collections; +import java.util.List; import javax.inject.Inject; /** * Handles sectioning for foreground service notifications. - * Puts non-min colorized foreground service notifications into the FGS section. See - * {@link NotifCoordinators} for section ordering priority. + * Puts non-min colorized foreground service notifications into the FGS section. See + * {@link NotifCoordinators} for section ordering priority. */ @CoordinatorScope public class ColorizedFgsCoordinator implements Coordinator { private static final String TAG = "ColorizedCoordinator"; + private final PromotedNotificationsInteractor mPromotedNotificationsInteractor; + private final CoroutineScope mMainScope; + + private List<String> mOrderedPromotedNotifKeys = Collections.emptyList(); @Inject - public ColorizedFgsCoordinator() { + public ColorizedFgsCoordinator( + @Application CoroutineScope mainScope, + PromotedNotificationsInteractor promotedNotificationsInteractor + ) { + mPromotedNotificationsInteractor = promotedNotificationsInteractor; + mMainScope = mainScope; } @Override - public void attach(NotifPipeline pipeline) { + public void attach(@NonNull NotifPipeline pipeline) { if (PromotedNotificationUi.isEnabled()) { pipeline.addPromoter(mPromotedOngoingPromoter); + + JavaAdapterKt.collectFlow(mMainScope, + mPromotedNotificationsInteractor.getOrderedChipNotificationKeys(), + (List<String> keys) -> { + mOrderedPromotedNotifKeys = keys; + mNotifSectioner.invalidateList("updated mOrderedPromotedNotifKeys"); + }); } } @@ -82,12 +104,24 @@ public class ColorizedFgsCoordinator implements Coordinator { return false; } - private NotifComparator mPreferPromoted = new NotifComparator("PreferPromoted") { + /** get the sort key for any entry in the ongoing section */ + private int getSortKey(@Nullable NotificationEntry entry) { + if (entry == null) return Integer.MAX_VALUE; + // Order all promoted notif keys first, using their order in the list + final int index = mOrderedPromotedNotifKeys.indexOf(entry.getKey()); + if (index >= 0) return index; + // Next, prioritize promoted ongoing over other notifications + return isPromotedOngoing(entry) ? Integer.MAX_VALUE - 1 : Integer.MAX_VALUE; + } + + private final NotifComparator mOngoingComparator = new NotifComparator( + "OngoingComparator") { @Override public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) { - return -1 * Booleans.compare( - isPromotedOngoing(o1.getRepresentativeEntry()), - isPromotedOngoing(o2.getRepresentativeEntry())); + return Integer.compare( + getSortKey(o1.getRepresentativeEntry()), + getSortKey(o2.getRepresentativeEntry()) + ); } }; @@ -95,7 +129,7 @@ public class ColorizedFgsCoordinator implements Coordinator { @Override public NotifComparator getComparator() { if (PromotedNotificationUi.isEnabled()) { - return mPreferPromoted; + return mOngoingComparator; } else { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java index d47fe20911f9..2e3ab926ad57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -129,21 +130,42 @@ public class HighPriorityProvider { >= NotificationManager.IMPORTANCE_DEFAULT); } + /** + * Returns whether the given ListEntry has a high priority child or is in a group with a child + * that's high priority + */ private boolean hasHighPriorityChild(ListEntry entry, boolean allowImplicit) { - if (entry instanceof NotificationEntry - && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { - return false; - } - - List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); - if (children != null) { - for (NotificationEntry child : children) { - if (child != entry && isHighPriority(child, allowImplicit)) { - return true; + if (NotificationBundleUi.isEnabled()) { + GroupEntry representativeGroupEntry = null; + if (entry instanceof GroupEntry) { + representativeGroupEntry = (GroupEntry) entry; + } else if (entry instanceof NotificationEntry){ + final NotificationEntry notificationEntry = entry.getRepresentativeEntry(); + if (notificationEntry.getParent() != null + && notificationEntry.getParent().getSummary() != null + && notificationEntry.getParent().getSummary() == notificationEntry) { + representativeGroupEntry = notificationEntry.getParent(); } } + return representativeGroupEntry != null && + representativeGroupEntry.getChildren().stream().anyMatch( + childEntry -> isHighPriority(childEntry, allowImplicit)); + + } else { + if (entry instanceof NotificationEntry + && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { + return false; + } + List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); + if (children != null) { + for (NotificationEntry child : children) { + if (child != entry && isHighPriority(child, allowImplicit)) { + return true; + } + } + } + return false; } - return false; } private boolean hasHighPriorityCharacteristics(NotificationEntry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java index 30386ab46382..ea369463da51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.render; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -38,6 +39,20 @@ public interface GroupExpansionManager { boolean isGroupExpanded(NotificationEntry entry); /** + * Whether the parent associated with this notification is expanded. + * If this notification is not part of a group or bundle, it will always return false. + */ + boolean isGroupExpanded(EntryAdapter entry); + + /** + * Set whether the group/bundle associated with this notification is expanded or not. + */ + void setGroupExpanded(EntryAdapter entry, boolean expanded); + + /** @return group/bundle expansion state after toggling. */ + boolean toggleGroupExpansion(EntryAdapter entry); + + /** * Set whether the group associated with this notification is expanded or not. */ void setGroupExpanded(NotificationEntry entry, boolean expanded); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index d1aff80b4e7c..16b98e20498a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -23,11 +23,13 @@ import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.io.PrintWriter; import java.util.ArrayList; @@ -55,6 +57,8 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl */ private final Set<NotificationEntry> mExpandedGroups = new HashSet<>(); + private final Set<EntryAdapter> mExpandedCollections = new HashSet<>(); + @Inject public GroupExpansionManagerImpl(DumpManager dumpManager, GroupMembershipManager groupMembershipManager) { @@ -63,11 +67,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } /** - * Cleanup entries from mExpandedGroups that no longer exist in the pipeline. + * Cleanup entries from internal tracking that no longer exist in the pipeline. */ private final OnBeforeRenderListListener mNotifTracker = (entries) -> { - if (mExpandedGroups.isEmpty()) { - return; // nothing to do + if (NotificationBundleUi.isEnabled()) { + if (mExpandedCollections.isEmpty()) { + return; // nothing to do + } + } else { + if (mExpandedGroups.isEmpty()) { + return; // nothing to do + } } final Set<NotificationEntry> renderingSummaries = new HashSet<>(); @@ -77,10 +87,25 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } } - // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. - final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); - for (NotificationEntry entry : groupsToRemove) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entryAdapter : mExpandedCollections) { + boolean isInPipeline = false; + for (NotificationEntry entry : renderingSummaries) { + if (entry.getKey().equals(entryAdapter.getKey())) { + isInPipeline = true; + break; + } + } + if (!isInPipeline) { + setGroupExpanded(entryAdapter, false); + } + } + } else { + // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. + final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); + for (NotificationEntry entry : groupsToRemove) { + setGroupExpanded(entry, false); + } } }; @@ -96,11 +121,13 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean isGroupExpanded(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); return mExpandedGroups.contains(mGroupMembershipManager.getGroupSummary(entry)); } @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); if (entry.getParent() == null) { if (expanded) { @@ -127,14 +154,61 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean toggleGroupExpansion(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); + setGroupExpanded(entry, !isGroupExpanded(entry)); + return isGroupExpanded(entry); + } + + @Override + public boolean isGroupExpanded(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry)); + } + + @Override + public void setGroupExpanded(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); + EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry); + if (!entry.isAttached()) { + if (expanded) { + Log.wtf(TAG, "Cannot expand group that is not attached"); + } else { + // The entry is no longer attached, but we still want to make sure we don't have + // a stale expansion state. + groupParent = entry; + } + } + + boolean changed; + if (expanded) { + changed = mExpandedCollections.add(groupParent); + } else { + changed = mExpandedCollections.remove(groupParent); + } + + // Only notify listeners if something changed. + if (changed) { + sendOnGroupExpandedChange(entry, expanded); + } + } + + @Override + public boolean toggleGroupExpansion(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); setGroupExpanded(entry, !isGroupExpanded(entry)); return isGroupExpanded(entry); } @Override public void collapseGroups() { - for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entry : new ArrayList<>(mExpandedCollections)) { + setGroupExpanded(entry, false); + } + } else { + for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { + setGroupExpanded(entry, false); + } } } @@ -145,9 +219,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl for (NotificationEntry entry : mExpandedGroups) { pw.println(" * " + entry.getKey()); } + pw.println(" mExpandedCollection: " + mExpandedCollections.size()); + for (EntryAdapter entry : mExpandedCollections) { + pw.println(" * " + entry.getKey()); + } } private void sendOnGroupExpandedChange(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); + for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { + listener.onGroupExpansionChange(entry.getRow(), expanded); + } + } + + private void sendOnGroupExpandedChange(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { listener.onGroupExpansionChange(entry.getRow(), expanded); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java index 3158782e6fea..69267e5d9e55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render; import android.annotation.NonNull; import android.annotation.Nullable; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -29,6 +30,13 @@ import java.util.List; * generally assumes that the notification is attached (aka its parent is not null). */ public interface GroupMembershipManager { + + /** + * @return whether a given entry is the root (GroupEntry or BundleEntry) in a collection which + * has children + */ + boolean isGroupRoot(@NonNull EntryAdapter entry); + /** * @return whether a given notification is the summary in a group which has children */ @@ -42,16 +50,15 @@ public interface GroupMembershipManager { NotificationEntry getGroupSummary(@NonNull NotificationEntry entry); /** - * Similar to {@link #getGroupSummary(NotificationEntry)} but doesn't get the visual summary - * but the logical summary, i.e when a child is isolated, it still returns the summary as if - * it wasn't isolated. - * TODO: remove this when migrating to the new pipeline, this is taken care of in the - * dismissal logic built into NotifCollection + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. */ @Nullable - default NotificationEntry getLogicalGroupSummary(@NonNull NotificationEntry entry) { - return getGroupSummary(entry); - } + EntryAdapter getGroupRoot(@NonNull EntryAdapter entry); /** * @return whether a given notification is a child in a group @@ -59,9 +66,10 @@ public interface GroupMembershipManager { boolean isChildInGroup(@NonNull NotificationEntry entry); /** - * Whether this is the only child in a group + * @return whether a given notification is a child in a group. The group may be a notification + * group or a bundle. */ - boolean isOnlyChildInGroup(@NonNull NotificationEntry entry); + boolean isChildInGroup(@NonNull EntryAdapter entry); /** * Get the children that are in the summary's group, not including those isolated. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java index da1247953c4c..80a9f8adf8f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java @@ -22,9 +22,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -41,6 +43,7 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { @Override public boolean isGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry.getParent() == null) { // The entry is not attached, so it doesn't count. return false; @@ -49,33 +52,47 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { return entry.getParent().getSummary() == entry; } + @Override + public boolean isGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry == entry.getGroupRoot(); + } + @Nullable @Override public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (isTopLevelEntry(entry) || entry.getParent() == null) { return null; } return entry.getParent().getSummary(); } + @Nullable + @Override + public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry.getGroupRoot(); + } + @Override public boolean isChildInGroup(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); // An entry is a child if it's not a summary or top level entry, but it is attached. return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null; } @Override - public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) { - if (entry.getParent() == null) { - return false; // The entry is not attached. - } - - return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1; + public boolean isChildInGroup(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + // An entry is a child if it's not a group root or top level entry, but it is attached. + return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry(); } @Nullable @Override public List<NotificationEntry> getChildren(@NonNull ListEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry instanceof GroupEntry) { return ((GroupEntry) entry).getChildren(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index be61dc95fe20..7d74a496853f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -46,6 +46,7 @@ import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; @@ -55,6 +56,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -118,7 +120,8 @@ public class HeadsUpManagerImpl @VisibleForTesting final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); private final HeadsUpManagerLogger mLogger; - private final int mMinimumDisplayTime; + private final int mMinimumDisplayTimeDefault; + private final int mMinimumDisplayTimeForUserInitiated; private final int mStickyForSomeTimeAutoDismissTime; private final int mAutoDismissTime; private final DelayableExecutor mExecutor; @@ -215,9 +218,11 @@ public class HeadsUpManagerImpl mGroupMembershipManager = groupMembershipManager; mVisualStabilityProvider = visualStabilityProvider; Resources resources = context.getResources(); - mMinimumDisplayTime = NotificationThrottleHun.isEnabled() + mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled() ? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling) : resources.getInteger(R.integer.heads_up_notification_minimum_time); + mMinimumDisplayTimeForUserInitiated = resources.getInteger( + R.integer.heads_up_notification_minimum_time_for_user_initiated); mStickyForSomeTimeAutoDismissTime = resources.getInteger( R.integer.sticky_heads_up_notification_time); mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); @@ -871,14 +876,24 @@ public class HeadsUpManagerImpl if (!hasPinnedHeadsUp() || topEntry == null) { return null; } else { + ExpandableNotificationRow topRow = topEntry.getRow(); if (topEntry.rowIsChildInGroup()) { - final NotificationEntry groupSummary = - mGroupMembershipManager.getGroupSummary(topEntry); - if (groupSummary != null) { - topEntry = groupSummary; + if (NotificationBundleUi.isEnabled()) { + final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot( + topRow.getEntryAdapter()); + if (adapter != null) { + topRow = adapter.getRow(); + } + } else { + final NotificationEntry groupSummary = + mGroupMembershipManager.getGroupSummary(topEntry); + if (groupSummary != null) { + topEntry = groupSummary; + topRow = topEntry.getRow(); + } } } - ExpandableNotificationRow topRow = topEntry.getRow(); + int[] tmpArray = new int[2]; topRow.getLocationOnScreen(tmpArray); int minX = tmpArray[0]; @@ -1358,7 +1373,12 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); if (updateEarliestRemovalTime) { - mEarliestRemovalTime = now + mMinimumDisplayTime; + if (StatusBarNotifChips.isEnabled() + && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) { + mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated; + } else { + mEarliestRemovalTime = now + mMinimumDisplayTimeDefault; + } } if (updatePostTime) { @@ -1377,7 +1397,7 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); return NotificationThrottleHun.isEnabled() ? Math.max(finishTime, mEarliestRemovalTime) - now - : Math.max(finishTime - now, mMinimumDisplayTime); + : Math.max(finishTime - now, mMinimumDisplayTimeDefault); }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt index 691f1f452da8..f755dbb48e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import javax.inject.Inject import kotlin.math.max @@ -112,14 +113,26 @@ class PeopleNotificationIdentifierImpl @Inject constructor( if (personExtractor.isPersonNotification(sbn)) TYPE_PERSON else TYPE_NON_PERSON private fun getPeopleTypeOfSummary(entry: NotificationEntry): Int { - if (!groupManager.isGroupSummary(entry)) { - return TYPE_NON_PERSON + if (NotificationBundleUi.isEnabled) { + if (!entry.sbn.notification.isGroupSummary) { + return TYPE_NON_PERSON; + } + + return getPeopleTypeForChildList(entry.parent?.children) + } else { + if (!groupManager.isGroupSummary(entry)) { + return TYPE_NON_PERSON + } + + return getPeopleTypeForChildList(groupManager.getChildren(entry)) } + } - val childTypes = groupManager.getChildren(entry) - ?.asSequence() - ?.map { getPeopleNotificationType(it) } - ?: return TYPE_NON_PERSON + private fun getPeopleTypeForChildList(children: List<NotificationEntry>?): Int { + val childTypes = children + ?.asSequence() + ?.map { getPeopleNotificationType(it) } + ?: return TYPE_NON_PERSON var groupType = TYPE_NON_PERSON for (childType in childTypes) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt index 393f95d3ad77..4bc685423659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.util.kotlin.FlowDumperImpl @@ -30,13 +29,12 @@ import kotlinx.coroutines.flow.map class AODPromotedNotificationInteractor @Inject constructor( - activeNotificationsInteractor: ActiveNotificationsInteractor, + promotedNotificationsInteractor: PromotedNotificationsInteractor, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { + /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = - activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> - notifs.firstNotNullOfOrNull { it.promotedContent } - } + promotedNotificationsInteractor.topPromotedNotificationContent val isPresent: Flow<Boolean> = content diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt new file mode 100644 index 000000000000..1015cfbefc41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * An interactor that provides details about promoted notification precedence, based on the + * presented order of current notification status bar chips. + */ +@SysUISingleton +class PromotedNotificationsInteractor +@Inject +constructor( + activeNotificationsInteractor: ActiveNotificationsInteractor, + callChipInteractor: CallChipInteractor, + notifChipsInteractor: StatusBarNotificationChipsInteractor, + @Background backgroundDispatcher: CoroutineDispatcher, +) { + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. + */ + private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> = + combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) { + callState, + notifChips -> + buildList { + val callData = callState.getNotifData()?.also { add(it) } + addAll( + notifChips.mapNotNull { + when (it.key) { + callData?.key -> null // do not re-add the same call + else -> NotifAndPromotedContent(it.key, it.promotedContent) + } + } + ) + } + } + + private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = + when (this) { + is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) + is OngoingCallModel.InCallWithVisibleApp -> + // TODO(b/395989259): support InCallWithVisibleApp when it has notif data + null + is OngoingCallModel.NoCall -> null + } + + /** + * The top promoted notification represented by a chip, with the order determined by the order + * of the chips, not the notifications. + */ + private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> = + orderedChipNotifications + .map { list -> list.firstNotNullOfOrNull { it.promotedContent } } + .distinctUntilNewInstance() + + /** This is the top-most promoted notification, which should avoid regular changing. */ + val topPromotedNotificationContent: Flow<PromotedNotificationContentModel?> = + combine( + topPromotedChipNotification, + activeNotificationsInteractor.topLevelRepresentativeNotifications, + ) { topChipNotif, topLevelNotifs -> + topChipNotif ?: topLevelNotifs.firstNotNullOfOrNull { it.promotedContent } + } + // #equals() can be a bit expensive on this object, but this flow will regularly try to + // emit the same immutable instance over and over, so just prevent that. + .distinctUntilNewInstance() + + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. Flows on the background context. + */ + val orderedChipNotificationKeys: Flow<List<String>> = + orderedChipNotifications + .map { list -> list.map { it.key } } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + /** + * Returns flow where all subsequent repetitions of the same object instance are filtered out. + */ + private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b } + + /** + * A custom pair, but providing clearer semantic names, and implementing equality as being the + * same instance of the promoted content model, which allows us to use distinctUntilChanged() on + * flows containing this without doing pixel comparisons on the Bitmaps inside Icon objects + * provided by the Notification. + */ + private data class NotifAndPromotedContent( + val key: String, + val promotedContent: PromotedNotificationContentModel?, + ) { + /** + * Define the equals of this object to only check the reference equality of the promoted + * content so that we can mark. + */ + override fun equals(other: Any?): Boolean { + return when { + other == null -> false + other === this -> true + other !is NotifAndPromotedContent -> return false + else -> key == other.key && promotedContent === other.promotedContent + } + } + + /** Define the hashCode to be very quick, even if it increases collisions. */ + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0) + return result + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index d1d1ea9b5ff4..1568e9e66c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -105,6 +105,7 @@ import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -120,6 +121,7 @@ import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedac import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix; import com.android.systemui.statusbar.notification.stack.AmbientState; @@ -268,6 +270,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private String mLoggingKey; private NotificationGuts mGuts; private NotificationEntry mEntry; + private EntryAdapter mEntryAdapter; private String mAppName; private NotificationRebindingTracker mRebindingTracker; private FalsingManager mFalsingManager; @@ -390,11 +393,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { - if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) - && mGroupMembershipManager.isGroupSummary(mEntry)) { + boolean isGroupRoot = NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry); + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) { mGroupExpansionChanging = true; - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); - boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry); + final boolean wasExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter) + : mGroupExpansionManager.isGroupExpanded(mEntry); + boolean nowExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter) + : mGroupExpansionManager.toggleGroupExpansion(mEntry); mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); if (shouldLogExpandClickMetric) { mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded); @@ -910,6 +919,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mEntry; } + @Nullable + public EntryAdapter getEntryAdapter() { + NotificationBundleUi.assertInNewMode(); + return mEntryAdapter; + } + @Override public boolean isHeadsUp() { return mIsHeadsUp; @@ -2010,11 +2025,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * * @param context context context of the view * @param attrs attributes used to initialize parent view + * @param user the user the row is associated to + */ + public ExpandableNotificationRow(Context context, AttributeSet attrs, UserHandle user) { + this(context, attrs, userContextForEntry(context, user)); + NotificationBundleUi.assertInNewMode(); + } + + /** + * Constructs an ExpandableNotificationRow. Used by layout inflation (with a custom {@code + * AsyncLayoutFactory} in {@link RowInflaterTask}. + * + * @param context context context of the view + * @param attrs attributes used to initialize parent view * @param entry notification that the row will be associated to (determines the user for the * ImageResolver) */ public ExpandableNotificationRow(Context context, AttributeSet attrs, NotificationEntry entry) { this(context, attrs, userContextForEntry(context, entry)); + NotificationBundleUi.assertInLegacyMode(); } private static Context userContextForEntry(Context base, NotificationEntry entry) { @@ -2025,6 +2054,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView UserHandle.of(entry.getSbn().getNormalizedUserId()), /* flags= */ 0); } + private static Context userContextForEntry(Context base, UserHandle user) { + if (base.getUserId() == user.getIdentifier()) { + return base; + } + return base.createContextAsUser(user, /* flags= */ 0); + } + private ExpandableNotificationRow(Context sysUiContext, AttributeSet attrs, Context userContext) { super(sysUiContext, attrs); @@ -2067,7 +2103,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView IStatusBarService statusBarService, UiEventLogger uiEventLogger, NotificationRebindingTracker notificationRebindingTracker) { - mEntry = entry; + + if (NotificationBundleUi.isEnabled()) { + // TODO (b/395857098): remove when all usages are migrated + mEntryAdapter = entry.getEntryAdapter(); + mEntry = entry; + } else { + mEntry = entry; + } mAppName = appName; mRebindingTracker = notificationRebindingTracker; if (mMenuRow == null) { @@ -2876,7 +2919,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion && !mChildrenContainer.showingAsLowPriority()) { - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + final boolean wasExpanded = isGroupExpanded(); mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded); onExpansionChanged(true /* userAction */, wasExpanded); return; @@ -3031,6 +3074,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean isGroupExpanded() { + if (NotificationBundleUi.isEnabled()) { + return mGroupExpansionManager.isGroupExpanded(mEntryAdapter); + } return mGroupExpansionManager.isGroupExpanded(mEntry); } @@ -3187,12 +3233,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setSensitive(boolean sensitive, boolean hideSensitive) { + if (notificationsRedesignTemplates() + && sensitive == mSensitive && hideSensitive == mSensitiveHiddenInGeneral) { + return; // nothing has changed + } + int intrinsicBefore = getIntrinsicHeight(); mSensitive = sensitive; mSensitiveHiddenInGeneral = hideSensitive; int intrinsicAfter = getIntrinsicHeight(); if (intrinsicBefore != intrinsicAfter) { notifyHeightChanged(/* needsAnimation= */ true); + } else if (notificationsRedesignTemplates()) { + // Just request the correct layout, even if the height hasn't changed + getShowingLayout().requestSelectLayout(/* needsAnimation= */ true); } } @@ -3227,11 +3281,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } boolean oldShowingPublic = mShowingPublic; mShowingPublic = mSensitive && hideSensitive; - if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) { + boolean isShowingLayoutNotChanged = mShowingPublic == oldShowingPublic; + if (mShowingPublicInitialized && isShowingLayoutNotChanged) { return; } - if (!animated) { + final boolean shouldSkipHideSensitiveAnimation = + Flags.skipHideSensitiveNotifAnimation() && isShowingLayoutNotChanged; + if (!animated || shouldSkipHideSensitiveAnimation) { if (!NotificationContentAlphaOptimization.isEnabled() || mShowingPublic != oldShowingPublic) { // Don't reset the alpha or cancel the animation if the showing layout doesn't @@ -3766,7 +3823,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void onExpandedByGesture(boolean userExpanded) { int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; - if (mGroupMembershipManager.isGroupSummary(mEntry)) { + if (NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry)) { event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; } mMetricsLogger.action(event, userExpanded); @@ -3802,7 +3861,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { - nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + nowExpanded = isGroupExpanded(); } // Note: nowExpanded is going to be true here on the first expansion of minimized groups, // even though the group itself is not expanded. Use mGroupExpansionManager to get the real diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index e311b53bfa64..0c1dd2e026b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; @@ -1457,12 +1458,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } @Override - public void handleInflationException(NotificationEntry entry, Exception e) { + public void handleInflationException(Exception e) { handleError(e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mEntry.onInflationTaskFinished(); mRow.onNotificationUpdated(); if (mCallback != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 0be1d5d9d79d..05934e7edfba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.lang.annotation.Retention; @@ -170,13 +171,29 @@ public interface NotificationRowContentBinder { * @param entry notification which failed to inflate content * @param e exception */ - void handleInflationException(NotificationEntry entry, Exception e); + default void handleInflationException(NotificationEntry entry, Exception e) { + handleInflationException(e); + } + + /** + * Callback for when there is an inflation exception + * + * @param e exception + */ + void handleInflationException(Exception e); /** * Callback for after the content views finish inflating. * * @param entry the entry with the content views set */ - void onAsyncInflationFinished(NotificationEntry entry); + default void onAsyncInflationFinished(NotificationEntry entry) { + onAsyncInflationFinished(); + } + + /** + * Callback for after the content views finish inflating. + */ + void onAsyncInflationFinished(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 517fc3a06d84..761d3fe91cd0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.NmSummarizationUiFlag +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -76,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationConten import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.policy.InflatedSmartReplyState import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder @@ -536,7 +538,7 @@ constructor( val ident: String = (sbn.packageName + "/0x" + Integer.toHexString(sbn.id)) Log.e(TAG, "couldn't inflate view for notification $ident", e) callback?.handleInflationException( - row.entry, + if (NotificationBundleUi.isEnabled) entry else row.entry, InflationException("Couldn't inflate contentViews$e"), ) @@ -554,11 +556,11 @@ constructor( logger.logAsyncTaskProgress(entry, "aborted") } - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { handleError(e) } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { this.entry.onInflationTaskFinished() row.onNotificationUpdated() callback?.onAsyncInflationFinished(this.entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 6883ec575d7e..da361406fa2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -21,10 +21,12 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import androidx.annotation.NonNull; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -52,7 +54,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @Override protected void executeStage( - @NonNull NotificationEntry entry, + final @NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @NonNull StageCallback callback) { RowContentBindParams params = getStageParams(entry); @@ -77,15 +79,35 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { InflationCallback inflationCallback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, Exception e) { - mNotifInflationErrorManager.setInflationError(entry, e); + public void handleInflationException(NotificationEntry errorEntry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.setInflationError(entry, e); + } else { + mNotifInflationErrorManager.setInflationError(errorEntry, e); + } + } + + @Override + public void handleInflationException(Exception e) { + } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { - mNotifInflationErrorManager.clearInflationError(entry); - getStageParams(entry).clearDirtyContentViews(); - callback.onStageFinished(entry); + public void onAsyncInflationFinished(NotificationEntry finishedEntry) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.clearInflationError(entry); + getStageParams(entry).clearDirtyContentViews(); + callback.onStageFinished(entry); + } else { + mNotifInflationErrorManager.clearInflationError(finishedEntry); + getStageParams(finishedEntry).clearDirtyContentViews(); + callback.onStageFinished(finishedEntry); + } + } + + @Override + public void onAsyncInflationFinished() { + } }; mBinder.cancelBind(entry, row); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java index 9f634bef4c5e..d60e37423e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import android.content.Context; +import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -30,8 +31,10 @@ import androidx.asynclayoutinflater.view.AsyncLayoutFactory; import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import com.android.systemui.res.R; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.SystemClock; import java.util.concurrent.Executor; @@ -53,11 +56,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; private long mInflateStartTimeMs; + private UserTracker mUserTracker; @Inject - public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) { + public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger, + UserTracker userTracker) { mSystemClock = systemClock; mLogger = logger; + mUserTracker = userTracker; } /** @@ -107,7 +113,8 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) { - return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger); + return new RowAsyncLayoutInflater( + entry, mSystemClock, mLogger, mUserTracker.getUserHandle()); } /** @@ -148,12 +155,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private final NotificationEntry mEntry; private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; + private final UserHandle mTargetUser; public RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock, - RowInflaterTaskLogger logger) { + RowInflaterTaskLogger logger, UserHandle targetUser) { mEntry = entry; mSystemClock = systemClock; mLogger = logger; + mTargetUser = targetUser; } @Nullable @@ -165,8 +174,12 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } final long startMs = mSystemClock.elapsedRealtime(); - final ExpandableNotificationRow row = - new ExpandableNotificationRow(context, attrs, mEntry); + ExpandableNotificationRow row = null; + if (NotificationBundleUi.isEnabled()) { + row = new ExpandableNotificationRow(context, attrs, mTargetUser); + } else { + row = new ExpandableNotificationRow(context, attrs, mEntry); + } final long elapsedMs = mSystemClock.elapsedRealtime() - startMs; mLogger.logCreatedRow(mEntry, elapsedMs); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index b9352bf64be4..3ee827332877 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -122,6 +122,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -2958,9 +2959,13 @@ public class NotificationStackScrollLayout } private boolean isChildInGroup(View child) { - return child instanceof ExpandableNotificationRow - && mGroupMembershipManager.isChildInGroup( - ((ExpandableNotificationRow) child).getEntry()); + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow childRow = (ExpandableNotificationRow) child; + return NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter()) + : mGroupMembershipManager.isChildInGroup(childRow.getEntry()); + } + return false; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 6385d53dbc8b..10b665d8ef01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -25,7 +25,7 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.nano.MetricsProto import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.repeatWhenAttachedToWindow import com.android.systemui.plugins.FalsingManager @@ -76,7 +76,7 @@ import kotlinx.coroutines.flow.stateIn class NotificationListViewBinder @Inject constructor( - @Background private val backgroundDispatcher: CoroutineDispatcher, + @NotifInflation private val inflationDispatcher: CoroutineDispatcher, private val hiderTracker: DisplaySwitchNotificationsHiderTracker, @ShadeDisplayAware private val configuration: ConfigurationState, private val falsingManager: FalsingManager, @@ -155,7 +155,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { footerView: FooterView -> traceAsync("bind FooterView") { parentView.setFooterView(footerView) @@ -240,7 +240,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { emptyShadeView: EmptyShadeView -> traceAsync("bind EmptyShadeView") { parentView.setEmptyShadeView(emptyShadeView) 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 2c8c7a1bdd44..54efa4a2bcf2 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 @@ -48,7 +48,6 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel @@ -137,7 +136,6 @@ constructor( private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, - private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, @@ -574,7 +572,6 @@ constructor( aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.notificationAlpha, - dozingToDreamingTransitionViewModel.notificationAlpha, dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 1dc9de489806..05a46cd9fa31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; @@ -215,7 +216,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, if (ExpandHeadsUpOnInlineReply.isEnabled()) { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } else if (!row.isChildInGroup()) { final boolean expandNotification; if (row.isPinned()) { @@ -233,7 +238,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, } else { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } if (android.app.Flags.compactHeadsUpNotificationReply() diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 28cf78f6777e..9f60fe212567 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -18,8 +18,10 @@ package com.android.systemui.theme; import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8; +import static com.android.systemui.Flags.hardwareColorStyles; import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; +import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET; @@ -73,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -99,6 +102,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -136,9 +140,11 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final DeviceProvisionedController mDeviceProvisionedController; private final Resources mResources; // Current wallpaper colors associated to a user. - private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); + @VisibleForTesting + protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); private final WallpaperManager mWallpaperManager; private final ActivityManager mActivityManager; + protected final SystemPropertiesHelper mSystemPropertiesHelper; @VisibleForTesting protected ColorScheme mColorScheme; // If fabricated overlays were already created for the current theme. @@ -423,7 +429,9 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { JavaAdapter javaAdapter, KeyguardTransitionInteractor keyguardTransitionInteractor, UiModeManager uiModeManager, - ActivityManager activityManager) { + ActivityManager activityManager, + SystemPropertiesHelper systemPropertiesHelper + ) { mContext = context; mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY); @@ -443,6 +451,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mKeyguardTransitionInteractor = keyguardTransitionInteractor; mUiModeManager = uiModeManager; mActivityManager = activityManager; + mSystemPropertiesHelper = systemPropertiesHelper; dumpManager.registerDumpable(TAG, this); Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor @@ -498,29 +507,38 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + WallpaperColors systemColor; + if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) { + Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults(); + mThemeStyle = defaultSettings.first; + Color seedColor = defaultSettings.second; + + // we only use the first color anyway, so we can pass only the single color we have + systemColor = new WallpaperColors( + /*primaryColor*/ seedColor, + /*secondaryColor*/ seedColor, + /*tertiaryColor*/ seedColor + ); + } else { + systemColor = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + } + // Upon boot, make sure we have the most up to date colors Runnable updateColors = () -> { - WallpaperColors systemColor = mWallpaperManager.getWallpaperColors( - getDefaultWallpaperColorsSource(mUserTracker.getUserId())); - Runnable applyColors = () -> { - if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); - mCurrentColors.put(mUserTracker.getUserId(), systemColor); - reevaluateSystemTheme(false /* forceReload */); - }; - if (mDeviceProvisionedController.isCurrentUserSetup()) { - mMainExecutor.execute(applyColors); - } else { - applyColors.run(); - } + if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); + mCurrentColors.put(mUserTracker.getUserId(), systemColor); + reevaluateSystemTheme(false /* forceReload */); }; // Whenever we're going directly to setup wizard, we need to process colors synchronously, // otherwise we'll see some jank when the activity is recreated. if (!mDeviceProvisionedController.isCurrentUserSetup()) { - updateColors.run(); + mMainExecutor.execute(updateColors); } else { mBgExecutor.execute(updateColors); } + mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null, UserHandle.USER_ALL); @@ -604,7 +622,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { @VisibleForTesting protected boolean isPrivateProfile(UserHandle userHandle) { - Context usercontext = mContext.createContextAsUser(userHandle,0); + Context usercontext = mContext.createContextAsUser(userHandle, 0); return usercontext.getSystemService(UserManager.class).isPrivateProfile(); } @@ -720,6 +738,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return true; } + @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized private void updateThemeOverlays() { final int currentUser = mUserTracker.getUserId(); final String overlayPackageJson = mSecureSettings.getStringForUser( @@ -746,7 +765,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE); if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) { try { - String colorString = systemPalette.getPackageName().toLowerCase(); + String colorString = systemPalette.getPackageName().toLowerCase(); if (!colorString.startsWith("#")) { colorString = "#" + colorString; } @@ -856,6 +875,75 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return style; } + protected Pair<Integer, String> getHardwareColorSetting() { + String deviceColorProperty = "ro.boot.hardware.color"; + + String[] themeData = mResources.getStringArray( + com.android.internal.R.array.theming_defaults); + + // Color can be hex (`#FF0000`) or `home_wallpaper` + Map<String, Pair<Integer, String>> themeMap = new HashMap<>(); + + // extract all theme settings + for (String themeEntry : themeData) { + String[] themeComponents = themeEntry.split("\\|"); + if (themeComponents.length != 3) continue; + themeMap.put(themeComponents[0], + new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2])); + } + + Pair<Integer, String> fallbackTheme = themeMap.get("*"); + if (fallbackTheme == null) { + Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME); + fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME); + } + + String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty); + Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue); + if (selectedTheme == null) { + Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue + + "' not found in theming_defaults: " + Arrays.toString(themeData)); + selectedTheme = fallbackTheme; + } + + return selectedTheme; + } + + @VisibleForTesting + protected Pair<Integer, Color> getThemeSettingsDefaults() { + + Pair<Integer, String> selectedTheme = getHardwareColorSetting(); + + // Last fallback color + Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE); + + // defaultColor will come from wallpaper or be parsed from a string + boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME); + + if (isWallpaper) { + WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + + if (wallpaperColors != null) { + defaultSeedColor = wallpaperColors.getPrimaryColor(); + } + + Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } else { + try { + defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second)); + Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error parsing color: " + selectedTheme.second, e); + // defaultSeedColor remains unchanged in this case + } + } + + return new Pair<>(selectedTheme.first, defaultSeedColor); + } + @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mSystemColors=" + mCurrentColors); diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt index e5c1e7daa25a..79ff38eabc08 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.util.settings.SettingsSingleThreadBackground import dagger.Module @@ -123,4 +124,19 @@ class SysUICoroutinesModule { ): CoroutineContext { return uiBgCoroutineDispatcher } + + /** Coroutine dispatcher for background notification inflation. */ + @Provides + @NotifInflation + @SysUISingleton + fun notifInflationCoroutineDispatcher( + @NotifInflation notifInflationExecutor: Executor, + @Background bgCoroutineDispatcher: CoroutineDispatcher, + ): CoroutineDispatcher { + if (com.android.systemui.Flags.useNotifInflationThreadForFooter()) { + return notifInflationExecutor.asCoroutineDispatcher() + } else { + return bgCoroutineDispatcher + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt new file mode 100644 index 000000000000..4d6eb4d8f391 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt @@ -0,0 +1,35 @@ +/* + * 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.util.settings.repository + +import android.provider.Settings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher + +/** Repository observing values of a [Settings.Secure] for the specified user. */ +@SysUISingleton +class SecureSettingsForUserRepository +@Inject +constructor( + secureSettings: SecureSettings, + @Background backgroundDispatcher: CoroutineDispatcher, + @Background backgroundContext: CoroutineContext, +) : SettingsForUserRepository(secureSettings, backgroundDispatcher, backgroundContext) diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt new file mode 100644 index 000000000000..94b3fd244a92 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.settings.repository + +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.util.settings.UserSettingsProxy +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** + * Repository observing values of a [UserSettingsProxy] for the specified user. This repository + * should be used for any system that tracks the desired user internally (e.g. the Quick Settings + * tiles system). In other cases, use a [UserAwareSettingsRepository] instead. + */ +abstract class SettingsForUserRepository( + private val userSettings: UserSettingsProxy, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundContext: CoroutineContext, +) { + fun boolSettingForUser( + userId: Int, + name: String, + defaultValue: Boolean = false, + ): Flow<Boolean> = + settingObserver(name, userId) { userSettings.getBoolForUser(name, defaultValue, userId) } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> { + return userSettings + .observerFlow(userId, name) + .onStart { emit(Unit) } + .map { settingsReader.invoke() } + } + + suspend fun setBoolForUser(userId: Int, name: String, value: Boolean) { + withContext(backgroundContext) { userSettings.putBoolForUser(name, value, userId) } + } + + suspend fun getBoolForUser(userId: Int, name: String, defaultValue: Boolean = false): Boolean { + return withContext(backgroundContext) { + userSettings.getBoolForUser(name, defaultValue, userId) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt index 73329b467c04..a8068cda685b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt @@ -33,7 +33,8 @@ import kotlinx.coroutines.withContext /** * Repository for observing values of a [UserSettingsProxy], for the currently active user. That * means that when the user is switched and the new user has a different value, the flow will emit - * the new value. + * the new value. For any system that tracks the desired user internally (e.g. the Quick Settings + * tiles system), use a [SettingsForUserRepository] instead. */ // TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal. abstract class UserAwareSettingsRepository( diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt index ec74f4f47bc9..300a7e070b6c 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.wallpapers.data.repository import android.app.WallpaperInfo +import android.graphics.PointF +import android.graphics.RectF import android.view.View import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject @@ -37,4 +39,8 @@ class NoopWallpaperRepository @Inject constructor() : WallpaperRepository { override val wallpaperSupportsAmbientMode = flowOf(false) override var rootView: View? = null override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {} + + override fun sendTapCommand(tapPosition: PointF) {} } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt index 2c3491b06a90..974468c16578 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt @@ -33,7 +33,8 @@ interface WallpaperFocalAreaRepository { val wallpaperFocalAreaBounds: StateFlow<RectF> - val wallpaperFocalAreaTapPosition: StateFlow<PointF> + /** It will be true when wallpaper requires focal area info. */ + val hasFocalArea: StateFlow<Boolean> /** top of notifications without bcsmartspace in small clock settings */ val notificationDefaultTop: StateFlow<Float> @@ -51,7 +52,9 @@ interface WallpaperFocalAreaRepository { } @SysUISingleton -class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAreaRepository { +class WallpaperFocalAreaRepositoryImpl +@Inject +constructor(val wallpaperRepository: WallpaperRepository) : WallpaperFocalAreaRepository { private val _shortcutAbsoluteTop = MutableStateFlow(0F) override val shortcutAbsoluteTop = _shortcutAbsoluteTop.asStateFlow() @@ -63,13 +66,11 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre override val wallpaperFocalAreaBounds: StateFlow<RectF> = _wallpaperFocalAreaBounds.asStateFlow() - private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F)) - override val wallpaperFocalAreaTapPosition: StateFlow<PointF> = - _wallpaperFocalAreaTapPosition.asStateFlow() - private val _notificationDefaultTop = MutableStateFlow(0F) override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow() + override val hasFocalArea = wallpaperRepository.shouldSendFocalArea + override fun setShortcutAbsoluteTop(top: Float) { _shortcutAbsoluteTop.value = top } @@ -84,9 +85,10 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre override fun setWallpaperFocalAreaBounds(bounds: RectF) { _wallpaperFocalAreaBounds.value = bounds + wallpaperRepository.sendLockScreenLayoutChangeCommand(bounds) } - override fun setTapPosition(point: PointF) { - _wallpaperFocalAreaTapPosition.value = point + override fun setTapPosition(tapPosition: PointF) { + wallpaperRepository.sendTapCommand(tapPosition) } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index a55f76b333d9..b07342c4c76d 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -21,22 +21,18 @@ import android.app.WallpaperManager import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.graphics.PointF +import android.graphics.RectF import android.os.Bundle import android.os.UserHandle import android.provider.Settings +import android.util.Log import android.view.View -import androidx.annotation.VisibleForTesting -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.Edge -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.res.R as SysUIR -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.shared.Flags.extendedWallpaperEffects import com.android.systemui.user.data.model.SelectedUserModel @@ -48,7 +44,6 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -76,6 +71,10 @@ interface WallpaperRepository { /** some wallpapers require bounds to be sent from keyguard */ val shouldSendFocalArea: StateFlow<Boolean> + + fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) + + fun sendTapCommand(tapPosition: PointF) } @SysUISingleton @@ -86,10 +85,8 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, userRepository: UserRepository, - wallpaperFocalAreaRepository: WallpaperFocalAreaRepository, private val wallpaperManager: WallpaperManager, private val context: Context, - keyguardTransitionInteractor: KeyguardTransitionInteractor, private val secureSettings: SecureSettings, ) : WallpaperRepository { private val wallpaperChanged: Flow<Unit> = @@ -109,9 +106,6 @@ constructor( // Only update the wallpaper status once the user selection has finished. .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE } - @VisibleForTesting var sendLockscreenLayoutJob: Job? = null - @VisibleForTesting var sendTapInShapeEffectsJob: Job? = null - override val wallpaperInfo: StateFlow<WallpaperInfo?> = if (!wallpaperManager.isWallpaperSupported) { MutableStateFlow(null).asStateFlow() @@ -143,77 +137,45 @@ constructor( override var rootView: View? = null + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) { + if (DEBUG) { + Log.d(TAG, "sendLockScreenLayoutChangeCommand $wallpaperFocalAreaBounds") + } + wallpaperManager.sendWallpaperCommand( + /* windowToken = */ rootView?.windowToken, + /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_LAYOUT_CHANGED, + /* x = */ 0, + /* y = */ 0, + /* z = */ 0, + /* extras = */ Bundle().apply { + putFloat("wallpaperFocalAreaLeft", wallpaperFocalAreaBounds.left) + putFloat("wallpaperFocalAreaRight", wallpaperFocalAreaBounds.right) + putFloat("wallpaperFocalAreaTop", wallpaperFocalAreaBounds.top) + putFloat("wallpaperFocalAreaBottom", wallpaperFocalAreaBounds.bottom) + }, + ) + } + + override fun sendTapCommand(tapPosition: PointF) { + if (DEBUG) { + Log.d(TAG, "sendTapCommand $tapPosition") + } + + wallpaperManager.sendWallpaperCommand( + /* windowToken = */ rootView?.windowToken, + /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP, + /* x = */ tapPosition.x.toInt(), + /* y = */ tapPosition.y.toInt(), + /* z = */ 0, + /* extras = */ Bundle(), + ) + } + override val shouldSendFocalArea = wallpaperInfo .map { val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget - if (shouldSendNotificationLayout) { - sendLockscreenLayoutJob = - scope.launch { - combine( - wallpaperFocalAreaRepository.wallpaperFocalAreaBounds, - keyguardTransitionInteractor - .transition( - edge = Edge.create(to = Scenes.Lockscreen), - edgeWithoutSceneContainer = - Edge.create(to = KeyguardState.LOCKSCREEN), - ) - .filter { transitionStep -> - transitionStep.transitionState == - TransitionState.STARTED - }, - ::Pair, - ) - .map { (bounds, _) -> bounds } - .collect { wallpaperFocalAreaBounds -> - wallpaperManager.sendWallpaperCommand( - /* windowToken = */ rootView?.windowToken, - /* action = */ WallpaperManager - .COMMAND_LOCKSCREEN_LAYOUT_CHANGED, - /* x = */ 0, - /* y = */ 0, - /* z = */ 0, - /* extras = */ Bundle().apply { - putFloat( - "wallpaperFocalAreaLeft", - wallpaperFocalAreaBounds.left, - ) - putFloat( - "wallpaperFocalAreaRight", - wallpaperFocalAreaBounds.right, - ) - putFloat( - "wallpaperFocalAreaTop", - wallpaperFocalAreaBounds.top, - ) - putFloat( - "wallpaperFocalAreaBottom", - wallpaperFocalAreaBounds.bottom, - ) - }, - ) - } - } - - sendTapInShapeEffectsJob = - scope.launch { - wallpaperFocalAreaRepository.wallpaperFocalAreaTapPosition.collect { - wallpaperFocalAreaTapPosition -> - wallpaperManager.sendWallpaperCommand( - /* windowToken = */ rootView?.windowToken, - /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP, - /* x = */ wallpaperFocalAreaTapPosition.x.toInt(), - /* y = */ wallpaperFocalAreaTapPosition.y.toInt(), - /* z = */ 0, - /* extras = */ null, - ) - } - } - } else { - sendLockscreenLayoutJob?.cancel() - sendTapInShapeEffectsJob?.cancel() - } shouldSendNotificationLayout } .stateIn( @@ -227,4 +189,9 @@ constructor( wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id) } } + + companion object { + private val TAG = WallpaperRepositoryImpl::class.simpleName + private val DEBUG = true + } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt index 9b0f8280cab2..09c6cdf0ce22 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -24,31 +24,25 @@ import android.util.Log import android.util.TypedValue import com.android.app.animation.MathUtils import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.wallpapers.data.repository.WallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.WallpaperRepository import javax.inject.Inject import kotlin.math.min -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter @SysUISingleton class WallpaperFocalAreaInteractor @Inject constructor( - @Application private val applicationScope: CoroutineScope, private val context: Context, private val wallpaperFocalAreaRepository: WallpaperFocalAreaRepository, shadeRepository: ShadeRepository, - activeNotificationsInteractor: ActiveNotificationsInteractor, - val wallpaperRepository: WallpaperRepository, ) { - val hasFocalArea = wallpaperRepository.shouldSendFocalArea + val hasFocalArea = wallpaperFocalAreaRepository.hasFocalArea val wallpaperFocalAreaBounds: Flow<RectF> = combine( @@ -126,6 +120,8 @@ constructor( val bottom = scaledBounds.bottom - scaledBottomMargin RectF(left, top, right, bottom).also { Log.d(TAG, "Focal area changes to $it") } } + // Make sure a valid rec + .filter { it.width() >= 0 && it.height() >= 0 } .distinctUntilChanged() fun setFocalAreaBounds(bounds: RectF) { diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt index 70a97d473c49..4cd49d03ad36 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt @@ -17,15 +17,41 @@ package com.android.systemui.wallpapers.ui.viewmodel import android.graphics.RectF +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map class WallpaperFocalAreaViewModel @Inject -constructor(private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor) { +constructor( + private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, + val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) { val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea - val wallpaperFocalAreaBounds = wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds + val wallpaperFocalAreaBounds = + combine( + wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds, + keyguardTransitionInteractor + .transition( + edge = Edge.create(to = Scenes.Lockscreen), + edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), + ) + .filter { transitionStep -> + // Should not filter by TransitionState.STARTED, it may race with + // wakingup command, causing layout change command not be received. + transitionStep.transitionState == TransitionState.FINISHED + }, + ::Pair, + ) + .map { (bounds, _) -> bounds } fun setFocalAreaBounds(bounds: RectF) { wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt index 8635bb0e8ab2..8635bb0e8ab2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 6d75c4ca3a38..6d75c4ca3a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index b6c63479990e..b6c63479990e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt index b4200b6850c8..b4200b6850c8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 296a0fc2eb40..296a0fc2eb40 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt index ae6b337a3fa0..ae6b337a3fa0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index 6cb6fed978b8..6cb6fed978b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt index 9c7f01495b58..9c7f01495b58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt index b0e93fbecbb9..b0e93fbecbb9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index a2bd5ec28f08..aaf5559290df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -66,6 +66,7 @@ import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.setSceneTransition import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -203,6 +204,7 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase( mediaCarouselViewModel = kosmos.mediaCarouselViewModel, mediaViewControllerFactory = mediaViewControllerFactory, deviceEntryInteractor = kosmos.deviceEntryInteractor, + mediaControlChipInteractor = kosmos.mediaControlChipInteractor, ) verify(configurationController).addCallback(capture(configListener)) verify(visualStabilityProvider) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 23282b16d8a8..205ccea657df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -84,7 +84,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this); // Mock - private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class); + private MediaOutputAdapterBase mMediaOutputBaseAdapter = mock(MediaOutputAdapterBase.class); private MediaController mMediaController = mock(MediaController.class); private PlaybackState mPlaybackState = mock(PlaybackState.class); private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class); @@ -219,7 +219,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void refresh_withIconCompat_iconIsVisible() { mIconCompat = IconCompat.createWithBitmap( Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)); - when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController); mMediaOutputBaseDialogImpl.refresh(); final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt index ab78029684d4..ab78029684d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt index 1a4749c3196c..1a4749c3196c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java index 49cbb5a924f1..49cbb5a924f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt index 89a3d5b5cf0b..89a3d5b5cf0b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index 79e78c9532c6..79e78c9532c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt index 8418598c256b..8418598c256b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index d67ce303c451..d67ce303c451 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt index fcbf0fe9a37a..fcbf0fe9a37a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt index 04319f05f6f9..04319f05f6f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 281ce16b539f..19d1224a9bf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -28,6 +28,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -46,6 +48,7 @@ import android.os.Bundle; 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.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; @@ -59,9 +62,12 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -83,6 +89,9 @@ public class NotificationEntryTest extends SysuiTestCase { private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class); private final FakeSystemClock mClock = new FakeSystemClock(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { Notification.Builder n = new Notification.Builder(mContext, "") @@ -444,6 +453,145 @@ public class NotificationEntryTest extends SysuiTestCase { // no crash, good } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getParent_adapter() { + GroupEntry ge = new GroupEntryBuilder() + .build(); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(ge) + .build(); + + assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void isTopLevelEntry_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + + assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getKey_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + + assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getRow_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupSummary() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isNull(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupChild() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry parent = new NotificationEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + GroupEntryBuilder groupEntry = new GroupEntryBuilder() + .setSummary(parent); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(groupEntry.build()) + .build(); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter()); + } private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index f1edb417a314..f1edb417a314 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt index 99dcd6c9a798..99dcd6c9a798 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 77b116e2e465..a6722c5f4c22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES; + import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE; @@ -29,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -189,6 +192,54 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) + public void setSensitive_doesNothingIfCalledAgain() throws Exception { + ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + measureAndLayout(row); + + // GIVEN a mocked public layout + NotificationContentView mockPublicLayout = mock(NotificationContentView.class); + row.setPublicLayout(mockPublicLayout); + + // GIVEN a sensitive notification row that's currently redacted + row.setHideSensitiveForIntrinsicHeight(true); + row.setSensitive(true, true); + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + verify(mockPublicLayout).requestSelectLayout(eq(true)); + clearInvocations(mockPublicLayout); + + // WHEN the row is set to the same sensitive settings + row.setSensitive(true, true); + + // VERIFY that the layout is not updated again + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + verify(mockPublicLayout, never()).requestSelectLayout(anyBoolean()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) + public void testSetSensitiveOnNotifRowUpdatesLayout() throws Exception { + // GIVEN a sensitive notification row that's currently redacted + ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + measureAndLayout(row); + row.setHideSensitiveForIntrinsicHeight(true); + row.setSensitive(true, true); + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + + // GIVEN a mocked private layout + NotificationContentView mockPrivateLayout = mock(NotificationContentView.class); + row.setPrivateLayout(mockPrivateLayout); + + // WHEN the row is set to no longer be sensitive + row.setSensitive(false, true); + + // VERIFY that the layout is updated + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout()); + verify(mockPrivateLayout).requestSelectLayout(eq(true)); + } + + @Test + @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception { // GIVEN a sensitive notification row that's currently redacted ExpandableNotificationRow row = mNotificationTestHelper.createRow(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 699e8c30afde..47238fedee4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -23,6 +23,7 @@ import android.service.notification.StatusBarNotification import android.testing.TestableLooper import android.testing.ViewUtils import android.view.NotificationHeaderView +import android.view.NotificationTopLineView import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -37,6 +38,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.FeedbackIcon import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -82,8 +84,21 @@ class NotificationContentViewTest : SysuiTestCase() { val mockEntry = createMockNotificationEntry() row = spy( - ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { - entry = mockEntry + when (NotificationBundleUi.isEnabled) { + true -> { + ExpandableNotificationRow( + mContext, + /* attrs= */ null, + UserHandle.CURRENT + ).apply { + entry = mockEntry + } + } + false -> { + ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { + entry = mockEntry + } + } } ) ViewUtils.attachView(fakeParent) @@ -270,7 +285,7 @@ class NotificationContentViewTest : SysuiTestCase() { val icon = FeedbackIcon( R.drawable.ic_feedback_alerted, - R.string.notification_feedback_indicator_alerted + R.string.notification_feedback_indicator_alerted, ) view.setFeedbackIcon(icon) @@ -291,10 +306,7 @@ class NotificationContentViewTest : SysuiTestCase() { val mockHeadsUpEB = mock<NotificationExpandButton>() val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB) - val view = - createContentView( - isSystemExpanded = false, - ) + val view = createContentView(isSystemExpanded = false) // Update all 3 child forms view.apply { @@ -319,12 +331,14 @@ class NotificationContentViewTest : SysuiTestCase() { private fun createMockNotificationHeaderView( height: Int, - mockExpandedEB: NotificationExpandButton + mockExpandedEB: NotificationExpandButton, ) = spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height }) .apply { whenever(this.animate()).thenReturn(mock()) whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) + whenever(this.findViewById<View>(R.id.notification_top_line)) + .thenReturn(mock<NotificationTopLineView>()) } @Test @@ -344,7 +358,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(true) @@ -373,7 +387,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(false) @@ -635,7 +649,7 @@ class NotificationContentViewTest : SysuiTestCase() { contractedView: View = createViewWithHeight(contractedHeight), expandedView: View = createViewWithHeight(expandedHeight), headsUpView: View = createViewWithHeight(contractedHeight), - row: ExpandableNotificationRow = this.row + row: ExpandableNotificationRow = this.row, ): NotificationContentView { val height = if (isSystemExpanded) expandedHeight else contractedHeight doReturn(height).whenever(row).intrinsicHeight @@ -647,7 +661,7 @@ class NotificationContentViewTest : SysuiTestCase() { setHeights( /* smallHeight= */ contractedHeight, /* headsUpMaxHeight= */ contractedHeight, - /* maxHeight= */ expandedHeight + /* maxHeight= */ expandedHeight, ) contractedChild = contractedView expandedChild = expandedView diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt index 86689cb88569..86689cb88569 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index 31f8590c0378..31f8590c0378 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt 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 14a1233045bb..10886760b521 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 @@ -63,6 +63,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; +import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository; @@ -118,10 +119,8 @@ public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private final FakeConfigurationController mConfigurationController = - new FakeConfigurationController(); - private final LargeScreenShadeInterpolator - mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + private FakeConfigurationController mConfigurationController; + private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator; private final TestScope mTestScope = mKosmos.getTestScope(); private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -137,6 +136,7 @@ public class ScrimControllerTest extends SysuiTestCase { private boolean mAlwaysOnEnabled; private TestableLooper mLooper; private Context mContext; + @Mock private DozeParameters mDozeParameters; @Mock private LightBarController mLightBarController; @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory; @@ -149,12 +149,11 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; - private final KeyguardTransitionInteractor mKeyguardTransitionInteractor = - mKosmos.getKeyguardTransitionInteractor(); - private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository = - mKosmos.getKeyguardTransitionRepository(); @Mock private KeyguardInteractor mKeyguardInteractor; + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private FakeKeyguardTransitionRepository mKeyguardTransitionRepository; + // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @@ -238,6 +237,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mContext.getColor(com.android.internal.R.color.materialColorSurface)) .thenAnswer(invocation -> mSurfaceColor); + mConfigurationController = new FakeConfigurationController(); + mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + mScrimBehind = spy(new ScrimView(mContext)); mScrimInFront = new ScrimView(mContext); mNotificationsScrim = new ScrimView(mContext); @@ -270,6 +272,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); + mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository(); + mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor(); + mScrimController = new ScrimController( mLightBarController, mDozeParameters, @@ -322,6 +327,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -337,6 +343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -373,6 +380,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -391,6 +399,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToOff() { mScrimController.legacyTransitionTo(ScrimState.OFF); finishAnimationsImmediately(); @@ -406,6 +415,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withRegularWallpaper() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -421,6 +431,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withFrontAlphaUpdates() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -465,6 +476,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -506,6 +518,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToPulsing_withFrontAlphaUpdates() { // Pre-condition // Need to go to AoD first because PULSING doesn't change @@ -551,6 +564,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer() { mScrimController.legacyTransitionTo(BOUNCER); finishAnimationsImmediately(); @@ -571,6 +585,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void lockscreenToHubTransition_setsBehindScrimAlpha() { // Start on lockscreen. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -617,6 +632,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void hubToLockscreenTransition_setsViewAlpha() { // Start on glanceable hub. mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -663,6 +679,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHub() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -677,6 +694,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -706,6 +724,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -734,6 +753,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHubOverDream() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -748,6 +768,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -777,6 +798,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -805,6 +827,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() { assertEquals(BOUNCER.getBehindTint(), 0x112233); mSurfaceColor = 0x223344; @@ -813,6 +836,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -825,6 +849,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -845,6 +870,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -867,6 +893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(BOUNCER); @@ -889,6 +916,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToBouncer() { mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED); finishAnimationsImmediately(); @@ -902,6 +930,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_clippedQs() { mScrimController.setClipsQsScrim(true); mScrimController.setRawPanelExpansionFraction(0f); @@ -960,6 +989,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() { mScrimController.setClipsQsScrim(false); mScrimController.setRawPanelExpansionFraction(0f); @@ -999,6 +1029,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimStateCallback() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1014,6 +1045,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansion() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1036,6 +1068,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion() { reset(mScrimBehind); mScrimController.setQsPosition(1f, 999 /* value doesn't matter */); @@ -1048,6 +1081,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1061,6 +1095,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_half_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1074,6 +1109,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansionAffectsAlpha() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1096,6 +1132,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromOff() { // Simulate unlock with fingerprint without AOD mScrimController.legacyTransitionTo(ScrimState.OFF); @@ -1118,6 +1155,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1140,6 +1178,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlanksBeforeLeavingAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1163,6 +1202,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlankCallbackWhenUnlockingFromPulse() { boolean[] blanked = {false}; // Simulate unlock with fingerprint @@ -1181,6 +1221,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void blankingNotRequired_leavingAoD() { // GIVEN display does NOT need blanking when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); @@ -1236,6 +1277,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallback() { int[] callOrder = {0, 0, 0}; int[] currentCall = {0}; @@ -1262,12 +1304,14 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallbacksWithoutAmbientDisplay() { mAlwaysOnEnabled = false; testScrimCallback(); } @Test + @DisableSceneContainer public void testScrimCallbackCancelled() { boolean[] cancelledCalled = {false}; mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() { @@ -1281,6 +1325,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHoldsWakeLock_whenAOD() { mScrimController.legacyTransitionTo(ScrimState.AOD); verify(mWakeLock).acquire(anyString()); @@ -1290,6 +1335,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesNotHoldWakeLock_whenUnlocking() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1297,6 +1343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCallbackInvokedOnSameStateTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1306,6 +1353,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testConservesExpansionOpacityAfterTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1323,6 +1371,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCancelsOldAnimationBeforeBlanking() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -1335,6 +1384,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsAreNotFocusable() { assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable()); assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable()); @@ -1343,6 +1393,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testEatsTouchEvent() { HashSet<ScrimState> eatsTouches = new HashSet<>(Collections.singletonList(ScrimState.AOD)); @@ -1359,6 +1410,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testAnimatesTransitionToAod() { when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); ScrimState.AOD.prepare(ScrimState.KEYGUARD); @@ -1373,6 +1425,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testIsLowPowerMode() { HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList( ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING)); @@ -1390,6 +1443,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsOpaque_whenShadeFullyExpanded() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(1); @@ -1404,6 +1458,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1419,6 +1474,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesntAnimate_whenUnlocking() { // LightRevealScrim will animate the transition, we should only hide the keyguard scrims. ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD); @@ -1439,6 +1495,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1454,6 +1511,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisibleOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); mScrimController.setQsPosition(0.25f, 300); @@ -1465,6 +1523,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimTransparent_whenOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); // even if shade is not pulled down, panel has expansion of 1 on the lockscreen @@ -1477,6 +1536,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() { mScrimController.setRawPanelExpansionFraction(1); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -1488,6 +1548,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); // clipping doesn't change tested logic but allows to assert scrims more in line with @@ -1504,6 +1565,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); @@ -1520,6 +1582,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1535,6 +1598,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1554,6 +1618,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); mScrimController.setClipsQsScrim(true); @@ -1574,6 +1639,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(true); @@ -1594,6 +1660,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(false); @@ -1605,6 +1672,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationTransparency_followsTransitionToFullShade() { mScrimController.setClipsQsScrim(true); @@ -1646,6 +1714,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationTransparency_followsNotificationScrimProgress() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); @@ -1662,6 +1731,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1697,6 +1767,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() { int overScrollAmount = 10; @@ -1706,6 +1777,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() { int overScrollAmount = 10; @@ -1715,6 +1787,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() { int overScrollAmount = 10; @@ -1724,6 +1797,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopGetsPassedToKeyguard() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -1734,6 +1808,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1744,6 +1819,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -1763,6 +1839,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void keyguardGoingAwayUpdateScrims() { when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); mScrimController.updateScrims(); @@ -1772,6 +1849,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Test + @DisableSceneContainer public void setUnOccludingAnimationKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -1786,6 +1864,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHidesScrimFlickerInActivity() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1804,6 +1883,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1813,6 +1893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void aodStateSetsFrontScrimToNotBlend() { mScrimController.legacyTransitionTo(ScrimState.AOD); assertFalse("Front scrim should not blend with main color", @@ -1820,6 +1901,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void applyState_unlocked_bouncerShowing() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setBouncerHiddenFraction(0.99f); @@ -1829,6 +1911,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void ignoreTransitionRequestWhileKeyguardTransitionRunning() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.mBouncerToGoneTransition.accept( @@ -1841,6 +1924,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); mScrimController.mBouncerToGoneTransition.accept( @@ -1851,6 +1935,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsLightBarController() { reset(mLightBarController); mScrimController.mBouncerToGoneTransition.accept( @@ -1862,6 +1947,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoNotAnimateChangeIfOccludeAnimationPlaying() { mScrimController.setOccludeAnimationPlaying(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1870,6 +1956,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); @@ -1942,9 +2029,9 @@ public class ScrimControllerTest extends SysuiTestCase { // Check combined scrim visibility. final int visibility; - if (scrimToAlpha.values().contains(OPAQUE)) { + if (scrimToAlpha.containsValue(OPAQUE)) { visibility = OPAQUE; - } else if (scrimToAlpha.values().contains(SEMI_TRANSPARENT)) { + } else if (scrimToAlpha.containsValue(SEMI_TRANSPARENT)) { visibility = SEMI_TRANSPARENT; } else { visibility = TRANSPARENT; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index dde6e2ee1866..dde6e2ee1866 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt index a230f0630d6e..a230f0630d6e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt index 1135a5f86952..1135a5f86952 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt index a1122c3cbcd2..a1122c3cbcd2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt index c7b685fba455..c7b685fba455 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt index b93c161a7039..b93c161a7039 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt index 32c598612aa6..32c598612aa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt index 25ceea951d3c..25ceea951d3c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt index 9440280649dd..9440280649dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt index b3f2113f86ec..b3f2113f86ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java index c896fc0bfb8a..c896fc0bfb8a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt index b4fbaad6ab37..b4fbaad6ab37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt index 60c0f342b874..f9917ac680e0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -44,6 +44,8 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : var pendingOverlays: Set<OverlayKey>? = null private set + var freezeAndAnimateToCurrentStateCallCount = 0 + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { if (_isPaused) { _pendingScene = toScene @@ -85,6 +87,10 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : hideOverlay(overlay) } + override fun freezeAndAnimateToCurrentState() { + freezeAndAnimateToCurrentStateCallCount++ + } + /** * Pauses scene and overlay changes. * diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 20e4523fda0f..55e35f2b2703 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -18,9 +18,11 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory @@ -31,6 +33,8 @@ val Kosmos.notificationsShadeOverlayContentViewModel: notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, + disableFlagsInteractor = disableFlagsInteractor, + mediaCarouselInteractor = mediaCarouselInteractor, activeNotificationsInteractor = activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index 878c2deb43b2..d8e0cfe4fbf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.util.time.fakeSystemClock val Kosmos.notifChipsViewModel: NotifChipsViewModel by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.notifChipsViewModel: NotifChipsViewModel by applicationCoroutineScope, statusBarNotificationChipsInteractor, headsUpNotificationInteractor, + fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt new file mode 100644 index 000000000000..59f5ecd2563f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.content.applicationContext +import android.graphics.drawable.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.icon.IconPack +import com.android.systemui.statusbar.notification.promoted.setPromotedContent +import org.mockito.kotlin.mock + +fun Kosmos.setIconPackWithMockIconViews(entry: NotificationEntry) { + entry.icons = + IconPack.buildPack( + /* statusBarIcon = */ mock(), + /* statusBarChipIcon = */ mock(), + /* shelfIcon = */ mock(), + /* aodIcon = */ mock(), + /* source = */ null, + ) +} + +fun Kosmos.buildOngoingCallEntry( + promoted: Boolean = false, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + buildNotificationEntry( + tag = "call", + promoted = promoted, + style = makeOngoingCallStyle(), + block = block, + ) + +fun Kosmos.buildPromotedOngoingEntry( + block: NotificationEntryBuilder.() -> Unit = {} +): NotificationEntry = + buildNotificationEntry(tag = "ron", promoted = true, style = null, block = block) + +fun Kosmos.buildNotificationEntry( + tag: String? = null, + promoted: Boolean = false, + style: Notification.Style? = null, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + NotificationEntryBuilder() + .apply { + setTag(tag) + setFlag(applicationContext, Notification.FLAG_PROMOTED_ONGOING, promoted) + modifyNotification(applicationContext) + .setSmallIcon(Icon.createWithContentUri("content://null")) + .setStyle(style) + } + .apply(block) + .build() + .also { + setIconPackWithMockIconViews(it) + if (promoted) setPromotedContent(it) + } + +private fun Kosmos.makeOngoingCallStyle(): Notification.CallStyle { + val pendingIntent = + PendingIntent.getBroadcast( + applicationContext, + 0, + Intent("action"), + PendingIntent.FLAG_IMMUTABLE, + ) + val person = Person.Builder().setName("person").build() + return Notification.CallStyle.forOngoingCall(person, pendingIntent) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt index a48b27015c02..fa3702cea5ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock -var Kosmos.notifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } +var Kosmos.notifPipeline by Kosmos.Fixture { mockNotifPipeline } +var Kosmos.mockNotifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index 63521de096c9..e55cd0dc16f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.promoted +import android.app.Notification import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform var Kosmos.promotedNotificationContentExtractor by @@ -28,3 +31,14 @@ var Kosmos.promotedNotificationContentExtractor by promotedNotificationLogger, ) } + +fun Kosmos.setPromotedContent(entry: NotificationEntry) { + val extractedContent = + promotedNotificationContentExtractor.extractContent( + entry, + Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), + RowImageInflater.newInstance(null).useForContentModel(), + ) + entry.promotedNotificationContentModel = + requireNotNull(extractedContent) { "extractContent returned null" } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt index df1c82278bc2..fcd484353011 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt @@ -18,12 +18,11 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { AODPromotedNotificationInteractor( - activeNotificationsInteractor = activeNotificationsInteractor, + promotedNotificationsInteractor = promotedNotificationsInteractor, dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt new file mode 100644 index 000000000000..093ec10e2642 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +val Kosmos.promotedNotificationsInteractor by + Kosmos.Fixture { + PromotedNotificationsInteractor( + activeNotificationsInteractor = activeNotificationsInteractor, + callChipInteractor = callChipInteractor, + notifChipsInteractor = statusBarNotificationChipsInteractor, + backgroundDispatcher = testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index e445a73b06d0..2543ca95eb3b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -41,6 +41,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.shared.system.DevicePolicyManagerWrapper import com.android.systemui.shared.system.PackageManagerWrapper @@ -346,10 +347,14 @@ class ExpandableNotificationRowBuilder( // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be // set, but we do not want to override an existing value that is needed by a specific test. + val userTracker = Mockito.mock(UserTracker::class.java, STUB_ONLY) + whenever(userTracker.userHandle).thenReturn(context.user) + val rowInflaterTask = RowInflaterTask( mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY), + userTracker ) val row = rowInflaterTask.inflateSynchronously(context, null, entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index bc1363ac3d5c..970b87cd368a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -33,7 +33,7 @@ import java.util.Optional val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( - backgroundDispatcher = testDispatcher, + inflationDispatcher = testDispatcher, hiderTracker = displaySwitchNotificationsHiderTracker, configuration = configurationState, falsingManager = falsingManager, 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 047bd13f0c27..7a2b7c24252b 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 @@ -29,7 +29,6 @@ import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel @@ -82,7 +81,6 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel, - dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt new file mode 100644 index 000000000000..81f71e9f7b2f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.settings.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.settings.repository.SecureSettingsForUserRepository + +val Kosmos.secureSettingsForUserRepository by + Kosmos.Fixture { + SecureSettingsForUserRepository(fakeSettings, testDispatcher, backgroundCoroutineContext) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt index aeff86ed89bb..24d2f1f0d901 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt @@ -34,12 +34,15 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { _wallpaperFocalAreaBounds.asStateFlow() private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F)) - override val wallpaperFocalAreaTapPosition: StateFlow<PointF> = + val wallpaperFocalAreaTapPosition: StateFlow<PointF> = _wallpaperFocalAreaTapPosition.asStateFlow() private val _notificationDefaultTop = MutableStateFlow(0F) override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow() + private val _hasFocalArea = MutableStateFlow(false) + override val hasFocalArea: StateFlow<Boolean> = _hasFocalArea.asStateFlow() + override fun setShortcutAbsoluteTop(top: Float) { _shortcutAbsoluteTop.value = top } @@ -56,7 +59,7 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { _wallpaperFocalAreaBounds.value = bounds } - override fun setTapPosition(point: PointF) { - _wallpaperFocalAreaTapPosition.value = point + override fun setTapPosition(tapPosition: PointF) { + _wallpaperFocalAreaTapPosition.value = tapPosition } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt index 8689e04e62dd..66bb803c182d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.wallpapers.data.repository import android.app.WallpaperInfo +import android.graphics.PointF +import android.graphics.RectF import android.view.View import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -34,9 +36,9 @@ class FakeWallpaperRepository : WallpaperRepository { private val _shouldSendFocalArea = MutableStateFlow(false) override val shouldSendFocalArea: StateFlow<Boolean> = _shouldSendFocalArea.asStateFlow() - fun setShouldSendFocalArea(shouldSendFocalArea: Boolean) { - _shouldSendFocalArea.value = shouldSendFocalArea - } + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {} + + override fun sendTapCommand(tapPosition: PointF) {} fun setWallpaperInfo(wallpaperInfo: WallpaperInfo?) { _wallpaperInfo.value = wallpaperInfo diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt index 7ebec6c3a7b9..1761503b2cc9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.wallpapers.data.repository import android.content.applicationContext import com.android.app.wallpaperManager import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -34,8 +33,6 @@ val Kosmos.wallpaperRepository by Fixture { bgDispatcher = testDispatcher, broadcastDispatcher = broadcastDispatcher, userRepository = userRepository, - keyguardTransitionInteractor = keyguardTransitionInteractor, - wallpaperFocalAreaRepository = wallpaperFocalAreaRepository, wallpaperManager = wallpaperManager, secureSettings = fakeSettings, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt index 88eb5511160b..eaf55a72be93 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -18,20 +18,14 @@ package com.android.systemui.wallpapers.domain.interactor import android.content.applicationContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.wallpaperRepository -val Kosmos.wallpaperFocalAreaInteractor by +var Kosmos.wallpaperFocalAreaInteractor by Kosmos.Fixture { WallpaperFocalAreaInteractor( - applicationScope = applicationCoroutineScope, context = applicationContext, wallpaperFocalAreaRepository = wallpaperFocalAreaRepository, shadeRepository = shadeRepository, - activeNotificationsInteractor = activeNotificationsInteractor, - wallpaperRepository = wallpaperRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt index 7e232c526732..4032503d04c1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt @@ -16,10 +16,14 @@ package com.android.systemui.wallpapers.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor var Kosmos.wallpaperFocalAreaViewModel by Kosmos.Fixture { - WallpaperFocalAreaViewModel(wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor) + WallpaperFocalAreaViewModel( + wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + ) } diff --git a/ravenwood/runtime-jni/ravenwood_initializer.cpp b/ravenwood/runtime-jni/ravenwood_initializer.cpp index 391c5d56b212..8a35ade649b2 100644 --- a/ravenwood/runtime-jni/ravenwood_initializer.cpp +++ b/ravenwood/runtime-jni/ravenwood_initializer.cpp @@ -26,6 +26,10 @@ #include <fcntl.h> #include <set> +#include <fstream> +#include <iostream> +#include <string> +#include <cstdlib> #include "jni_helper.h" @@ -182,17 +186,82 @@ static jboolean removeSystemProperty(JNIEnv* env, jclass, jstring javaKey) { } } +// Find the PPID of child_pid using /proc/N/stat. The 4th field is the PPID. +// Also returns child_pid's process name (2nd field). +static pid_t getppid_of(pid_t child_pid, std::string& out_process_name) { + if (child_pid < 0) { + return -1; + } + std::string stat_file = "/proc/" + std::to_string(child_pid) + "/stat"; + std::ifstream stat_stream(stat_file); + if (!stat_stream.is_open()) { + ALOGW("Unable to open '%s': %s", stat_file.c_str(), strerror(errno)); + return -1; + } + + std::string field; + int field_count = 0; + while (std::getline(stat_stream, field, ' ')) { + if (++field_count == 4) { + return atoi(field.c_str()); + } + if (field_count == 2) { + out_process_name = field; + } + } + ALOGW("Unexpected format in '%s'", stat_file.c_str()); + return -1; +} + +// Find atest's PID. Climb up the process tree, and find "atest-py3". +static pid_t find_atest_pid() { + auto ret = getpid(); // self (isolation runner process) + + while (ret != -1) { + std::string proc; + ret = getppid_of(ret, proc); + if (proc == "(atest-py3)") { + return ret; + } + } + + return ret; +} + +// If $RAVENWOOD_LOG_OUT is set, redirect stdout/err to this file. +// Originally it was added to allow to monitor log in realtime, with +// RAVENWOOD_LOG_OUT=$(tty) atest... +// +// As a special case, if $RAVENWOOD_LOG_OUT is set to "-", we try to find +// atest's process and send the output to its stdout. It's sort of hacky, but +// this allows shell redirection to work on Ravenwood output too, +// so e.g. `atest ... |tee atest.log` would work on Ravenwood's output. +// (which wouldn't work with `RAVENWOOD_LOG_OUT=$(tty)`). +// +// Otherwise -- if $RAVENWOOD_LOG_OUT isn't set -- atest/tradefed just writes +// the test's output to its own log file. static void maybeRedirectLog() { auto ravenwoodLogOut = getenv("RAVENWOOD_LOG_OUT"); - if (ravenwoodLogOut == NULL) { + if (ravenwoodLogOut == NULL || *ravenwoodLogOut == '\0') { return; } - ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to %s", ravenwoodLogOut); + std::string path; + if (strcmp("-", ravenwoodLogOut) == 0) { + pid_t ppid = find_atest_pid(); + if (ppid < 0) { + ALOGI("RAVENWOOD_LOG_OUT set to '-', but unable to find atest's PID"); + return; + } + path = std::format("/proc/{}/fd/1", ppid); + } else { + path = ravenwoodLogOut; + } + ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to '%s'", path.c_str()); // Redirect stdin / stdout to /dev/tty. - int ttyFd = open(ravenwoodLogOut, O_WRONLY | O_APPEND); + int ttyFd = open(path.c_str(), O_WRONLY | O_APPEND); if (ttyFd == -1) { - ALOGW("$RAVENWOOD_LOG_OUT is set to %s, but failed to open: %s ", ravenwoodLogOut, + ALOGW("$RAVENWOOD_LOG_OUT is set, but failed to open '%s': %s ", path.c_str(), strerror(errno)); return; } diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 961022b7231b..517279bd7527 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -54,15 +54,21 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ProcessMap; import com.android.internal.os.Clock; import com.android.internal.os.MonotonicClock; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import com.android.server.IoThread; import com.android.server.ServiceThread; import com.android.server.SystemServiceManager; import com.android.server.wm.WindowProcessController; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -1006,6 +1012,12 @@ public final class AppStartInfoTracker { throws IOException, WireTypeMismatchException, ClassNotFoundException { long token = proto.start(fieldId); String pkgName = ""; + + // Create objects for reuse. + ByteArrayInputStream byteArrayInputStream = null; + ObjectInputStream objectInputStream = null; + TypedXmlPullParser typedXmlPullParser = null; + for (int next = proto.nextField(); next != ProtoInputStream.NO_MORE_FIELDS; next = proto.nextField()) { @@ -1017,7 +1029,7 @@ public final class AppStartInfoTracker { AppStartInfoContainer container = new AppStartInfoContainer(mAppStartInfoHistoryListSize); int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS, - pkgName); + pkgName, byteArrayInputStream, objectInputStream, typedXmlPullParser); // If the isolated process flag is enabled and the uid is that of an isolated // process, then break early so that the container will not be added to mData. @@ -1052,6 +1064,12 @@ public final class AppStartInfoTracker { out = af.startWrite(); ProtoOutputStream proto = new ProtoOutputStream(out); proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now); + + // Create objects for reuse. + ByteArrayOutputStream byteArrayOutputStream = null; + ObjectOutputStream objectOutputStream = null; + TypedXmlSerializer typedXmlSerializer = null; + synchronized (mLock) { succeeded = forEachPackageLocked( (packageName, records) -> { @@ -1060,8 +1078,9 @@ public final class AppStartInfoTracker { int uidArraySize = records.size(); for (int j = 0; j < uidArraySize; j++) { try { - records.valueAt(j) - .writeToProto(proto, AppsStartInfoProto.Package.USERS); + records.valueAt(j).writeToProto(proto, + AppsStartInfoProto.Package.USERS, byteArrayOutputStream, + objectOutputStream, typedXmlSerializer); } catch (IOException e) { Slog.w(TAG, "Unable to write app start info into persistent" + "storage: " + e); @@ -1414,19 +1433,23 @@ public final class AppStartInfoTracker { } @GuardedBy("mLock") - void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException { + void writeToProto(ProtoOutputStream proto, long fieldId, + ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream, + TypedXmlSerializer typedXmlSerializer) throws IOException { long token = proto.start(fieldId); proto.write(AppsStartInfoProto.Package.User.UID, mUid); int size = mInfos.size(); for (int i = 0; i < size; i++) { - mInfos.get(i) - .writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO); + mInfos.get(i).writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO, + byteArrayOutputStream, objectOutputStream, typedXmlSerializer); } proto.write(AppsStartInfoProto.Package.User.MONITORING_ENABLED, mMonitoringModeEnabled); proto.end(token); } - int readFromProto(ProtoInputStream proto, long fieldId, String packageName) + int readFromProto(ProtoInputStream proto, long fieldId, String packageName, + ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream, + TypedXmlPullParser typedXmlPullParser) throws IOException, WireTypeMismatchException, ClassNotFoundException { long token = proto.start(fieldId); for (int next = proto.nextField(); @@ -1440,7 +1463,8 @@ public final class AppStartInfoTracker { // Create record with monotonic time 0 in case the persisted record does not // have a create time. ApplicationStartInfo info = new ApplicationStartInfo(0); - info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO); + info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO, + byteArrayInputStream, objectInputStream, typedXmlPullParser); info.setPackageName(packageName); mInfos.add(info); break; diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index 30c2a82296ca..604cb30294a9 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -418,7 +418,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { evictedEvents.addAll(mCache); mCache.clear(); } - mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents); + Message msg = mSqliteWriteHandler.obtainMessage( + WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents); + mSqliteWriteHandler.sendMessage(msg); } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 6d6e1fb6bfb3..ef80d59993e9 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -18,6 +18,7 @@ package com.android.server.audio; import static android.media.audio.Flags.scoManagedByAudio; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO; @@ -290,8 +291,8 @@ public class AudioDeviceBroker { } @GuardedBy("mDeviceStateLock") - /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { - mBtHelper.onSetBtScoActiveDevice(btDevice); + /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { + mBtHelper.onSetBtScoActiveDevice(btDevice, deviceSwitch); } /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) { @@ -941,6 +942,7 @@ public class AudioDeviceBroker { final @NonNull String mEventSource; final int mAudioSystemDevice; final int mMusicDevice; + final boolean mIsDeviceSwitch; BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state, int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { @@ -953,6 +955,8 @@ public class AudioDeviceBroker { mEventSource = d.mEventSource; mAudioSystemDevice = audioDevice; mMusicDevice = AudioSystem.DEVICE_NONE; + mIsDeviceSwitch = optimizeBtDeviceSwitch() + && d.mNewDevice != null && d.mPreviousDevice != null; } // constructor used by AudioDeviceBroker to search similar message @@ -966,6 +970,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } // constructor used by AudioDeviceInventory when config change failed @@ -980,6 +985,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } BtDeviceInfo(@NonNull BtDeviceInfo src, int state) { @@ -992,6 +998,7 @@ public class AudioDeviceBroker { mEventSource = src.mEventSource; mAudioSystemDevice = src.mAudioSystemDevice; mMusicDevice = src.mMusicDevice; + mIsDeviceSwitch = false; } // redefine equality op so we can match messages intended for this device @@ -1026,7 +1033,8 @@ public class AudioDeviceBroker { + " isLeOutput=" + mIsLeOutput + " eventSource=" + mEventSource + " audioSystemDevice=" + mAudioSystemDevice - + " musicDevice=" + mMusicDevice; + + " musicDevice=" + mMusicDevice + + " isDeviceSwitch=" + mIsDeviceSwitch; } } @@ -1196,6 +1204,8 @@ public class AudioDeviceBroker { AudioSystem.setParameters("A2dpSuspended=true"); AudioSystem.setParameters("LeAudioSuspended=true"); AudioSystem.setParameters("BT_SCO=on"); + mBluetoothA2dpSuspendedApplied = true; + mBluetoothLeSuspendedApplied = true; } else { AudioSystem.setParameters("BT_SCO=off"); if (mBluetoothA2dpSuspendedApplied) { @@ -1680,10 +1690,11 @@ public class AudioDeviceBroker { } /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, - boolean connect, @Nullable BluetoothDevice btDevice) { + boolean connect, @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { synchronized (mDeviceStateLock) { return mDeviceInventory.handleDeviceConnection( - attributes, connect, false /*for test*/, btDevice); + attributes, connect, false /*for test*/, btDevice, deviceSwitch); } } @@ -1776,6 +1787,18 @@ public class AudioDeviceBroker { pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio); + pw.println("\n" + prefix + "Bluetooth SCO on" + + ", requested: " + mBluetoothScoOn + + ", applied: " + mBluetoothScoOnApplied); + pw.println("\n" + prefix + "Bluetooth A2DP suspended" + + ", requested ext: " + mBluetoothA2dpSuspendedExt + + ", requested int: " + mBluetoothA2dpSuspendedInt + + ", applied " + mBluetoothA2dpSuspendedApplied); + pw.println("\n" + prefix + "Bluetooth LE Audio suspended" + + ", requested ext: " + mBluetoothLeSuspendedExt + + ", requested int: " + mBluetoothLeSuspendedInt + + ", applied " + mBluetoothLeSuspendedApplied); + mBtHelper.dump(pw, prefix); } @@ -1930,10 +1953,12 @@ public class AudioDeviceBroker { || btInfo.mIsLeOutput) ? mAudioService.getBluetoothContextualVolumeStream() : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID || (mScoManagedByAudio - && btInfo.mProfile == BluetoothProfile.HEADSET)) { + && btInfo.mProfile == BluetoothProfile.HEADSET)) + && (btInfo.mState == BluetoothProfile.STATE_CONNECTED + || !btInfo.mIsDeviceSwitch)) { onUpdateCommunicationRouteClient( bluetoothScoRequestOwnerAttributionSource(), "setBluetoothActiveDevice"); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index ef10793fd955..ae91934e7498 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -799,7 +799,7 @@ public class AudioDeviceInventory { di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, - di.mDeviceCodecFormat); + di.mDeviceCodecFormat, false /*deviceSwitch*/); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { failedReconnectionDeviceList.add(di); } @@ -811,7 +811,7 @@ public class AudioDeviceInventory { EventLogger.Event.ALOGE, TAG); mConnectedDevices.remove(di.getKey(), di); if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } } @@ -851,7 +851,8 @@ public class AudioDeviceInventory { Log.d(TAG, "onSetBtActiveDevice" + " btDevice=" + btInfo.mDevice + " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile) - + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState)); + + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState) + + " isDeviceSwitch=" + btInfo.mIsDeviceSwitch); } String address = btInfo.mDevice.getAddress(); if (!BluetoothAdapter.checkBluetoothAddress(address)) { @@ -897,7 +898,8 @@ public class AudioDeviceInventory { break; case BluetoothProfile.A2DP: if (switchToUnavailable) { - makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat); + makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { // device is not already connected if (btInfo.mVolume != -1) { @@ -911,7 +913,7 @@ public class AudioDeviceInventory { break; case BluetoothProfile.HEARING_AID: if (switchToUnavailable) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), streamType, "onSetBtActiveDevice"); @@ -921,7 +923,8 @@ public class AudioDeviceInventory { case BluetoothProfile.LE_AUDIO_BROADCAST: if (switchToUnavailable) { makeLeAudioDeviceUnavailableNow(address, - btInfo.mAudioSystemDevice, di.mDeviceCodecFormat); + btInfo.mAudioSystemDevice, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeLeAudioDeviceAvailable( btInfo, streamType, codec, "onSetBtActiveDevice"); @@ -930,9 +933,10 @@ public class AudioDeviceInventory { case BluetoothProfile.HEADSET: if (mDeviceBroker.isScoManagedByAudio()) { if (switchToUnavailable) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { - mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice); + mDeviceBroker.onSetBtScoActiveDevice( + btInfo.mDevice, false /*deviceSwitch*/); } } break; @@ -1053,19 +1057,19 @@ public class AudioDeviceInventory { /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { synchronized (mDevicesLock) { - makeA2dpDeviceUnavailableNow(address, a2dpCodec); + makeA2dpDeviceUnavailableNow(address, a2dpCodec, false /*deviceSwitch*/); } } /*package*/ void onMakeLeAudioDeviceUnavailableNow(String address, int device, int codec) { synchronized (mDevicesLock) { - makeLeAudioDeviceUnavailableNow(address, device, codec); + makeLeAudioDeviceUnavailableNow(address, device, codec, false /*deviceSwitch*/); } } /*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) { synchronized (mDevicesLock) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, false /*deviceSwitch*/); } } @@ -1180,7 +1184,8 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mAttributes, - wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) { + wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, + null, false /*deviceSwitch*/)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -1788,14 +1793,15 @@ public class AudioDeviceInventory { */ /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, boolean connect, boolean isForTesting, - @Nullable BluetoothDevice btDevice) { + @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { int device = attributes.getInternalType(); String address = attributes.getAddress(); String deviceName = attributes.getName(); if (AudioService.DEBUG_DEVICES) { Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:" + Integer.toHexString(device) + " address:" + address - + " name:" + deviceName + ")"); + + " name:" + deviceName + ", deviceSwitch: " + deviceSwitch + ")"); } MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection") .set(MediaMetrics.Property.ADDRESS, address) @@ -1829,7 +1835,8 @@ public class AudioDeviceInventory { res = AudioSystem.AUDIO_STATUS_OK; } else { res = mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + false /*deviceSwitch*/); } if (res != AudioSystem.AUDIO_STATUS_OK) { final String reason = "not connecting device 0x" + Integer.toHexString(device) @@ -1856,7 +1863,8 @@ public class AudioDeviceInventory { status = true; } else if (!connect && isConnected) { mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + deviceSwitch); // always remove even if disconnection failed mConnectedDevices.remove(deviceKey); mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes); @@ -2030,7 +2038,7 @@ public class AudioDeviceInventory { } } if (disconnect) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } @@ -2068,7 +2076,8 @@ public class AudioDeviceInventory { || info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST) && info.mIsLeOutput) || info.mProfile == BluetoothProfile.HEARING_AID - || info.mProfile == BluetoothProfile.A2DP)) { + || info.mProfile == BluetoothProfile.A2DP) + && !info.mIsDeviceSwitch) { @AudioService.ConnectionState int asState = (info.mState == BluetoothProfile.STATE_CONNECTED) ? AudioService.CONNECTION_STATE_CONNECTED @@ -2124,7 +2133,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false); // TODO: log in MediaMetrics once distinction between connection failure and // double connection is made. @@ -2362,7 +2371,7 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeA2dpDeviceUnavailableNow(String address, int codec) { + private void makeA2dpDeviceUnavailableNow(String address, int codec, boolean deviceSwitch) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address) .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(codec)) .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow"); @@ -2393,7 +2402,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_UNAVAILABLE, codec); + AudioSystem.DEVICE_STATE_UNAVAILABLE, codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2404,7 +2413,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable")).printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch)) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); @@ -2440,7 +2450,7 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available A2DP source device addr=" @@ -2465,7 +2475,7 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); @@ -2485,7 +2495,7 @@ public class AudioDeviceInventory { DEVICE_OUT_HEARING_AID, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available HearingAid addr=" + address @@ -2515,12 +2525,12 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeHearingAidDeviceUnavailable(String address) { + private void makeHearingAidDeviceUnavailable(String address, boolean deviceSwitch) { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, deviceSwitch); // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); @@ -2622,7 +2632,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false /*deviceSwitch*/); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address @@ -2669,13 +2679,13 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeLeAudioDeviceUnavailableNow(String address, int device, - @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { + @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, boolean deviceSwitch) { AudioDeviceAttributes ada = null; if (device != AudioSystem.DEVICE_NONE) { ada = new AudioDeviceAttributes(device, address); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - codec); + codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2685,7 +2695,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable").printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 86871ea45d13..057d1274d47d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -63,6 +63,7 @@ import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.deferWearPermissionUpdates; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringMyCar; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; @@ -4990,6 +4991,8 @@ public class AudioService extends IAudioService.Stub + cacheGetStreamMinMaxVolume()); pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:" + cacheGetStreamVolume()); + pw.println("\tcom.android.media.audio.optimizeBtDeviceSwitch:" + + optimizeBtDeviceSwitch()); } private void dumpAudioMode(PrintWriter pw) { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index e86c34cab88a..a6267c156fb3 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -367,9 +367,9 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, * @return */ public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { + int codecFormat, boolean deviceSwitch) { invalidateRoutingCache(); - return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat); + return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat, deviceSwitch); } /** diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 922116999bc7..844e3524384d 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -26,6 +26,8 @@ import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothA2dp; @@ -393,8 +395,11 @@ public class BtHelper { + "received with null profile proxy for device: " + btDevice)).printLog(TAG)); return; + } - onSetBtScoActiveDevice(btDevice); + boolean deviceSwitch = optimizeBtDeviceSwitch() + && btDevice != null && mBluetoothHeadsetDevice != null; + onSetBtScoActiveDevice(btDevice, deviceSwitch); } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); onScoAudioStateChanged(btState); @@ -814,7 +819,7 @@ public class BtHelper { if (device == null) { continue; } - onSetBtScoActiveDevice(device); + onSetBtScoActiveDevice(device, false /*deviceSwitch*/); } } else { Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter"); @@ -907,7 +912,8 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { + private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive, + boolean deviceSwitch) { if (btDevice == null) { return true; } @@ -919,12 +925,12 @@ public class BtHelper { if (isActive) { audioDevice = btHeadsetDeviceToAudioDevice(btDevice); result = mDeviceBroker.handleDeviceConnection( - audioDevice, true /*connect*/, btDevice); + audioDevice, true /*connect*/, btDevice, false /*deviceSwitch*/); } else { AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice); if (ada != null) { result = mDeviceBroker.handleDeviceConnection( - ada, false /*connect*/, btDevice); + ada, false /*connect*/, btDevice, deviceSwitch); } else { // Disconnect all possible audio device types if the disconnected device type is // unknown @@ -935,7 +941,8 @@ public class BtHelper { }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, address, name), false /*connect*/, btDevice); + outDeviceType, address, name), false /*connect*/, btDevice, + deviceSwitch); } } } @@ -944,7 +951,7 @@ public class BtHelper { // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( inDevice, address, name), - isActive, btDevice) && result; + isActive, btDevice, deviceSwitch) && result; if (result) { if (isActive) { mResolvedScoAudioDevices.put(btDevice, audioDevice); @@ -961,18 +968,18 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { + /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice) - + " -> " + getAnonymizedAddress(btDevice)); + + " -> " + getAnonymizedAddress(btDevice) + ", deviceSwitch: " + deviceSwitch); final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice; if (Objects.equals(btDevice, previousActiveDevice)) { return; } - if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) { + if (!handleBtScoActiveDeviceChange(previousActiveDevice, false, deviceSwitch)) { Log.w(TAG, "onSetBtScoActiveDevice() failed to remove previous device " + getAnonymizedAddress(previousActiveDevice)); } - if (!handleBtScoActiveDeviceChange(btDevice, true)) { + if (!handleBtScoActiveDeviceChange(btDevice, true, false /*deviceSwitch*/)) { Log.e(TAG, "onSetBtScoActiveDevice() failed to add new device " + getAnonymizedAddress(btDevice)); // set mBluetoothHeadsetDevice to null when failing to add new device diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 0aa7227ac7e6..d4bb1d52c111 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2614,7 +2614,8 @@ public final class DisplayManagerService extends SystemService { // Blank or unblank the display immediately to match the state requested // by the display power controller (if known). DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); - if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display == null) { return null; @@ -5574,7 +5575,9 @@ public final class DisplayManagerService extends SystemService { final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked( id).getPrimaryDisplayDeviceLocked(); final int flags = displayDevice.getDisplayDeviceInfoLocked().flags; - if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags + .correctVirtualDisplayPowerState()) { final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(id); if (displayPowerController != null) { diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 4779b690adfb..e7939bb50ece 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -371,7 +371,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCallback = callback; mProjection = projection; mMediaProjectionCallback = mediaProjectionCallback; - mDisplayState = Display.STATE_ON; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + // The display's power state depends on the power state of the state of its + // display / power group, which we don't know here. Initializing to UNKNOWN allows + // the first call to requestDisplayStateLocked() to set the correct state. + // This also triggers VirtualDisplay.Callback to tell the owner the initial state. + mDisplayState = Display.STATE_UNKNOWN; + } else { + mDisplayState = Display.STATE_ON; + } mPendingChanges |= PENDING_SURFACE_CHANGE; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled(); @@ -564,14 +572,23 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mInfo.yDpi = mDensityDpi; mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame mInfo.flags = 0; - if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE - | DisplayDeviceInfo.FLAG_NEVER_BLANK; - } - if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { - mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } else { - mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE + | DisplayDeviceInfo.FLAG_NEVER_BLANK; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { + mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + } else { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP; diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 70143f1c1a98..acdc0e0cf891 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -456,9 +456,8 @@ flag { flag { name: "enable_display_content_mode_management" namespace: "lse_desktop_experience" - description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode." + description: "Enable switching the content mode of connected displays between mirroring and extended. Also change the default content mode to extended mode." bug: "378385869" - is_fixed_read_only: true } flag { diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 2af74f620c95..7e8bb28b6a37 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,8 +569,7 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() - && !isDozingInternal()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { return; } diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java index 94842041af82..ab86433ca50d 100644 --- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java +++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java @@ -58,9 +58,9 @@ public interface AudioDeviceVolumeManagerWrapper { void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener); /** * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior( @@ -69,7 +69,7 @@ public interface AudioDeviceVolumeManagerWrapper { void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener); } diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java index ff99ace38ef0..10cbb00d2398 100644 --- a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java +++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java @@ -61,21 +61,21 @@ public class DefaultAudioDeviceVolumeManagerWrapper public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { - mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor, - vclistener, handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) { + mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, + handlesVolumeAdjustment, executor, vclistener); } @Override public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) { mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume, - executor, vclistener, handlesVolumeAdjustment); + handlesVolumeAdjustment, executor, vclistener); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 89f0d0edbf2b..6d973ac8d1b5 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -4798,15 +4798,15 @@ public class HdmiControlService extends SystemService { Slog.d(TAG, "Enabling absolute volume behavior"); for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) { getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior( - device, volumeInfo, mServiceThreadExecutor, - mAbsoluteVolumeChangedListener, true); + device, volumeInfo, true, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener); } } else if (tv() != null) { Slog.d(TAG, "Enabling adjust-only absolute volume behavior"); for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) { getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior( - device, volumeInfo, mServiceThreadExecutor, - mAbsoluteVolumeChangedListener, true); + device, volumeInfo, true, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener); } } diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 87f693cc7291..1ace41cba364 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -344,4 +344,42 @@ public abstract class InputManagerInternal { */ public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId) throws XmlPullParserException, IOException; + + /** + * An interface for filtering pointer motion event before cursor position is determined. + * <p> + * Different from {@code android.view.InputFilter}, this filter can filter motion events at + * an early stage of the input pipeline, but only called for pointer's relative motion events. + * Unless the user really needs to filter events before the cursor position in the display is + * determined, use {@code android.view.InputFilter} instead. + */ + public interface AccessibilityPointerMotionFilter { + /** + * Called everytime pointer's relative motion event happens. + * The returned dx and dy will be used to move the cursor in the display. + * <p> + * This call happens on the input hot path and it is extremely performance sensitive. It + * also must not call back into native code. + * + * @param dx delta x of the event in pixels. + * @param dy delta y of the event in pixels. + * @param currentX the cursor x coordinate on the screen before the motion event. + * @param currentY the cursor y coordinate on the screen before the motion event. + * @param displayId the display ID of the current cursor. + * @return an array of length 2, delta x and delta y after filtering the motion. The delta + * values are in pixels and must be between 0 and original delta. + */ + @NonNull + float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY, + int displayId); + } + + /** + * Registers an {@code AccessibilityCursorFilter}. + * + * @param filter The filter to register. If a filter is already registered, the old filter is + * unregistered. {@code null} unregisters the filter that is already registered. + */ + public abstract void registerAccessibilityPointerMotionFilter( + @Nullable AccessibilityPointerMotionFilter filter); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 8624f4230e9c..0e37238bcb84 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -25,8 +25,8 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.hardware.input.Flags.enableCustomizableInputGestures; -import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.keyEventActivityDetection; +import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER; @@ -193,15 +193,11 @@ public class InputManagerService extends IInputManager.Stub private static final int MSG_SYSTEM_READY = 5; private static final int DEFAULT_VIBRATION_MAGNITUDE = 192; - private static final AdditionalDisplayInputProperties - DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); private final NativeInputManagerService mNative; private final Context mContext; private final InputManagerHandler mHandler; - @UserIdInt - private int mCurrentUserId = UserHandle.USER_SYSTEM; private DisplayManagerInternal mDisplayManagerInternal; private WindowManagerInternal mWindowManagerInternal; @@ -289,7 +285,7 @@ public class InputManagerService extends IInputManager.Stub final Object mKeyEventActivityLock = new Object(); @GuardedBy("mKeyEventActivityLock") - private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify = + private final List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify = new ArrayList<>(); // Rate limit for key event activity detection. Prevent the listener from being notified @@ -460,6 +456,14 @@ public class InputManagerService extends IInputManager.Stub private boolean mShowKeyPresses = false; private boolean mShowRotaryInput = false; + /** + * A lock for the accessibility pointer motion filter. Don't call native methods while holding + * this lock. + */ + private final Object mAccessibilityPointerMotionFilterLock = new Object(); + private InputManagerInternal.AccessibilityPointerMotionFilter + mAccessibilityPointerMotionFilter = null; + /** Point of injection for test dependencies. */ @VisibleForTesting static class Injector { @@ -2593,6 +2597,23 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") + final float[] filterPointerMotion(float dx, float dy, float currentX, float currentY, + int displayId) { + // This call happens on the input hot path and it is extremely performance sensitive. + // This must not call back into native code. This is called while the + // PointerChoreographer's lock is held. + synchronized (mAccessibilityPointerMotionFilterLock) { + if (mAccessibilityPointerMotionFilter == null) { + throw new IllegalStateException( + "filterCursor is invoked but no callback is registered."); + } + return mAccessibilityPointerMotionFilter.filterPointerMotionEvent(dx, dy, currentX, + currentY, displayId); + } + } + + // Native callback. + @SuppressWarnings("unused") @VisibleForTesting public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { notifyKeyActivityListeners(event); @@ -3215,7 +3236,6 @@ public class InputManagerService extends IInputManager.Stub } private void handleCurrentUserChanged(@UserIdInt int userId) { - mCurrentUserId = userId; mKeyGestureController.setCurrentUserId(userId); } @@ -3828,6 +3848,12 @@ public class InputManagerService extends IInputManager.Stub payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId); } } + + @Override + public void registerAccessibilityPointerMotionFilter( + AccessibilityPointerMotionFilter filter) { + InputManagerService.this.registerAccessibilityPointerMotionFilter(filter); + } } @Override @@ -4014,6 +4040,26 @@ public class InputManagerService extends IInputManager.Stub mPointerIconCache.setAccessibilityScaleFactor(displayId, scaleFactor); } + void registerAccessibilityPointerMotionFilter( + InputManagerInternal.AccessibilityPointerMotionFilter filter) { + // `#filterPointerMotion` expects that when it's called, `mAccessibilityPointerMotionFilter` + // is not null. + // Also, to avoid potential lock contention, we shouldn't call native method while holding + // the lock here. Native code calls `#filterPointerMotion` while PointerChoreographer's + // lock is held. + // Thus, we must set filter before we enable the filter in native, and reset the filter + // after we disable the filter. + // This also ensures the previously installed filter isn't called after the filter is + // updated. + mNative.setAccessibilityPointerMotionFilterEnabled(false); + synchronized (mAccessibilityPointerMotionFilterLock) { + mAccessibilityPointerMotionFilter = filter; + } + if (filter != null) { + mNative.setAccessibilityPointerMotionFilterEnabled(true); + } + } + interface KeyboardBacklightControllerInterface { default void incrementKeyboardBacklight(int deviceId) {} default void decrementKeyboardBacklight(int deviceId) {} diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index f34338a397db..32409d39db3b 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -315,6 +315,16 @@ interface NativeInputManagerService { */ boolean setKernelWakeEnabled(int deviceId, boolean enabled); + /** + * Set whether the accessibility pointer motion filter is enabled. + * <p> + * Once enabled, {@link InputManagerService#filterPointerMotion} is called for evety motion + * event from pointer devices. + * + * @param enabled {@code true} if the filter is enabled, {@code false} otherwise. + */ + void setAccessibilityPointerMotionFilterEnabled(boolean enabled); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -628,5 +638,8 @@ interface NativeInputManagerService { @Override public native boolean setKernelWakeEnabled(int deviceId, boolean enabled); + + @Override + public native void setAccessibilityPointerMotionFilterEnabled(boolean enabled); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 334e7b5240ce..508bc2f811e0 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -365,7 +365,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return mCurrentImeUserId; } - /** + /** * Figures out the target IME user ID associated with the given {@code displayId}. * * @param displayId the display ID to be queried about @@ -1332,8 +1332,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = bindingController.getSelectedMethodId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null - && !settings.getMethodMap().get(selectedMethodId).isSystem()) { + final InputMethodInfo selectedImi = settings.getMethodMap().get(selectedMethodId); + if (selectedImi != null && !selectedImi.isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index f7a4d3d9132c..889df512dd60 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -157,6 +157,12 @@ public class ZenModeHelper { static final int RULE_LIMIT_PER_PACKAGE = 100; private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30); + /** + * Amount of time since last activation after which implicit rules that have never been + * customized by the user are automatically cleaned up. + */ + private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30); + private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000; /** @@ -534,7 +540,7 @@ public class ZenModeHelper { ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); if (sleepingRule != null && !sleepingRule.enabled - && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) { + && !sleepingRule.isUserModified()) { config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); } } @@ -864,7 +870,7 @@ public class ZenModeHelper { // We don't try to preserve system-owned rules because their conditionIds (used as // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot // delete a system-owned rule. - if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + if (origin == ORIGIN_APP && ruleToRemove.isUserModified() && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); if (deletedKey != null) { @@ -1282,7 +1288,7 @@ public class ZenModeHelper { // * the request comes from an origin that can always update values, like the user, or // * the rule has not yet been user modified, and thus can be updated by the app. boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) - || rule.canBeUpdatedByApp(); + || !rule.isUserModified(); // For all other values, if updates are not allowed, we discard the update. if (!updateValues) { @@ -1914,6 +1920,7 @@ public class ZenModeHelper { * <ul> * <li>Rule instances whose owner is not installed. * <li>Deleted rules that were deleted more than 30 days ago. + * <li>Implicit rules that haven't been used in 30 days (and have not been customized). * </ul> */ private void cleanUpZenRules() { @@ -1932,6 +1939,10 @@ public class ZenModeHelper { } } + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + deleteUnusedImplicitRules(newConfig.automaticRules); + } + if (!newConfig.equals(mConfig)) { setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "cleanUpZenRules", Process.SYSTEM_UID); @@ -1957,6 +1968,29 @@ public class ZenModeHelper { } } + private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) { + if (ruleList == null) { + return; + } + Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR); + + for (int i = ruleList.size() - 1; i >= 0; i--) { + ZenRule rule = ruleList.valueAt(i); + if (isImplicitRuleId(rule.id) && !rule.isUserModified()) { + if (rule.lastActivation == null) { + // This rule existed before we started tracking activation time. It *might* be + // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR) + // before being removed if truly unused. + rule.lastActivation = mClock.instant(); + } + + if (rule.lastActivation.isBefore(deleteIfUnusedSince)) { + ruleList.removeAt(i); + } + } + } + } + /** * @return a copy of the zen mode configuration */ @@ -2091,6 +2125,20 @@ public class ZenModeHelper { } } + // Update last activation for rules that are being activated. + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + Instant now = mClock.instant(); + if (!mConfig.isManualActive() && config.isManualActive()) { + config.manualRule.lastActivation = now; + } + for (ZenRule rule : config.automaticRules.values()) { + ZenRule previousRule = mConfig.automaticRules.get(rule.id); + if (rule.isActive() && (previousRule == null || !previousRule.isActive())) { + rule.lastActivation = now; + } + } + } + mConfig = config; dispatchOnConfigChanged(); updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 4153cd1be0a6..76c5240ab623 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,15 +1163,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private boolean shouldShowHub() { - final boolean hubEnabled = Settings.Secure.getIntForUser( - mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, - 1, mCurrentUserId) == 1; - - return mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && mDreamManagerInternal.dreamConditionActive(); - } - @VisibleForTesting void powerPress(long eventTime, int count, int displayId) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block @@ -1270,10 +1261,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (shouldShowHub() && keyguardAvailable) { - // If the hub can be launched, send a message to keyguard. We do not know if - // the hub is already running or not, keyguard handles turning screen off if - // it is. + if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled + && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { + // If the hub can be launched, send a message to keyguard. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1334,14 +1324,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { * @param isScreenOn Whether the screen is currently on. * @param noDreamAction The action to perform if dreaming is not possible. */ - private boolean attemptToDreamFromShortPowerButtonPress( + private void attemptToDreamFromShortPowerButtonPress( boolean isScreenOn, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); - return false; + return; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1349,7 +1339,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" + " press (isScreenOn=" + isScreenOn + ")"); noDreamAction.run(); - return false; + return; } synchronized (mLock) { @@ -1360,8 +1350,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); - - return true; } /** @@ -6410,17 +6398,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) { return; } - - if (!shouldShowHub() - && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP - && event.getKeyCode() == KEYCODE_POWER - && attemptToDreamFromShortPowerButtonPress(false, () -> {})) { - // In the case that we should wake to dream and successfully initiate dreaming, do not - // continue waking up. Doing so will exit the dream state and cause UI to react - // accordingly. - return; - } - wakeUpFromWakeKey( event.getEventTime(), event.getKeyCode(), diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index a4f983c33eb9..b607b0fce9ab 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2952,13 +2952,16 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } private boolean isOpaqueInner(@NonNull WindowContainer<?> container) { - // If it's a leaf task fragment, then opacity is calculated based on its activities. - if (container.asTaskFragment() != null - && ((TaskFragment) container).isLeafTaskFragment()) { + final boolean isActivity = container.asActivityRecord() != null; + final boolean isLeafTaskFragment = container.asTaskFragment() != null + && ((TaskFragment) container).isLeafTaskFragment(); + if (isActivity || isLeafTaskFragment) { + // When it is an activity or leaf task fragment, then opacity is calculated based + // on itself or its activities. return container.getActivity(this, true /* traverseTopToBottom */, null /* boundary */) != null; } - // When not a leaf, it's considered opaque if any of its opaque children fill this + // Otherwise, it's considered opaque if any of its opaque children fill this // container, unless the children are adjacent fragments, in which case as long as they // are all opaque then |container| is also considered opaque, even if the adjacent // task fragment aren't filling. diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index f07e6722d836..0d0c0bad24fa 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -124,6 +124,7 @@ static struct { jmethodID notifyStylusGestureStarted; jmethodID notifyVibratorState; jmethodID filterInputEvent; + jmethodID filterPointerMotion; jmethodID interceptKeyBeforeQueueing; jmethodID interceptMotionBeforeQueueingNonInteractive; jmethodID interceptKeyBeforeDispatching; @@ -451,6 +452,8 @@ public: void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId, const vec2& position) override; void notifyMouseCursorFadedOnTyping() override; + std::optional<vec2> filterPointerMotionForAccessibility( + const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) override; /* --- InputFilterPolicyInterface implementation --- */ void notifyStickyModifierStateChanged(uint32_t modifierState, @@ -938,6 +941,27 @@ void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged"); } +std::optional<vec2> NativeInputManager::filterPointerMotionForAccessibility( + const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) { + JNIEnv* env = jniEnv(); + ScopedFloatArrayRO filtered(env, + jfloatArray( + env->CallObjectMethod(mServiceObj, + gServiceClassInfo.filterPointerMotion, + delta.x, delta.y, current.x, + current.y, displayId.val()))); + if (checkAndClearExceptionFromCallback(env, "filterPointerMotionForAccessibilityLocked")) { + ALOGE("Disabling accessibility pointer motion filter due to an error. " + "The filter state in Java and PointerChoreographer would no longer be in sync."); + return std::nullopt; + } + LOG_ALWAYS_FATAL_IF(filtered.size() != 2, + "Accessibility pointer motion filter is misbehaving. Returned array size " + "%zu should be 2.", + filtered.size()); + return vec2{filtered[0], filtered[1]}; +} + sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(ui::LogicalDisplayId displayId) { JNIEnv* env = jniEnv(); jlong nativeSurfaceControlPtr = @@ -3271,6 +3295,12 @@ static jboolean nativeSetKernelWakeEnabled(JNIEnv* env, jobject nativeImplObj, j return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled); } +static void nativeSetAccessibilityPointerMotionFilterEnabled(JNIEnv* env, jobject nativeImplObj, + jboolean enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->getInputManager()->getChoreographer().setAccessibilityPointerMotionFilterEnabled(enabled); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -3398,6 +3428,8 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive}, {"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId}, {"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled}, + {"setAccessibilityPointerMotionFilterEnabled", "(Z)V", + (void*)nativeSetAccessibilityPointerMotionFilterEnabled}, }; #define FIND_CLASS(var, className) \ @@ -3482,6 +3514,8 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.filterInputEvent, clazz, "filterInputEvent", "(Landroid/view/InputEvent;I)Z"); + GET_METHOD_ID(gServiceClassInfo.filterPointerMotion, clazz, "filterPointerMotion", "(FFFFI)[F"); + GET_METHOD_ID(gServiceClassInfo.interceptKeyBeforeQueueing, clazz, "interceptKeyBeforeQueueing", "(Landroid/view/KeyEvent;I)I"); diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index a96c477c78d2..f731b50d81b4 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -17,6 +17,8 @@ package com.android.server.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.internal.util.Preconditions.checkCallAuthorization; @@ -79,6 +81,25 @@ public class SupervisionService extends ISupervisionManager.Stub { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this + * method is called, the launched activity still need to perform validity checks as the + * supervision state can change when it's launched. A null intent is returned if supervision is + * disabled at the time of this method call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + */ + @Override + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + // TODO(b/392961554): Implement createAuthenticationIntent API + throw new UnsupportedOperationException(); + } + + /** * Returns whether supervision is enabled for the given user. * * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or @@ -86,6 +107,7 @@ public class SupervisionService extends ISupervisionManager.Stub { */ @Override public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { + enforceAnyPermission(QUERY_USERS, MANAGE_USERS); if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -96,6 +118,7 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { + // TODO(b/395630828): Ensure that this method can only be called by the system. if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -181,8 +204,8 @@ public class SupervisionService extends ISupervisionManager.Stub { * Ensures that supervision is enabled when the supervision app is the profile owner. * * <p>The state syncing with the DevicePolicyManager can only enable supervision and never - * disable. Supervision can only be disabled explicitly via calls to the - * {@link #setSupervisionEnabledForUser} method. + * disable. Supervision can only be disabled explicitly via calls to the {@link + * #setSupervisionEnabledForUser} method. */ private void syncStateWithDevicePolicyManager(@UserIdInt int userId) { final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal(); @@ -221,6 +244,17 @@ public class SupervisionService extends ISupervisionManager.Stub { mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED); } + /** Enforces that the caller has at least one of the given permission. */ + private void enforceAnyPermission(String... permissions) { + boolean authorized = false; + for (String permission : permissions) { + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + authorized = true; + } + } + checkCallAuthorization(authorized); + } + /** Provides local services in a lazy manner. */ static class Injector { private final Context mContext; @@ -280,7 +314,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } @VisibleForTesting - @SuppressLint("MissingPermission") // not needed for a system service + @SuppressLint("MissingPermission") void registerProfileOwnerListener() { IntentFilter poIntentFilter = new IntentFilter(); poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 5d64cb638702..2d3f7231cc5c 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -34,12 +34,12 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import android.app.ActivityManager; import android.app.Instrumentation; import android.content.res.Configuration; import android.graphics.Insets; +import android.os.Build; import android.os.RemoteException; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; @@ -86,25 +86,36 @@ public class InputMethodServiceTest { "android:id/input_method_nav_back"; private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID = "android:id/input_method_nav_ime_switcher"; - private static final long TIMEOUT_IN_SECONDS = 3; - private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1"; - private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; + + /** Timeout until the uiObject should be found. */ + private static final long TIMEOUT_MS = 5000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout until the event is expected. */ + private static final long EXPECT_TIMEOUT_MS = 3000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout during which the event is not expected. */ + private static final long NOT_EXCEPT_TIMEOUT_MS = 2000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Command to set showing the IME when a hardware keyboard is connected. */ + private static final String SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD = + "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD; + /** Command to get verbose ImeTracker logging state. */ + private static final String GET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "getprop persist.debug.imetracker"; + /** Command to set verbose ImeTracker logging state. */ + private static final String SET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "setprop persist.debug.imetracker"; /** The ids of the subtypes of SimpleIme. */ private static final int[] SUBTYPE_IDS = new int[]{1, 2}; - private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); + private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); @Rule - public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider); - - @Rule public final TestName mName = new TestName(); private Instrumentation mInstrumentation; @@ -114,7 +125,8 @@ public class InputMethodServiceTest { private String mInputMethodId; private TestActivity mActivity; private InputMethodServiceWrapper mInputMethodService; - private boolean mShowImeWithHardKeyboardEnabled; + private boolean mOriginalVerboseImeTrackerLoggingEnabled; + private boolean mOriginalShowImeWithHardKeyboardEnabled; @Before public void setUp() throws Exception { @@ -123,9 +135,12 @@ public class InputMethodServiceTest { mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); mInputMethodId = getInputMethodId(); + mOriginalVerboseImeTrackerLoggingEnabled = getVerboseImeTrackerLogging(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(true); + } prepareIme(); prepareActivity(); - mInstrumentation.waitForIdleSync(); mUiDevice.freezeRotation(); mUiDevice.setOrientationNatural(); // Waits for input binding ready. @@ -148,17 +163,18 @@ public class InputMethodServiceTest { .that(mInputMethodService.getCurrentInputViewStarted()).isFalse(); }); // Save the original value of show_ime_with_hard_keyboard from Settings. - mShowImeWithHardKeyboardEnabled = + mOriginalShowImeWithHardKeyboardEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); } @After public void tearDown() throws Exception { mUiDevice.unfreezeRotation(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(false); + } // Change back the original value of show_ime_with_hard_keyboard in Settings. - executeShellCommand(mShowImeWithHardKeyboardEnabled - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + setShowImeWithHardKeyboard(mOriginalShowImeWithHardKeyboardEnabled); executeShellCommand("ime disable " + mInputMethodId); } @@ -170,7 +186,7 @@ public class InputMethodServiceTest { public void testShowHideKeyboard_byUserAction() { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Performs click on EditText to bring up the IME. Log.i(TAG, "Click on EditText"); @@ -201,14 +217,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInputMethodManager() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -219,14 +233,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInsetsController() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.hideImeWithWindowInsetsController(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -234,53 +246,18 @@ public class InputMethodServiceTest { /** * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. - * - * <p>With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked - * will be just apply the requested visibility (by using the callback). Therefore, we will - * lose flags like HIDE_IMPLICIT_ONLY. */ @Test public void testShowHideSelf() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // IME request to show itself without any flags, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(0 /* flags */), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_IMPLICIT_ONLY"); - } - - // IME request to hide itself without any flags, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf(0 /* flags */), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); - - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to show itself with flag SHOW_IMPLICIT, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), - EVENT_SHOW, true /* eventExpected */, true /* shown */, - "IME is shown with SHOW_IMPLICIT"); - - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, true /* eventExpected */, false /* shown */, - "IME is not shown after HIDE_IMPLICIT_ONLY"); - } } /** @@ -289,28 +266,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -323,28 +297,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should not show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isFalse()); + assertWithMessage("InputView should not show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isFalse(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -357,7 +328,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInput_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -386,49 +357,17 @@ public class InputMethodServiceTest { } /** - * This checks that an explicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputExplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the - // IME should be shown. - verifyInputViewStatusOnMainSync( - () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** - * This checks that an implicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputImplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, - // the IME should be shown. - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** * This checks that an explicit show request when the IME is not previously shown, * and it should be shown in fullscreen mode, results in the IME being shown. */ @Test public void testShowSoftInputExplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -442,34 +381,40 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request when the IME is not previously shown, - * and it should be shown in fullscreen mode, results in the IME not being shown. + * and it should be shown in fullscreen mode behaves like an explicit show request, resulting + * in the IME being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when the IME is not previously shown, + * and it should be shown in fullscreen mode, would result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + } } /** @@ -478,7 +423,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -497,17 +442,17 @@ public class InputMethodServiceTest { } /** - * This checks that an implicit show request when a hardware keyboard is connected, - * results in the IME not being shown. + * This checks that an implicit show request when a hardware keyboard is connected behaves + * like an explicit show request, resulting in the IME being shown. This is due to the + * refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when a hardware keyboard is connected would + * result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -516,10 +461,20 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - verifyInputViewStatusOnMainSync(() ->assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)) - .isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -532,7 +487,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -565,17 +520,17 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request followed by connecting a hardware keyboard - * and a configuration change, does not trigger IMS#onFinishInputView, - * but results in the IME being hidden. + * and a configuration change behaves like an explicit show request, resulting in the IME + * still being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request followed by connecting a hardware keyboard + * and a configuration change, would not trigger IMS#onFinishInputView, but resulted in the + * IME being hidden. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -596,16 +551,23 @@ public class InputMethodServiceTest { // Simulate a fake configuration change to avoid the recreation of TestActivity. config.orientation = Configuration.ORIENTATION_LANDSCAPE; - // Normally, IMS#onFinishInputView will be called when finishing the input view by - // the user. But if IMS#hideWindow is called when receiving a new configuration change, - // we don't expect that it's user-driven to finish the lifecycle of input view with - // IMS#onFinishInputView, because the input view will be re-initialized according - // to the last #mShowInputRequested state. So in this case we treat the input view as - // still alive. - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, - false /* shown */, "IME is not shown after a configuration change"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); + } else { + // Normally, IMS#onFinishInputView will be called when finishing the input view by + // the user. But if IMS#hideWindow is called when receiving a new configuration + // change, we don't expect that it's user-driven to finish the lifecycle of input + // view with IMS#onFinishInputView, because the input view will be re-initialized + // according to the last #mShowInputRequested state. So in this case we treat the + // input view as still alive. + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, + false /* shown */, "IME is not shown after a configuration change"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -619,7 +581,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -628,12 +590,10 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - // Explicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Implicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), @@ -654,17 +614,18 @@ public class InputMethodServiceTest { /** * This checks that a forced show request directly followed by an explicit show request, - * and then a hide not always request, still results in the IME being shown - * (i.e. the explicit show request retains the forced state). + * and then a not always hide request behaves like a normal hide request, resulting in the + * IME being hidden (i.e. the explicit show request does not retain the forced state). This is + * due to the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_FORCED}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * HIDE_NOT_ALWAYS. + * <p>Previously, a forced show request directly followed by an explicit show request, + * and then a not always hide request, would result in the IME still being shown + * (i.e. the explicit show request would retain the forced state). */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), @@ -674,11 +635,123 @@ public class InputMethodServiceTest { mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) - .isTrue(), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_NOT_ALWAYS"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_NOT_ALWAYS"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_NOT_ALWAYS"); + } + } + + /** + * This checks that an explicit show request followed by an implicit only hide request + * behaves like a normal hide request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show request followed by an implicit only hide request + * would result in the IME still being shown. + */ + @Test + public void testShowSoftInputExplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show request followed by an implicit only hide request + * results in the IME being hidden. + */ + @Test + public void testShowSoftInputImplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } + + /** + * This checks that an explicit show self request followed by an implicit only hide self request + * behaves like a normal hide self request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show self request followed by an implicit only hide self request + * would result in the IME still being shown. + */ + @Test + public void testShowSelfExplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show self request followed by an implicit only hide self request + * results in the IME being hidden. + */ + @Test + public void testShowSelfImplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); } /** @@ -686,7 +759,7 @@ public class InputMethodServiceTest { */ @Test public void testFullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); Log.i(TAG, "Set orientation natural"); verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */, @@ -723,25 +796,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is shown after show request") @@ -758,25 +828,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(false /* enabled */); + setDrawsImeNavBarAndSwitcherButton(false /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially not shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after show request") @@ -792,7 +859,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -818,7 +885,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -844,7 +911,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -855,7 +922,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -884,7 +951,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -893,7 +960,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -956,7 +1023,8 @@ public class InputMethodServiceTest { runnable.run(); } mInstrumentation.waitForIdleSync(); - eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + eventCalled = latch.await(eventExpected ? EXPECT_TIMEOUT_MS : NOT_EXCEPT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); } catch (InterruptedException e) { fail("Interrupted while waiting for latch: " + e.getMessage()); return; @@ -1016,10 +1084,8 @@ public class InputMethodServiceTest { verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */, "IME is not shown"); if (eventExpected) { - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -1062,6 +1128,7 @@ public class InputMethodServiceTest { private void prepareActivity() { mActivity = TestActivity.startSync(mInstrumentation); + mInstrumentation.waitForIdleSync(); Log.i(TAG, "Finish preparing activity with editor."); } @@ -1086,21 +1153,51 @@ public class InputMethodServiceTest { * @param enable the value to be set. */ private void setShowImeWithHardKeyboard(boolean enable) { + if (mInputMethodService == null) { + // If the IME is no longer around, reset the setting unconditionally. + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); + return; + } + final boolean currentEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); if (currentEnabled != enable) { - executeShellCommand(enable - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); eventually(() -> assertWithMessage("showImeWithHardKeyboard updated") .that(mInputMethodService.getShouldShowImeWithHardKeyboardForTesting()) .isEqualTo(enable)); } } - private static void executeShellCommand(@NonNull String cmd) { + /** + * Gets the verbose logging state in {@link android.view.inputmethod.ImeTracker}. + * + * @return {@code true} iff verbose logging is enabled. + */ + private static boolean getVerboseImeTrackerLogging() { + return executeShellCommand(GET_VERBOSE_IME_TRACKER_LOGGING_CMD).trim().equals("1"); + } + + /** + * Sets verbose logging in {@link android.view.inputmethod.ImeTracker}. + * + * @param enabled whether to enable or disable verbose logging. + * + * @implNote This must use {@link ActivityManager#notifySystemPropertiesChanged()} to listen + * for changes to the system property for the verbose ImeTracker logging. + */ + private void setVerboseImeTrackerLogging(boolean enabled) { + final var context = mInstrumentation.getContext(); + final var am = context.getSystemService(ActivityManager.class); + + executeShellCommand(SET_VERBOSE_IME_TRACKER_LOGGING_CMD + " " + (enabled ? "1" : "0")); + am.notifySystemPropertiesChanged(); + } + + @NonNull + private static String executeShellCommand(@NonNull String cmd) { Log.i(TAG, "Run command: " + cmd); - SystemUtil.runShellCommandOrThrow(cmd); + return SystemUtil.runShellCommandOrThrow(cmd); } /** @@ -1113,8 +1210,7 @@ public class InputMethodServiceTest { @NonNull private UiObject2 getUiObject(@NonNull BySelector bySelector) { - final var uiObject = mUiDevice.wait(Until.findObject(bySelector), - TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS)); + final var uiObject = mUiDevice.wait(Until.findObject(bySelector), TIMEOUT_MS); assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull(); return uiObject; } @@ -1137,10 +1233,10 @@ public class InputMethodServiceTest { * * <p>Note, neither of these are normally drawn when in three button navigation mode. * - * @param enabled whether the IME nav bar and IME Switcher button are drawn. + * @param enable whether the IME nav bar and IME Switcher button are drawn. */ - private void setDrawsImeNavBarAndSwitcherButton(boolean enabled) { - final int flags = enabled ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; + private void setDrawsImeNavBarAndSwitcherButton(boolean enable) { + final int flags = enable ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(flags); } diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java index 558d1a7c4e8b..d4d4dcaa4f48 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java @@ -111,12 +111,6 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override - public void requestHideSelf(int flags) { - Log.i(TAG, "requestHideSelf() " + flags); - super.requestHideSelf(flags); - } - - @Override public void onConfigurationChanged(Configuration newConfig) { Log.i(TAG, "onConfigurationChanged() " + newConfig); super.onConfigurationChanged(newConfig); diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java index b5a538fa09f8..c7da27420cbb 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java @@ -103,7 +103,7 @@ public class AudioDeviceInventoryTest { // NOTE: for now this is only when flag asDeviceConnectionFailure is true if (asDeviceConnectionFailure()) { when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, @@ -115,7 +115,7 @@ public class AudioDeviceInventoryTest { // test that the device is added when AudioSystem returns AUDIO_STATUS_OK // when setDeviceConnectionState is called for the connection when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_OK); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java index ce59a86c6ca3..39e7d727f7c5 100644 --- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java +++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java @@ -51,9 +51,9 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter { // Overrides of AudioSystemAdapter @Override public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { - Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s", - attributes.toString(), state, Integer.toHexString(codecFormat))); + int codecFormat, boolean deviceSwitch) { + Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s %b", + attributes.toString(), state, Integer.toHexString(codecFormat), deviceSwitch)); return AudioSystem.AUDIO_STATUS_OK; } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index fca0cfbc7d2f..cf2c15c5daca 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -432,7 +432,7 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java index ec44a918f8e8..f44517a47f55 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java @@ -112,7 +112,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @@ -135,7 +135,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test @@ -160,7 +160,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java index 7294ba62cdae..90f94cb4b596 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java @@ -183,9 +183,9 @@ public class FakeAudioFramework { public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } @@ -193,9 +193,9 @@ public class FakeAudioFramework { public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 4c1544f14667..67efb9e76692 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.userModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = new ZenPolicy(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenPolicyUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenDeviceEffectsUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test @@ -563,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(456); + } } config.automaticRules.put(rule.id, rule); @@ -600,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, ruleActual.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, ruleActual.lastActivation); + } } if (Flags.backupRestoreLogging()) { verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); @@ -633,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(789); + } } Parcel parcel = Parcel.obtain(); @@ -664,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, parceled.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, parceled.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, parceled.lastActivation); + } } assertEquals(rule, parceled); @@ -746,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_APP; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(123); + } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -781,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, fromXml.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, fromXml.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, fromXml.lastActivation); + } } } @@ -908,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; assertThat(rule.userModifiedFields).isEqualTo(1); - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeRuleXml(rule, baos); @@ -916,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule fromXml = readRuleXml(bais); assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields); - assertThat(fromXml.canBeUpdatedByApp()).isFalse(); + assertThat(fromXml.isUserModified()).isTrue(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 8a5f80cb3e49..6d0bf8b322fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -475,7 +475,8 @@ public class ZenModeDiffTest extends UiServiceTestCase { // "Metadata" fields are never compared. Set<String> exemptFields = new LinkedHashSet<>( Set.of("userModifiedFields", "zenPolicyUserModifiedFields", - "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin")); + "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin", + "lastActivation")); // Flagged fields are only compared if their flag is on. if (Flags.modesUi()) { exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. 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 4d2f105e27b3..0ab11e0cbe3d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -23,6 +23,7 @@ import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; +import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT; import static android.app.Flags.FLAG_MODES_MULTIUSER; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; @@ -124,7 +125,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import static java.time.temporal.ChronoUnit.DAYS; + import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager; @@ -219,7 +223,6 @@ import java.io.Reader; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.LinkedList; @@ -2233,8 +2236,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id, implicitRuleBeforeModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -2271,8 +2273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { implicitRuleWithModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -4611,7 +4612,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isTrue(); + assertThat(storedRule.isUserModified()).isFalse(); } @Test @@ -4719,7 +4720,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS @@ -4761,7 +4762,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -5713,8 +5714,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Start with deleted rules from 2 different packages. Instant now = Instant.ofEpochMilli(1701796461000L); - ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now); - ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now); + ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now); + ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); @@ -5832,9 +5833,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testRuleCleanup() throws Exception { Instant now = Instant.ofEpochMilli(1701796461000L); - Instant yesterday = now.minus(1, ChronoUnit.DAYS); - Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS); - Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); mTestClock.setNowMillis(now.toEpochMilli()); when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt())) @@ -5847,24 +5848,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { config.user = 42; mZenModeHelper.mConfigs.put(42, config); // okay rules (not deleted, package exists, with a range of creation dates). - config.automaticRules.put("ar1", newZenRule("good_pkg", now, null)); - config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null)); - config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null)); + config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now)); + config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday)); + config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo)); // newish rules for a missing package - config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null)); + config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday)); // oldish rules belonging to a missing package - config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null)); + config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo)); // rules deleted recently - config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday)); - config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo)); + config.deletedRules.put("del1", + newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday)); + config.deletedRules.put("del2", + newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo)); // rules deleted a long time ago - config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del3", + newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo)); // rules for a missing package, created recently and deleted recently - config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now)); + config.deletedRules.put("del4", newDeletedZenRule("del4", "bad_pkg", yesterday, now)); // rules for a missing package, created a long time ago and deleted recently - config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now)); + config.deletedRules.put("del5", newDeletedZenRule("del5", "bad_pkg", twoMonthsAgo, now)); // rules for a missing package, created a long time ago and deleted a long time ago - config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del6", + newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo)); mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. @@ -5874,14 +5879,115 @@ public class ZenModeHelperTest extends UiServiceTestCase { .containsExactly("del1", "del2", "del4"); } - private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) { + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing a bunch of implicit rules + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // used recently + ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday); + ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently1.id, usedRecently1); + config.automaticRules.put(usedRecently2.id, usedRecently2); + // not used in a long time + ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo); + config.automaticRules.put(longUnused.id, longUnused); + // created a long time ago, before lastActivation tracking + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, before lastActivation tracking + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + // not used in a long time, but was customized by user + ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo); + longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS; + config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized); + // created a long time ago, before lastActivation tracking, and was customized by user + ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo, + null); + oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON; + config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id, + oldAndLastUsageUnknownAndCustomized); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // The recently used OR modified OR last-used-unknown rules stay. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown, + newAndLastUsageUnknown, longUnusedButCustomized, + oldAndLastUsageUnknownAndCustomized); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant aWeekAgo = now.minus(7, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing implicit rules. + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // with last activation known + ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently.id, usedRecently); + // created a long time ago, with last activation unknown + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, with last activation unknown + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // All rules stayed. + usedRecently = getZenRule(usedRecently.id); + oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id); + newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id); + + // The rules with an unknown last usage have been assigned a placeholder one. + assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo); + assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now); + assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now); + } + + private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt, + @NonNull Instant deletedAt) { + ZenRule rule = newZenRule(id, pkg, createdAt); + rule.deletionInstant = deletedAt; + return rule; + } + + private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt, + @Nullable Instant lastActivatedAt) { + ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt); + implicitRule.lastActivation = lastActivatedAt; + return implicitRule; + } + + private static ZenRule newZenRule(String id, String pkg, Instant createdAt) { ZenRule rule = new ZenRule(); + rule.id = id; rule.pkg = pkg; rule.creationTime = createdAt.toEpochMilli(); rule.enabled = true; - rule.deletionInstant = deletedAt; + rule.deletionInstant = null; // Plus stuff so that isValidAutomaticRule() passes - rule.name = "A rule from " + pkg + " created on " + createdAt; + rule.name = "Rule " + id; rule.conditionId = Uri.parse(rule.name); return rule; } @@ -5919,11 +6025,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() { // Assume existence of a system-owned rule that is currently ACTIVE. - ZenRule systemRule = newZenRule("android", Instant.now(), null); + ZenRule systemRule = newZenRule("systemRule", "android", Instant.now()); systemRule.zenMode = ZEN_MODE_ALARMS; systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("systemRule", systemRule); + config.automaticRules.put(systemRule.id, systemRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5935,11 +6041,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5955,11 +6061,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -7255,6 +7361,125 @@ public class ZenModeHelperTest extends UiServiceTestCase { "config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID); } + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_updatesLastActivation() { + String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition")) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isNull(); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant secondActivation = Instant.ofEpochMilli(500); + mTestClock.setNow(secondActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setManualZenMode_updatesLastActivation() { + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull(); + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null, + ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID); + + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() { + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); + assertThat(implicitRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() { + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + // Manual activation comes first + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + + // Now the app says the rule should be active (assume it's on a schedule, and the app + // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't + // change the activation state. + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void addOrUpdateRule_doesNotUpdateLastActivation() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason", + CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); @@ -7272,22 +7497,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = - Correspondence.transforming(zr -> { - Parcel p = Parcel.obtain(); - try { - zr.writeToParcel(p, 0); - p.setDataPosition(0); - ZenRule copy = new ZenRule(p); - copy.creationTime = 0; - copy.userModifiedFields = 0; - copy.zenPolicyUserModifiedFields = 0; - copy.zenDeviceEffectsUserModifiedFields = 0; - return copy; - } finally { - p.recycle(); - } - }, - "Ignoring timestamp and userModifiedFields"); + Correspondence.transforming( + ZenModeHelperTest::cloneWithoutMetadata, + ZenModeHelperTest::cloneWithoutMetadata, + "Ignoring timestamps and userModifiedFields"); + + private static ZenRule cloneWithoutMetadata(ZenRule rule) { + Parcel p = Parcel.obtain(); + try { + rule.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; + copy.lastActivation = null; + return copy; + } finally { + p.recycle(); + } + } private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -7693,6 +7923,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { return mNowMillis; } + private void setNow(Instant instant) { + mNowMillis = instant.toEpochMilli(); + } + private void setNowMillis(long millis) { mNowMillis = millis; } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index 3c74ad06a21f..a9be47d71213 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -509,6 +509,32 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); } + @Test + public void testOpaque_nonLeafTaskFragmentWithDirectActivity_opaque() { + final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .build(); + directChildActivity.setOccludesParent(true); + final Task nonLeafTask = directChildActivity.getTask(); + final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(), + true /* createdByOrganizer */, false /* isEmbedded */); + nonLeafTask.addChild(directChildFragment, 0); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isTrue(); + } + + @Test + public void testOpaque_nonLeafTaskFragmentWithDirectActivity_transparent() { + final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .build(); + directChildActivity.setOccludesParent(false); + final Task nonLeafTask = directChildActivity.getTask(); + final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(), + true /* createdByOrganizer */, false /* isEmbedded */); + nonLeafTask.addChild(directChildFragment, 0); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isFalse(); + } + @NonNull private TaskFragment createChildTaskFragment(@NonNull Task parent, @WindowConfiguration.WindowingMode int windowingMode, diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index e6c3fb369b91..1e91bedb5c18 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; - import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.doReturn; @@ -30,7 +28,6 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; -import android.provider.Settings; import android.window.DesktopModeFlags; import androidx.test.filters.SmallTest; @@ -74,14 +71,12 @@ public class DesktopModeHelperTest { doReturn(mContext.getContentResolver()).when(mMockContext).getContentResolver(); resetDesktopModeFlagsCache(); resetEnforceDeviceRestriction(); - resetFlagOverride(); } @After public void tearDown() throws Exception { resetDesktopModeFlagsCache(); resetEnforceDeviceRestriction(); - resetFlagOverride(); } @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, @@ -167,7 +162,8 @@ public class DesktopModeHelperTest { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test - public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() + throws Exception { doReturn(true).when(mMockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ); @@ -246,13 +242,10 @@ public class DesktopModeHelperTest { cachedToggleOverride.set(/* obj= */ null, /* value= */ null); } - private void resetFlagOverride() { - Settings.Global.putString(mContext.getContentResolver(), - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null); - } - - private void setFlagOverride(DesktopModeFlags.ToggleOverride override) { - Settings.Global.putInt(mContext.getContentResolver(), - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); + private void setFlagOverride(DesktopModeFlags.ToggleOverride override) throws Exception { + Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(/* obj= */ null, /* value= */ override); } }
\ No newline at end of file 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 82435b24dad6..6e109a8d4eaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -164,6 +164,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BooleanSupplier; /** * Tests for the {@link DisplayContent} class. @@ -2674,16 +2675,67 @@ public class DisplayContentTests extends WindowTestsBase { public void testKeyguardGoingAwayWhileAodShown() { mDisplayContent.getDisplayPolicy().setAwake(true); - final WindowState appWin = newWindowBuilder("appWin", TYPE_APPLICATION).setDisplay( - mDisplayContent).build(); - final ActivityRecord activity = appWin.mActivityRecord; + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); - mAtm.mKeyguardController.setKeyguardShown(appWin.getDisplayId(), true /* keyguardShowing */, - true /* aodShowing */); - assertFalse(activity.isVisibleRequested()); + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Finish unlock + keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + } + + @Test + public void testKeyguardGoingAwayCanceledWhileAodShown() { + mDisplayContent.getDisplayPolicy().setAwake(true); - mAtm.mKeyguardController.keyguardGoingAway(appWin.getDisplayId(), 0 /* flags */); - assertTrue(activity.isVisibleRequested()); + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Same API call a second time cancels the unlock, because AOD isn't changing. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardShowing.getAsBoolean()); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); } @Test diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index b7b209b78300..100690dcbb65 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -821,6 +821,25 @@ public final class SatelliteManager { "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT"; /** + * A boolean value indicating whether application is optimized to utilize low bandwidth + * satellite data. + * The applications that are optimized for low bandwidth satellite data should set this + * property to {@code true} in the manifest to indicate to platform about the same. + * {@code + * <application> + * <meta-data + * android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED" + * android:value="true"/> + * </application> + * } + * <p> + * When {@code true}, satellite data optimized network is available for applications. + */ + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = + "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"; + + /** * Registers a {@link SatelliteStateChangeListener} to receive callbacks when the satellite * state may have changed. * @@ -3840,6 +3859,35 @@ public final class SatelliteManager { } } + /** + * Get list of application packages name that are optimized for low bandwidth satellite data. + * + * @return List of application packages name with data optimized network property. + * + * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED} + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public @NonNull List<String> getSatelliteDataOptimizedApps() { + List<String> appsNames = new ArrayList<>(); + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + appsNames = telephony.getSatelliteDataOptimizedApps(); + } else { + throw new IllegalStateException("telephony service is null."); + } + } catch (RemoteException ex) { + loge("getSatelliteDataOptimizedApps() RemoteException:" + ex); + ex.rethrowAsRuntimeException(); + } + + return appsNames; + } + @Nullable private static ITelephony getITelephony() { ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 08c003027c5b..1c6652daf498 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3596,4 +3596,15 @@ interface ITelephony { * @hide */ int getCarrierIdFromIdentifier(in CarrierIdentifier carrierIdentifier); + + + /** + * Get list of applications that are optimized for low bandwidth satellite data. + * + * @return List of Application Name with data optimized network property. + * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED} + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + List<String> getSatelliteDataOptimizedApps(); } diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 83d22d923c78..4d379e45a81a 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,18 +18,24 @@ package android.os.test; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** @@ -44,7 +50,9 @@ import java.util.concurrent.Executor; * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -54,24 +62,46 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; - private AutoDispatchThread mAutoDispatchThread; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + Method[] methods = TestLooperManager.class.getMethods(); + for (Method method : methods) { + if (method.getName().equals("peekWhen")) { + return true; + } + } + return false; + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { try { LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE); LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + MESSAGE_MARK_IN_USE_METHOD = null; + } else { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + } } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -106,6 +136,13 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + if (isAtLeastBaklava()) { + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); + } else { + mTestLooperManager = null; + } + mClock = clock; } @@ -117,19 +154,61 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { + private Message getMessageLinkedListLegacy() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; + } + messages.add(message); + } + + // Repost all Messages back to the queue with a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + // Ugly trick to reset the Message's "in use" flag. + // This is needed because the Message cannot be re-enqueued if it's + // marked in use. + message.copyFrom(message); + + // Adjust the Message's delivery time. + long newWhen = message.getWhen() - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + + // Send the Message back to its Handler to be re-enqueued. + message.getTarget().sendMessageAtTime(message, newWhen); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -147,12 +226,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNext() { + private Message messageQueueNextLegacy() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -185,18 +264,46 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); + public boolean isIdle() { + if (isAtLeastBaklava()) { + return isIdleBaklava(); + } else { + return isIdleLegacy(); + } + } + + private boolean isIdleBaklava() { + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; + } + private synchronized boolean isIdleLegacy() { + Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public synchronized Message nextMessage() { + public Message nextMessage() { + if (isAtLeastBaklava()) { + return nextMessageBaklava(); + } else { + return nextMessageLegacy(); + } + } + + private Message nextMessageBaklava() { + if (isIdle()) { + return mTestLooperManager.poll(); + } else { + return null; + } + } + + private synchronized Message nextMessageLegacy() { if (isIdle()) { - return messageQueueNext(); + return messageQueueNextLegacy(); } else { return null; } @@ -206,9 +313,26 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public synchronized void dispatchNext() { + public void dispatchNext() { + if (isAtLeastBaklava()) { + dispatchNextBaklava(); + } else { + dispatchNextLegacy(); + } + } + + private void dispatchNextBaklava() { + assertTrue(isIdle()); + Message msg = mTestLooperManager.poll(); + if (msg == null) { + return; + } + msg.getTarget().dispatchMessage(msg); + } + + private synchronized void dispatchNextLegacy() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = messageQueueNextLegacy(); if (msg == null) { return; } diff --git a/tests/vcn/Android.bp b/tests/vcn/Android.bp index 51a300bff7ea..661ed07a5669 100644 --- a/tests/vcn/Android.bp +++ b/tests/vcn/Android.bp @@ -16,13 +16,19 @@ android_test { name: "FrameworksVcnTests", // For access hidden connectivity methods in tests defaults: ["framework-connectivity-test-defaults"], + + // TODO: b/374174952 Use 36 after Android B finalization + min_sdk_version: "35", + srcs: [ "java/**/*.java", "java/**/*.kt", ], platform_apis: true, - test_suites: ["device-tests"], - certificate: "platform", + test_suites: [ + "general-tests", + "mts-tethering", + ], static_libs: [ "android.net.vcn.flags-aconfig-java-export", "androidx.test.rules", diff --git a/tests/vcn/AndroidManifest.xml b/tests/vcn/AndroidManifest.xml index a8f657c89f76..08effbd1f7cf 100644 --- a/tests/vcn/AndroidManifest.xml +++ b/tests/vcn/AndroidManifest.xml @@ -16,8 +16,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.frameworks.tests.vcn"> - <uses-sdk android:minSdkVersion="33" - android:targetSdkVersion="33"/> + <!-- TODO: b/374174952 Use 36 after Android B finalization --> + <uses-sdk android:minSdkVersion="35" android:targetSdkVersion="35" /> + <application> <uses-library android:name="android.test.runner" /> </application> diff --git a/tests/vcn/AndroidTest.xml b/tests/vcn/AndroidTest.xml index dc521fd7bcd9..9c8362f36cb2 100644 --- a/tests/vcn/AndroidTest.xml +++ b/tests/vcn/AndroidTest.xml @@ -14,12 +14,20 @@ limitations under the License. --> <configuration description="Runs VCN Tests."> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> <option name="test-file-name" value="FrameworksVcnTests.apk" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <option name="test-tag" value="FrameworksVcnTests" /> + + <!-- Run tests in MTS only if the Tethering Mainline module is installed. --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.tethering" /> + </object> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.frameworks.tests.vcn" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> diff --git a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java index 156961312323..0fa11ae1fe7d 100644 --- a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java +++ b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java @@ -23,11 +23,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.HashSet; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnCellUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase { private static final Set<String> ALLOWED_PLMN_IDS = new HashSet<>(); private static final Set<Integer> ALLOWED_CARRIER_IDS = new HashSet<>(); diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java index 73a0a6183cb6..fa97de0aff45 100644 --- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java @@ -29,11 +29,14 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.content.Context; +import android.os.Build; import android.os.Parcel; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -42,7 +45,10 @@ import org.junit.runner.RunWith; import java.util.Collections; import java.util.Set; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnConfigTest { private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName(); diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java index 59dc68900100..990cc74caf6c 100644 --- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java @@ -34,10 +34,13 @@ import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; import android.net.vcn.persistablebundleutils.IkeSessionParamsUtilsTest; import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtilsTest; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,7 +52,10 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConfigTest { // Public for use in VcnGatewayConnectionTest diff --git a/tests/vcn/java/android/net/vcn/VcnManagerTest.java b/tests/vcn/java/android/net/vcn/VcnManagerTest.java index 8461de6d877b..1739fbc0fa6d 100644 --- a/tests/vcn/java/android/net/vcn/VcnManagerTest.java +++ b/tests/vcn/java/android/net/vcn/VcnManagerTest.java @@ -38,16 +38,28 @@ import android.net.NetworkCapabilities; import android.net.vcn.VcnManager.VcnStatusCallback; import android.net.vcn.VcnManager.VcnStatusCallbackBinder; import android.net.vcn.VcnManager.VcnUnderlyingNetworkPolicyListener; +import android.os.Build; import android.os.ParcelUuid; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.net.UnknownHostException; import java.util.UUID; import java.util.concurrent.Executor; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnManagerTest { private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0)); private static final String GATEWAY_CONNECTION_NAME = "gatewayConnectionName"; diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java index 7bc9970629a6..52952eb3f2cc 100644 --- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java +++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java @@ -30,12 +30,24 @@ import static org.junit.Assert.fail; import android.net.NetworkCapabilities; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.os.Build; import android.os.Parcel; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnTransportInfoTest { private static final int SUB_ID = 1; private static final int NETWORK_ID = 5; diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java index a674425efea3..c82d2003dbf6 100644 --- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java @@ -22,9 +22,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import android.net.NetworkCapabilities; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; +import org.junit.runner.RunWith; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnUnderlyingNetworkPolicyTest { private static final VcnUnderlyingNetworkPolicy DEFAULT_NETWORK_POLICY = new VcnUnderlyingNetworkPolicy( diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java index 2110d6ee7c86..22361cc71f12 100644 --- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java @@ -22,14 +22,20 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.net.TelephonyNetworkSpecifier; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnUnderlyingNetworkSpecifierTest { private static final int[] TEST_SUB_IDS = new int[] {1, 2, 3, 5}; diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java index 3ce6c8f9386d..fb040d8f9b91 100644 --- a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java @@ -30,13 +30,25 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.TelephonyNetworkSpecifier; import android.net.wifi.WifiInfo; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; import java.util.Collections; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnUtilsTest { private static final int SUB_ID = 1; diff --git a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java index 4063178e005d..2c072e1cbc88 100644 --- a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java +++ b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java @@ -22,10 +22,23 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnWifiUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase { private static final String SSID = "TestWifi"; diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java index bc8e9d3200b6..01e9ac2ac3cf 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java @@ -21,11 +21,14 @@ import static android.telephony.TelephonyManager.APPTYPE_USIM; import static org.junit.Assert.assertEquals; import android.net.eap.EapSessionConfig; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +38,10 @@ import java.nio.charset.StandardCharsets; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class EapSessionConfigUtilsTest { private static final byte[] EAP_ID = "test@android.net".getBytes(StandardCharsets.US_ASCII); diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java index 4f3930f9b5af..821e5a6c94cb 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java @@ -25,10 +25,13 @@ import android.net.ipsec.ike.IkeIpv4AddrIdentification; import android.net.ipsec.ike.IkeIpv6AddrIdentification; import android.net.ipsec.ike.IkeKeyIdIdentification; import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +42,10 @@ import java.net.InetAddress; import javax.security.auth.x500.X500Principal; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeIdentificationUtilsTest { private static void verifyPersistableBundleEncodeDecodeIsLossless(IkeIdentification id) { diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java index 9f7d2390938f..7200aee1c012 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java @@ -29,14 +29,16 @@ import android.net.InetAddresses; import android.net.eap.EapSessionConfig; import android.net.ipsec.ike.IkeFqdnIdentification; import android.net.ipsec.ike.IkeSessionParams; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.org.bouncycastle.util.io.pem.PemObject; import com.android.internal.org.bouncycastle.util.io.pem.PemReader; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,7 +54,10 @@ import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeSessionParamsUtilsTest { // Public for use in VcnGatewayConnectionConfigTest, EncryptedTunnelParamsUtilsTest diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java index 28cf38a2a583..957e785d70c0 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java @@ -20,17 +20,23 @@ import static org.junit.Assert.assertEquals; import android.net.InetAddresses; import android.net.ipsec.ike.IkeTrafficSelector; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; import java.net.InetAddress; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeTrafficSelectorUtilsTest { private static final int START_PORT = 16; diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java index 664044a9e7d4..1e8f5ff2dc07 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java @@ -21,15 +21,21 @@ import static org.junit.Assert.assertEquals; import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.IkeSaProposal; import android.net.ipsec.ike.SaProposal; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class SaProposalUtilsTest { /** Package private so that IkeSessionParamsUtilsTest can use it */ diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java index f9dc9eb4d5ae..7d17724112ec 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java @@ -20,14 +20,20 @@ import static org.junit.Assert.assertEquals; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TunnelConnectionParamsUtilsTest { // Public for use in VcnGatewayConnectionConfigTest diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java index e0b5f0ef0381..3d7348a79b8c 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java @@ -25,10 +25,13 @@ import android.net.InetAddresses; import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.IkeTrafficSelector; import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,7 +40,10 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TunnelModeChildSessionParamsUtilsTest { // Package private for use in EncryptedTunnelParamsUtilsTest diff --git a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java index 47638b002f37..99c7aa72146b 100644 --- a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java @@ -33,9 +33,12 @@ import static org.junit.Assert.assertTrue; import static java.util.Collections.emptyList; import android.net.ipsec.ike.ChildSaProposal; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,7 +46,10 @@ import org.junit.runner.RunWith; import java.util.Arrays; import java.util.List; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class MtuUtilsTest { private void verifyUnderlyingMtuZero(boolean isIpv4) { diff --git a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java index c84e60086b37..f7786af840ee 100644 --- a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java @@ -21,10 +21,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +38,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class PersistableBundleUtilsTest { private static final String TEST_KEY = "testKey"; diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 26a2a0636792..a97f9a837bab 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -79,6 +79,7 @@ import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkPolicy; import android.net.vcn.util.PersistableBundleUtils; import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; +import android.os.Build; import android.os.IBinder; import android.os.ParcelUuid; import android.os.PersistableBundle; @@ -93,7 +94,6 @@ import android.telephony.TelephonyManager; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.VcnManagementService.VcnCallback; import com.android.server.VcnManagementService.VcnStatusCallbackInfo; @@ -101,6 +101,8 @@ import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Rule; @@ -117,8 +119,10 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; -/** Tests for {@link VcnManagementService}. */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnManagementServiceTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java index 77f82f0d8cf4..6276be27fbf5 100644 --- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java +++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java @@ -54,6 +54,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.vcn.VcnManager; +import android.os.Build; import android.os.Handler; import android.os.ParcelUuid; import android.os.PersistableBundle; @@ -69,9 +70,10 @@ import android.util.ArrayMap; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.HandlerExecutor; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -87,8 +89,10 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TelephonySubscriptionTrackerTest { private static final String PACKAGE_NAME = diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index 74db6a5211a0..6608dda95a4b 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -70,16 +70,18 @@ import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnManager.VcnErrorCode; import android.net.vcn.VcnTransportInfo; import android.net.vcn.util.MtuUtils; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback; import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration; import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -94,8 +96,10 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; -/** Tests for VcnGatewayConnection.ConnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { private static final int PARALLEL_SA_COUNT = 4; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java index 3c70759a2fa6..f6123d29f35a 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java @@ -26,17 +26,22 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.net.ipsec.ike.IkeSessionParams; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -/** Tests for VcnGatewayConnection.ConnectingState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectionTestBase { private VcnIkeSession mIkeSession; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java index f3eb82f46de7..7cfaf5be5111 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java @@ -30,16 +30,21 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.net.IpSecManager; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.DisconnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnectionTestBase { @Before diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java index 78aefad9f8ff..9132d830c54e 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java @@ -23,15 +23,21 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.os.Build; + import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.DisconnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionDisconnectingStateTest extends VcnGatewayConnectionTestBase { @Before diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java index 6568cdd44377..d5ef4e028709 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java @@ -27,15 +27,21 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.os.Build; + import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.RetryTimeoutState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionRetryTimeoutStateTest extends VcnGatewayConnectionTestBase { private long mFirstRetryInterval; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java index b9fe76a24d20..5283322682ee 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java @@ -61,15 +61,17 @@ import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnManager; import android.net.vcn.VcnTransportInfo; import android.net.wifi.WifiInfo; +import android.os.Build; import android.os.ParcelUuid; import android.os.Process; import android.telephony.SubscriptionInfo; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -87,8 +89,10 @@ import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { private static final int TEST_UID = Process.myUid() + 1; diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java index e9026e22b6b2..2b92428918db 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java @@ -29,12 +29,14 @@ import android.annotation.NonNull; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkRequest; +import android.os.Build; import android.os.test.TestLooper; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -44,8 +46,10 @@ import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.List; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnNetworkProviderTest { private static final int TEST_SCORE_UNSATISFIED = 0; diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java index 6d269686e42f..bd4aeba761da 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java @@ -49,20 +49,26 @@ import android.net.Uri; import android.net.vcn.VcnConfig; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; +import android.os.Build; import android.os.ParcelUuid; import android.os.test.TestLooper; import android.provider.Settings; import android.telephony.TelephonyManager; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + import com.android.server.VcnManagementService.VcnCallback; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.Vcn.VcnGatewayStatusCallback; import com.android.server.vcn.Vcn.VcnUserMobileDataStateListener; import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; @@ -73,6 +79,11 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnTest { private static final String PKG_NAME = VcnTest.class.getPackage().getName(); private static final ParcelUuid TEST_SUB_GROUP = new ParcelUuid(new UUID(0, 0)); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java index c11b6bb3435d..53a36d3e4d6a 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java @@ -44,16 +44,22 @@ import static org.mockito.Mockito.when; import android.content.BroadcastReceiver; import android.content.Intent; import android.net.IpSecTransformState; +import android.os.Build; import android.os.OutcomeReceiver; import android.os.PowerManager; +import androidx.test.filters.SmallTest; + import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculationResult; import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator; import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper; import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -63,6 +69,11 @@ import java.util.Arrays; import java.util.BitSet; import java.util.concurrent.TimeUnit; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase { private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName(); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java index 4f34f9f8f74c..a9c637f7c943 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java @@ -42,16 +42,28 @@ import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkTemplate; import android.net.vcn.VcnWifiUnderlyingNetworkTemplate; +import android.os.Build; import android.os.PersistableBundle; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Collections; import java.util.List; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase { private UnderlyingNetworkRecord mWifiNetworkRecord; private UnderlyingNetworkRecord mCellNetworkRecord; diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java index e540932d0e1f..99c508c139ec 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java @@ -58,6 +58,7 @@ import android.net.vcn.VcnCellUnderlyingNetworkTemplate; import android.net.vcn.VcnCellUnderlyingNetworkTemplateTest; import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.os.Build; import android.os.ParcelUuid; import android.os.test.TestLooper; import android.telephony.CarrierConfigManager; @@ -65,6 +66,8 @@ import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; @@ -73,9 +76,12 @@ import com.android.server.vcn.routeselection.UnderlyingNetworkController.Network import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -89,6 +95,11 @@ import java.util.List; import java.util.Set; import java.util.UUID; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class UnderlyingNetworkControllerTest { private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0)); private static final int INITIAL_SUB_ID_1 = 1; diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java index a315b0690ec5..27c1bc105bde 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java @@ -38,19 +38,30 @@ import static org.mockito.Mockito.when; import android.net.IpSecTransform; import android.net.vcn.VcnGatewayConnectionConfig; +import android.os.Build; + +import androidx.test.filters.SmallTest; import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import java.util.concurrent.TimeUnit; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { private static final int PENALTY_TIMEOUT_MIN = 10; private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN); |