diff options
253 files changed, 7377 insertions, 2235 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 006b907ef283..60be8a76e3b2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -45170,6 +45170,7 @@ package android.telephony { field public static final String KEY_RTT_UPGRADE_SUPPORTED_BOOL = "rtt_upgrade_supported_bool"; field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = "satellite_connected_notification_throttle_millis_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int"; field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DISPLAY_NAME_STRING = "satellite_display_name_string"; @@ -48742,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 ac5af908eb31..975c2c27cb22 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -478,8 +478,8 @@ package android.app { method public void destroy(); method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions(); method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle); - method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); - method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); + method @Deprecated public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); + method @Deprecated public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo); method public void removeOverridePermissionState(int, @NonNull String); method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle); @@ -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/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/UiAutomation.java b/core/java/android/app/UiAutomation.java index 7b63ab80964d..464bcc025d92 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -956,10 +956,9 @@ public final class UiAutomation { * <p> * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> - * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @return Whether event injection succeeded. + * @param event the event to inject + * @param sync whether to inject the event synchronously + * @return {@code true} if event injection succeeded */ public boolean injectInputEvent(InputEvent event, boolean sync) { return injectInputEvent(event, sync, true /* waitForAnimations */); @@ -972,15 +971,21 @@ public final class UiAutomation { * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @param waitForAnimations Whether to wait for all window container animations and surface - * operations to complete. - * @return Whether event injection succeeded. + * @param event the event to inject + * @param sync whether to inject the event synchronously. + * @param waitForAnimations whether to wait for all window container animations and surface + * operations to complete + * @return {@code true} if event injection succeeded * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated // Deprecated for CTS tests + @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250 public boolean injectInputEvent(@NonNull InputEvent event, boolean sync, boolean waitForAnimations) { try { @@ -1003,9 +1008,15 @@ public final class UiAutomation { * Events injected to the input subsystem using the standard {@link #injectInputEvent} method * skip the accessibility input filter to avoid feedback loops. * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated + @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250 public void injectInputEventToInputFilter(@NonNull InputEvent event) { try { mUiAutomationConnection.injectInputEventToInputFilter(event); 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/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/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index d8919160320a..7850e377ec4d 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -612,6 +612,7 @@ public final class DisplayManager { PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS, PRIVATE_EVENT_TYPE_HDR_SDR_RATIO_CHANGED, PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED, + PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface PrivateEventType {} @@ -677,7 +678,7 @@ public final class DisplayManager { * through the {@link DisplayListener#onDisplayChanged} callback method. New brightness * values can be retrieved via {@link android.view.Display#getBrightnessInfo()}. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * * @hide */ @@ -690,7 +691,7 @@ public final class DisplayManager { * * Requires that {@link Display#isHdrSdrRatioAvailable()} is true. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * * @hide */ @@ -699,11 +700,19 @@ public final class DisplayManager { /** * Event type to register for a display's connection changed. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * @hide */ public static final long PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED = 1L << 2; + /** + * Event type to register for a display's committed state changes. + * + * @see #registerDisplayListener(DisplayListener, Handler, long, long) + * @hide + */ + public static final long PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 3; + /** @hide */ public DisplayManager(Context context) { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 339dbf2c2029..a7d610e54e2c 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -113,7 +113,8 @@ public final class DisplayManagerGlobal { EVENT_DISPLAY_CONNECTED, EVENT_DISPLAY_DISCONNECTED, EVENT_DISPLAY_REFRESH_RATE_CHANGED, - EVENT_DISPLAY_STATE_CHANGED + EVENT_DISPLAY_STATE_CHANGED, + EVENT_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface DisplayEvent {} @@ -128,6 +129,8 @@ public final class DisplayManagerGlobal { public static final int EVENT_DISPLAY_DISCONNECTED = 7; public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8; public static final int EVENT_DISPLAY_STATE_CHANGED = 9; + public static final int EVENT_DISPLAY_COMMITTED_STATE_CHANGED = 10; + @LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = { INTERNAL_EVENT_FLAG_DISPLAY_ADDED, @@ -139,6 +142,7 @@ public final class DisplayManagerGlobal { INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, INTERNAL_EVENT_FLAG_DISPLAY_STATE, INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED, + INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface InternalEventFlag {} @@ -152,6 +156,8 @@ public final class DisplayManagerGlobal { public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6; public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7; public static final long INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED = 1L << 8; + public static final long INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 9; + @UnsupportedAppUsage private static DisplayManagerGlobal sInstance; @@ -1550,6 +1556,12 @@ public final class DisplayManagerGlobal { mListener.onDisplayChanged(displayId); } break; + case EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + if ((mInternalEventFlagsMask + & INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0) { + mListener.onDisplayChanged(displayId); + } + break; } if (DEBUG) { Trace.endSection(); @@ -1710,6 +1722,8 @@ public final class DisplayManagerGlobal { return "EVENT_DISPLAY_REFRESH_RATE_CHANGED"; case EVENT_DISPLAY_STATE_CHANGED: return "EVENT_DISPLAY_STATE_CHANGED"; + case EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + return "EVENT_DISPLAY_COMMITTED_STATE_CHANGED"; } return "UNKNOWN"; } @@ -1756,6 +1770,13 @@ public final class DisplayManagerGlobal { & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED; } + + if (Flags.committedStateSeparateEvent()) { + if ((privateEventFlags + & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED; + } + } return baseEventMask; } diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java index 48c5887d80d0..586830c8d189 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java @@ -224,6 +224,10 @@ public class FingerprintSensorConfigurations implements Parcelable { } catch (RemoteException e) { Log.d(TAG, "Unable to get sensor properties!"); } + + if (props == null) { + props = new SensorProps[]{}; + } return props; } } 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/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 464756842caf..41a64e22e058 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -1595,8 +1595,17 @@ public abstract class WallpaperService extends Service { mWindow.setSession(mSession); mLayout.packageName = getPackageName(); - mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, - mCaller.getHandler()); + if (com.android.server.display.feature.flags.Flags + .displayListenerPerformanceImprovements() + && com.android.server.display.feature.flags.Flags + .committedStateSeparateEvent()) { + mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, + mCaller.getHandler(), DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED); + } else { + mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, + mCaller.getHandler()); + } mDisplay = mIWallpaperEngine.mDisplay; // Use window context of TYPE_WALLPAPER so client can access UI resources correctly. mDisplayContext = createDisplayContext(mDisplay) 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/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index ecdbaa3cd2f4..d880072aa404 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -44,6 +44,7 @@ import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import com.android.internal.display.BrightnessSynchronizer; +import com.android.server.display.feature.flags.Flags; import java.util.Arrays; import java.util.Objects; @@ -447,18 +448,20 @@ public final class DisplayInfo implements Parcelable { } public boolean equals(DisplayInfo other) { - return equals(other, /* compareRefreshRate */ true); + return equals(other, /* compareOnlyBasicChanges */ false); } /** * Compares if the two DisplayInfo objects are equal or not * @param other The other DisplayInfo against which the comparison is to be done - * @param compareRefreshRate Indicates if the refresh rate is also to be considered in - * comparison + * @param compareOnlyBasicChanges Indicates if the changes to be compared are the ones which + * could lead to an emission of + * {@link android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED} + * event * @return */ - public boolean equals(DisplayInfo other, boolean compareRefreshRate) { - boolean isEqualWithoutRefreshRate = other != null + public boolean equals(DisplayInfo other, boolean compareOnlyBasicChanges) { + boolean isEqualWithOnlyBasicChanges = other != null && layerStack == other.layerStack && flags == other.flags && type == other.type @@ -494,7 +497,6 @@ public final class DisplayInfo implements Parcelable { && physicalXDpi == other.physicalXDpi && physicalYDpi == other.physicalYDpi && state == other.state - && committedState == other.committedState && ownerUid == other.ownerUid && Objects.equals(ownerPackageName, other.ownerPackageName) && removeMode == other.removeMode @@ -512,14 +514,19 @@ public final class DisplayInfo implements Parcelable { thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId) && canHostTasks == other.canHostTasks; - if (compareRefreshRate) { - return isEqualWithoutRefreshRate + if (!Flags.committedStateSeparateEvent()) { + isEqualWithOnlyBasicChanges = isEqualWithOnlyBasicChanges + && (committedState == other.committedState); + } + if (!compareOnlyBasicChanges) { + return isEqualWithOnlyBasicChanges && (getRefreshRate() == other.getRefreshRate()) && appVsyncOffsetNanos == other.appVsyncOffsetNanos && presentationDeadlineNanos == other.presentationDeadlineNanos - && (modeId == other.modeId); + && (modeId == other.modeId) + && (committedState == other.committedState); } - return isEqualWithoutRefreshRate; + return isEqualWithOnlyBasicChanges; } @Override 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/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 578b7b6a63fa..ede0b3cf8cce 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -2684,9 +2684,10 @@ public class AccessibilityNodeInfo implements Parcelable { * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection} * should be constructed with {@code this} node or a descendant of it. * - * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and {@link - * AccessibilityNodeInfo#setFocused} should both be called with {@code true} before setting the - * selection in order to make {@code this} node a candidate to contain a selection. + * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and + * {@link AccessibilityNodeInfo#setFocused} should both be called with {@code true} + * before setting the selection in order to make {@code this} node a candidate to + * contain a selection. * * <p><b>Note:</b> Cannot be called from an AccessibilityService. This class is made immutable * before being delivered to an AccessibilityService. @@ -2706,12 +2707,10 @@ public class AccessibilityNodeInfo implements Parcelable { * Gets the extended selection, which is a representation of selection that spans multiple nodes * that exist within the subtree of the node defining selection. * - * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection} - * should be constructed with {@code this} node or a descendant of it. - * - * <p><b>Note:</b> In order for a node to be a candidate to contain a selection, {@link - * AccessibilityNodeInfo#isFocusable()} ()} and {@link AccessibilityNodeInfo#isFocused()} should - * both be return with {@code true}. + * <p><b>Note:</b> Nodes that are candidates to contain a selection should return + * {@code true} from {@link #isFocusable()} and {@link #isFocused()}. + * The start and end {@link SelectionPosition}s of this {@link Selection} + * should exist within {@code this} node or its descendants. * * @return The extended selection within the node's subtree, or {@code null} if no selection * exists. @@ -5840,8 +5839,8 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Instantiates a new SelectionPosition. * - * @param view The {@link View} containing the virtual descendant associated with the - * selection position. + * @param view The {@link View} containing the text associated with this selection + * position. * @param offset The offset for a selection position within {@code view}'s text content, * which should be a value between 0 and the length of {@code view}'s text. */ 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/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/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..4974285d3a35 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" /> diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index dc2f0a69375d..9383807ec761 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.os.Handler; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -348,6 +349,26 @@ public class DisplayManagerGlobalTest { DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS)); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT) + public void test_mapPrivateEventCommittedStateChanged_flagEnabled() { + // Test public flags mapping + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED)); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT) + public void test_mapPrivateEventCommittedStateChanged_flagDisabled() { + // Test public flags mapping + assertEquals(0, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED)); + } + private void waitForHandler() { mHandler.runWithScissors(() -> { }, 0); 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/keystore/java/android/security/GateKeeper.java b/keystore/java/android/security/GateKeeper.java index 464714fe2895..c2792e1f2394 100644 --- a/keystore/java/android/security/GateKeeper.java +++ b/keystore/java/android/security/GateKeeper.java @@ -28,7 +28,7 @@ import android.service.gatekeeper.IGateKeeperService; * * @hide */ -public abstract class GateKeeper { +public final class GateKeeper { public static final long INVALID_SECURE_USER_ID = 0; diff --git a/keystore/java/android/security/keystore/ArrayUtils.java b/keystore/java/android/security/keystore/ArrayUtils.java index f22b6041800f..6472ca9957d0 100644 --- a/keystore/java/android/security/keystore/ArrayUtils.java +++ b/keystore/java/android/security/keystore/ArrayUtils.java @@ -23,7 +23,7 @@ import java.util.function.Consumer; /** * @hide */ -public abstract class ArrayUtils { +public final class ArrayUtils { private ArrayUtils() {} public static String[] nullToEmpty(String[] array) { diff --git a/keystore/java/android/security/keystore/Utils.java b/keystore/java/android/security/keystore/Utils.java index e58b1ccb5370..c38ce8e86a15 100644 --- a/keystore/java/android/security/keystore/Utils.java +++ b/keystore/java/android/security/keystore/Utils.java @@ -23,7 +23,7 @@ import java.util.Date; * * @hide */ -abstract class Utils { +public final class Utils { private Utils() {} static Date cloneIfNotNull(Date value) { diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java index 1394bd443f03..9d306ce1ed38 100644 --- a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java +++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java @@ -38,7 +38,9 @@ import java.util.function.Consumer; /** * @hide */ -public abstract class KeyStore2ParameterUtils { +public final class KeyStore2ParameterUtils { + + private KeyStore2ParameterUtils() {} /** * This function constructs a {@link KeyParameter} expressing a boolean value. diff --git a/keystore/java/android/security/keystore2/KeymasterUtils.java b/keystore/java/android/security/keystore2/KeymasterUtils.java index 614e3684c417..02f3f578d03e 100644 --- a/keystore/java/android/security/keystore2/KeymasterUtils.java +++ b/keystore/java/android/security/keystore2/KeymasterUtils.java @@ -16,13 +16,10 @@ package android.security.keystore2; -import android.security.keymaster.KeymasterArguments; import android.security.keymaster.KeymasterDefs; -import android.security.keystore.KeyProperties; import java.security.AlgorithmParameters; import java.security.NoSuchAlgorithmException; -import java.security.ProviderException; import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.InvalidParameterSpecException; @@ -30,7 +27,7 @@ import java.security.spec.InvalidParameterSpecException; /** * @hide */ -public abstract class KeymasterUtils { +public final class KeymasterUtils { private KeymasterUtils() {} @@ -86,47 +83,6 @@ public abstract class KeymasterUtils { } } - /** - * Adds {@code KM_TAG_MIN_MAC_LENGTH} tag, if necessary, to the keymaster arguments for - * generating or importing a key. This tag may only be needed for symmetric keys (e.g., HMAC, - * AES-GCM). - */ - public static void addMinMacLengthAuthorizationIfNecessary(KeymasterArguments args, - int keymasterAlgorithm, - int[] keymasterBlockModes, - int[] keymasterDigests) { - switch (keymasterAlgorithm) { - case KeymasterDefs.KM_ALGORITHM_AES: - if (com.android.internal.util.ArrayUtils.contains( - keymasterBlockModes, KeymasterDefs.KM_MODE_GCM)) { - // AES GCM key needs the minimum length of AEAD tag specified. - args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, - AndroidKeyStoreAuthenticatedAESCipherSpi.GCM - .MIN_SUPPORTED_TAG_LENGTH_BITS); - } - break; - case KeymasterDefs.KM_ALGORITHM_HMAC: - // HMAC key needs the minimum length of MAC set to the output size of the associated - // digest. This is because we do not offer a way to generate shorter MACs and - // don't offer a way to verify MACs (other than by generating them). - if (keymasterDigests.length != 1) { - throw new ProviderException( - "Unsupported number of authorized digests for HMAC key: " - + keymasterDigests.length - + ". Exactly one digest must be authorized"); - } - int keymasterDigest = keymasterDigests[0]; - int digestOutputSizeBits = getDigestOutputSizeBits(keymasterDigest); - if (digestOutputSizeBits == -1) { - throw new ProviderException( - "HMAC key authorized for unsupported digest: " - + KeyProperties.Digest.fromKeymaster(keymasterDigest)); - } - args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, digestOutputSizeBits); - break; - } - } - static String getEcCurveFromKeymaster(int ecCurve) { switch (ecCurve) { case android.hardware.security.keymint.EcCurve.P_224: diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index ab2f3ef94eb6..68970e68de07 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -3,5 +3,5 @@ pbdr@google.com pragyabajoria@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com, peanutbutter@google.com, jeremysim@google.com per-file res*/*/tv_*.xml = bronger@google.com 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/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 00901a4d980d..00c446c3da60 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -27,6 +27,7 @@ import android.hardware.display.DisplayManager; import android.os.SystemProperties; import android.view.Display; import android.view.WindowManager; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -271,7 +272,7 @@ public class DesktopModeStatus { * frontend implementations). */ public static boolean enableMultipleDesktops(@NonNull Context context) { - return Flags.enableMultipleDesktopsBackend() + return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() && Flags.enableMultipleDesktopsFrontend() && canEnterDesktopMode(context); } 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/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 7d80ee5f3bb6..f8b18f29c797 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -53,6 +53,7 @@ import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.pip2.phone.PipUiStateChangeController; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -85,11 +86,13 @@ public abstract class Pip2Module { @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, displayController, pipDesktopState); + pipUiStateChangeController, displayController, splitScreenControllerOptional, + pipDesktopState); } @WMSingleton @@ -140,9 +143,10 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - pipDesktopState); + splitScreenControllerOptional, pipDesktopState); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 6f455df6cfec..c38558d7bde9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -26,6 +26,7 @@ import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERN import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE +import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags @@ -62,7 +63,7 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { desktopTasksController.onDeskRemovedListener = this } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 03bc42f08d59..0cc8a6a5c1a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import com.android.window.flags.Flags +import android.window.DesktopExperienceFlags import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -56,7 +56,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN) } if (args.size < 3) { @@ -95,7 +95,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -116,7 +116,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -137,7 +137,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -158,7 +158,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -167,7 +167,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -188,7 +188,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -204,12 +204,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.moveToFullscreen(taskId, transitionSource = UNKNOWN) + return true } private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -225,7 +225,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -246,7 +246,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("$prefix moveTaskToDesk <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") pw.println("$prefix moveToNextDisplay <taskId> ") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index aecbf1a23cb2..99f052832a51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -214,6 +214,13 @@ public class DesktopModeVisualIndicator { return result; } + /** + * Returns the [DragStartState] of the visual indicator. + */ + DragStartState getDragStartState() { + return mDragStartState; + } + @VisibleForTesting Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) { final Region region = new Region(); 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 4777e7f93bc9..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 @@ -22,6 +22,7 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.core.util.forEach import androidx.core.util.valueIterator @@ -137,7 +138,7 @@ class DesktopRepository( private var desktopGestureExclusionExecutor: Executor? = null private val desktopData: DesktopData = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { MultiDesktopData() } else { SingleDesktopData() @@ -226,10 +227,19 @@ class DesktopRepository( desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } + /** Sets the given desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) { + desktopData.setDeskInactive(deskId) + } + /** Returns the id of the active desk in the given display, if any. */ @VisibleForTesting fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId + /** Returns the id of the desk to which this task belongs. */ + fun getDeskIdForTask(taskId: Int): Int? = + desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId + /** * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk. * @@ -270,20 +280,40 @@ class DesktopRepository( @VisibleForTesting fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) { val affectedDisplays = mutableSetOf<Int>() - desktopData.forAllDesks { displayId, desk -> - if (desk.deskId != excludedDeskId && desk.activeTasks.remove(taskId)) { - logD( - "Removed active task=%d displayId=%d deskId=%d", - taskId, - displayId, - desk.deskId, - ) - affectedDisplays.add(displayId) + desktopData + .desksSequence() + .filter { desk -> desk.displayId != excludedDeskId } + .forEach { desk -> + val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false) + if (removed) { + logD( + "Removed active task=%d displayId=%d deskId=%d", + taskId, + desk.displayId, + desk.deskId, + ) + affectedDisplays.add(desk.displayId) + } } - } affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) } } + private fun removeActiveTaskFromDesk( + deskId: Int, + taskId: Int, + notifyListeners: Boolean = true, + ): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + if (desk.activeTasks.remove(taskId)) { + logD("Removed active task=%d from deskId=%d", taskId, desk.deskId) + if (notifyListeners) { + updateActiveTasksListeners(desk.displayId) + } + return true + } + return false + } + /** * Adds given task to the closing task list for [displayId]'s active desk. * @@ -322,10 +352,22 @@ class DesktopRepository( fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks } + @VisibleForTesting + fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.activeTasks + } + fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks } fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks } + @VisibleForTesting + fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.visibleTasks + } + fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks } /** @@ -415,12 +457,19 @@ class DesktopRepository( /** Removes task from visible tasks of all desks except [excludedDeskId]. */ private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { desktopData.forAllDesks { displayId, desk -> - if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners(displayId, desk.visibleTasks.size) + if (desk.deskId != excludedDeskId) { + removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId) } } } + private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) { + val desk = desktopData.getDesk(deskId) ?: return + if (desk.visibleTasks.remove(taskId)) { + notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size) + } + } + /** * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners. * @@ -576,15 +625,26 @@ class DesktopRepository( /** * Set whether the given task is the full-immersive task in this display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with + * an explicit desk id instead of using this function and defaulting to the active one. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { - val desktopData = desktopData.getActiveDesk(displayId) ?: return + val activeDesk = desktopData.getActiveDesk(displayId) ?: return + setTaskInFullImmersiveStateInDesk( + deskId = activeDesk.deskId, + taskId = taskId, + immersive = immersive, + ) + } + + /** Sets whether the given task is the full-immersive task in the given desk. */ + fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) { + val desk = desktopData.getDesk(deskId) ?: return if (immersive) { - desktopData.fullImmersiveTaskId = taskId + desk.fullImmersiveTaskId = taskId } else { - if (desktopData.fullImmersiveTaskId == taskId) { - desktopData.fullImmersiveTaskId = null + if (desk.fullImmersiveTaskId == taskId) { + desk.fullImmersiveTaskId = null } } } @@ -674,7 +734,8 @@ class DesktopRepository( /** * Minimizes the task for [taskId] and [displayId]'s active display. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit + * desk id instead of using this function and defaulting to the active one. */ fun minimizeTask(displayId: Int, taskId: Int) { if (displayId == INVALID_DISPLAY) { @@ -683,32 +744,41 @@ class DesktopRepository( getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId) return - } else { - logD("Minimize Task: display=%d, task=%d", displayId, taskId) - desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId) - ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) } - updateTask(displayId, taskId, isVisible = false) + val deskId = desktopData.getActiveDesk(displayId)?.deskId + if (deskId == null) { + logD("Minimize task: No active desk found for task: taskId=%d", taskId) + return + } + minimizeTaskInDesk(displayId, deskId, taskId) + } + + /** Minimizes the task in its desk. */ + @VisibleForTesting + fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { + logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) + desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) + ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) + updateTaskInDesk(displayId, deskId, taskId, isVisible = false) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + updatePersistentRepositoryForDesk(deskId) } } /** * Unminimizes the task for [taskId] and [displayId]. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead. */ fun unminimizeTask(displayId: Int, taskId: Int) { logD("Unminimize Task: display=%d, task=%d", displayId, taskId) - var removed = false - desktopData.forAllDesks(displayId) { desk -> - if (desk.minimizedTasks.remove(taskId)) { - removed = true - } - } - if (!removed) { - logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId) + desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) } + } + + private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) { + logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId) + if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) { + logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId) } } @@ -729,7 +799,7 @@ class DesktopRepository( * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id * will be looked up from the task id. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [removeTaskFromDesk] instead. */ fun removeTask(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d", taskId) @@ -745,24 +815,33 @@ class DesktopRepository( private fun removeTaskFromDisplay(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId) desktopData.forAllDesks(displayId) { desk -> - if (desk.freeformTasksInZOrder.remove(taskId)) { - logD( - "Remaining freeform tasks in desk: %d, tasks: %s", - desk.deskId, - desk.freeformTasksInZOrder.toDumpString(), - ) - } + removeTaskFromDesk(deskId = desk.deskId, taskId = taskId) } + } + + /** Removes the given task from the given desk. */ + fun removeTaskFromDesk(deskId: Int, taskId: Int) { + 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). boundsBeforeMaximizeByTaskId.remove(taskId) boundsBeforeFullImmersiveByTaskId.remove(taskId) - // Remove task from unminimized task if it is minimized. - unminimizeTask(displayId, taskId) + val desk = desktopData.getDesk(deskId) ?: return + if (desk.freeformTasksInZOrder.remove(taskId)) { + logD( + "Remaining freeform tasks in desk: %d, tasks: %s", + desk.deskId, + desk.freeformTasksInZOrder.toDumpString(), + ) + } + unminimizeTaskFromDesk(deskId, taskId) // Mark task as not in immersive if it was immersive. - setTaskInFullImmersiveState(displayId = displayId, taskId = taskId, immersive = false) - removeActiveTask(taskId) - removeVisibleTask(taskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false) + removeActiveTaskFromDesk(deskId = deskId, taskId = taskId) + removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + updatePersistentRepositoryForDesk(desk.deskId) } } @@ -832,24 +911,29 @@ class DesktopRepository( private fun updatePersistentRepository(displayId: Int) { val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList() mainCoroutineScope.launch { - desks.forEach { desk -> - try { - persistentRepository.addOrUpdateDesktop( - // Use display id as desk id for now since only once desk per display - // is supported. - userId = userId, - desktopId = desk.deskId, - visibleTasks = desk.visibleTasks, - minimizedTasks = desk.minimizedTasks, - freeformTasksInZOrder = desk.freeformTasksInZOrder, - ) - } catch (exception: Exception) { - logE( - "An exception occurred while updating the persistent repository \n%s", - exception.stackTrace, - ) - } - } + desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) } + } + } + + private fun updatePersistentRepositoryForDesk(deskId: Int) { + val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return + mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) } + } + + private suspend fun updatePersistentRepositoryForDesk(desk: Desk) { + try { + persistentRepository.addOrUpdateDesktop( + userId = userId, + desktopId = desk.deskId, + visibleTasks = desk.visibleTasks, + minimizedTasks = desk.minimizedTasks, + freeformTasksInZOrder = desk.freeformTasksInZOrder, + ) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace, + ) } } @@ -866,21 +950,27 @@ class DesktopRepository( desktopData .desksSequence() .groupBy { it.displayId } - .forEach { (displayId, desks) -> + .map { (displayId, desks) -> + Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks) + } + .forEach { (displayId, activeDeskId, desks) -> pw.println("${prefix}Display #$displayId:") + pw.println("${innerPrefix}activeDesk=$activeDeskId") + pw.println("${innerPrefix}desks:") + val desksPrefix = "$innerPrefix " desks.forEach { desk -> - pw.println("${innerPrefix}Desk #${desk.deskId}:") - pw.print("$innerPrefix activeTasks=") + pw.println("${desksPrefix}Desk #${desk.deskId}:") + pw.print("$desksPrefix activeTasks=") pw.println(desk.activeTasks.toDumpString()) - pw.print("$innerPrefix visibleTasks=") + pw.print("$desksPrefix visibleTasks=") pw.println(desk.visibleTasks.toDumpString()) - pw.print("$innerPrefix freeformTasksInZOrder=") + pw.print("$desksPrefix freeformTasksInZOrder=") pw.println(desk.freeformTasksInZOrder.toDumpString()) - pw.print("$innerPrefix minimizedTasks=") + pw.print("$desksPrefix minimizedTasks=") pw.println(desk.minimizedTasks.toDumpString()) - pw.print("$innerPrefix fullImmersiveTaskId=") + pw.print("$desksPrefix fullImmersiveTaskId=") pw.println(desk.fullImmersiveTaskId) - pw.print("$innerPrefix topTransparentFullscreenTaskId=") + pw.print("$desksPrefix topTransparentFullscreenTaskId=") pw.println(desk.topTransparentFullscreenTaskId) } } @@ -910,6 +1000,9 @@ class DesktopRepository( /** Sets the given desk as the active desk in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) + /** Sets the desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) + /** * Returns the default desk in the given display. Useful when the system wants to activate a * desk but doesn't care about which one it activates (e.g. when putting a window into a @@ -990,6 +1083,11 @@ class DesktopRepository( // existence of visible desktop windows, among other factors. } + override fun setDeskInactive(deskId: Int) { + // No-op, in single-desk setups, which desktop is "active" is determined by the + // existence of visible desktop windows, among other factors. + } + override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId) override fun getAllActiveDesks(): Set<Desk> = @@ -1058,6 +1156,14 @@ class DesktopRepository( display.activeDeskId = desk.deskId } + override fun setDeskInactive(deskId: Int) { + desktopDisplays.forEach { id, display -> + if (display.activeDeskId == deskId) { + display.activeDeskId = null + } + } + } + override fun getDefaultDesk(displayId: Int): Desk? { val display = desktopDisplays[displayId] ?: return null return display.orderedDesks.find { it.deskId == display.activeDeskId } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index 4d87b2189115..e831d5eecdc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -42,6 +42,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + // TODO: b/394281403 - with multiple desks, it's possible to have a non-freeform task + // inside a desk, so this should be decoupled from windowing mode. + // Also, changes in/out of desks are handled by the [DesksTransitionObserver], which has + // more specific information about the desk involved in the transition, which might be + // more accurate than assuming it's always the default/active desk in the display, as this + // method does. // Case 1: Freeform task is changed in Desktop Mode. if (isFreeformTask(taskInfo)) { if (taskInfo.isVisible) { 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 522d83ec50eb..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 @@ -41,6 +41,7 @@ import android.os.Handler import android.os.IBinder import android.os.SystemProperties import android.os.UserHandle +import android.util.Slog import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.MotionEvent @@ -53,6 +54,7 @@ import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_FRONT import android.widget.Toast +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER @@ -151,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, @@ -428,7 +440,7 @@ class DesktopTasksController( /** Creates a new desk in the given display. */ fun createDesk(displayId: Int) { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksOrganizer.createDesk(displayId) { deskId -> taskRepository.addDesk(displayId = displayId, deskId = deskId) } @@ -479,7 +491,7 @@ class DesktopTasksController( ): Boolean { val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) if (runningTask != null) { - moveRunningTaskToDesk( + return moveRunningTaskToDesk( task = runningTask, deskId = deskId, wct = wct, @@ -561,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( @@ -607,7 +619,7 @@ class DesktopTasksController( addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActiveDeskWithTask( token = transition, @@ -619,6 +631,7 @@ class DesktopTasksController( } else { taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) } + return true } /** @@ -635,7 +648,7 @@ class DesktopTasksController( task: RunningTaskInfo, ): Int? { val taskIdToMinimize = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // Activate the desk first. prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) @@ -655,7 +668,7 @@ class DesktopTasksController( // Bring other apps to front first. bringDesktopAppsToFrontBeforeShowingNewTask(displayId, wct, task.taskId) } - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { prepareMoveTaskToDesk(wct, task, deskId) } else { addMoveToDesktopChanges(wct, task) @@ -714,7 +727,7 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so // this shouldn't be necessary at all. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { @@ -746,7 +759,7 @@ class DesktopTasksController( addPendingMinimizeTransition(it, taskId, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActiveDeskWithTask( token = transition, @@ -787,22 +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) - 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) { @@ -836,10 +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) - 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( @@ -861,6 +906,7 @@ class DesktopTasksController( ) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + desktopExitRunnable?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ @@ -908,7 +954,8 @@ class DesktopTasksController( ) { logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true) + 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)) { @@ -916,12 +963,14 @@ class DesktopTasksController( wct.reorder(task.token, /* onTop= */ true) } - exitDesktopTaskTransitionHandler.startTransition( - transitionSource, - wct, - position, - mOnAnimationFinishedCallback, - ) + val transition = + exitDesktopTaskTransitionHandler.startTransition( + transitionSource, + wct, + position, + mOnAnimationFinishedCallback, + ) + deactivationRunnable?.invoke(transition) // handles case where we are moving to full screen without closing all DW tasks. if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) { @@ -1180,6 +1229,8 @@ class DesktopTasksController( wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) } + // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going + // home. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { performDesktopExitCleanupIfNeeded( task.taskId, @@ -1543,7 +1594,7 @@ class DesktopTasksController( private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) { // Move home to front, ensures that we go back home when all desktop windows are closed val useParamDisplayId = - Flags.enableMultipleDesktopsBackend() || + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || Flags.enablePerDisplayDesktopWallpaperActivity() moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct) // Currently, we only handle the desktop on the default display really. @@ -1726,33 +1777,59 @@ class DesktopTasksController( } } - /** - * Remove wallpaper activity if task provided is last task and wallpaper activity token is not - * null - */ - private fun performDesktopExitCleanupIfNeeded( - taskId: Int, + private fun willExitDesktop( + triggerTaskId: Int, displayId: Int, - wct: WindowContainerTransaction, forceToFullscreen: Boolean, - shouldEndUpAtHome: Boolean = true, - ) { - taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) + ): Boolean { if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) { + return false } } else if ( Flags.enableDesktopWindowingPip() && taskRepository.isMinimizedPipPresentInDisplay(displayId) && !forceToFullscreen ) { - return + return false } else { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) { + return false } } + return true + } + + private fun performDesktopExitCleanupIfNeeded( + taskId: Int, + displayId: Int, + wct: WindowContainerTransaction, + forceToFullscreen: Boolean, + shouldEndUpAtHome: Boolean = true, + ): RunOnTransitStart? { + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen) + if (!willExitDesktop(taskId, displayId, forceToFullscreen)) { + return null + } + // 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 ) @@ -1762,6 +1839,7 @@ class DesktopTasksController( // intent. addLaunchHomePendingIntent(wct, displayId) } + return prepareDeskDeactivationIfNeeded(wct, deskId) } fun releaseVisualIndicator() { @@ -2091,7 +2169,16 @@ class DesktopTasksController( ): WindowContainerTransaction? { logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch") val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) wct.reorder(task.token, true) return wct } @@ -2115,7 +2202,16 @@ class DesktopTasksController( // launched. We should make this task go to fullscreen instead of freeform. Note // that this means any re-launch of a freeform window outside of desktop will be in // fullscreen as long as default-desktop flag is disabled. - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) return wct } bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) @@ -2211,7 +2307,16 @@ class DesktopTasksController( // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED // set when needed can interfere with future split / multi-instance transitions. return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) } } return null @@ -2239,10 +2344,25 @@ class DesktopTasksController( } // Already fullscreen, no-op. if (task.isFullscreen) return null - return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) + } } - /** Handle task closing by removing wallpaper activity if it's the last active task */ + /** + * Handle task closing by removing wallpaper activity if it's the last active task. + * + * TODO: b/394268248 - desk needs to be deactivated. + */ private fun handleTaskClosing( task: RunningTaskInfo, transition: IBinder, @@ -2309,7 +2429,7 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, deskId: Int, ) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return val displayId = taskRepository.getDisplayForDesk(deskId) val displayLayout = displayController.getDisplayLayout(displayId) ?: return val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId) @@ -2393,10 +2513,15 @@ class DesktopTasksController( return bounds } + /** + * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to + * be deactivated. + */ private fun addMoveToFullscreenChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, - ) { + willExitDesktop: Boolean, + ): RunOnTransitStart? { val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -2411,12 +2536,16 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - - performDesktopExitCleanupIfNeeded( - taskInfo.taskId, - taskInfo.displayId, - wct, - forceToFullscreen = true, + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true) + } + taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false) + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + return performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = taskInfo.displayId, + willExitDesktop = willExitDesktop, shouldEndUpAtHome = false, ) } @@ -2441,6 +2570,8 @@ class DesktopTasksController( /** * Adds split screen changes to a transaction. Note that bounds are not reset here due to * animation; see {@link onDesktopSplitSelectAnimComplete} + * + * TODO: b/394268248 - desk needs to be deactivated. */ private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { // This windowing mode is to get the transition animation started; once we complete @@ -2541,7 +2672,7 @@ class DesktopTasksController( fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { val displayId = taskRepository.getDisplayForDesk(deskId) val wct = WindowContainerTransaction() - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { @@ -2562,7 +2693,7 @@ class DesktopTasksController( val transition = transitions.startTransition(transitionType, wct, handler) handler?.setTransition(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActivateDesk( token = transition, @@ -2577,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) { @@ -2598,7 +2746,7 @@ class DesktopTasksController( logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) val tasksToRemove = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { taskRepository.getActiveTaskIdsInDesk(deskId) } else { // TODO: 362720497 - make sure minimized windows are also removed in WM @@ -2607,7 +2755,7 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { tasksToRemove.forEach { val task = shellTaskOrganizer.getRunningTaskInfo(it) if (task != null) { @@ -2620,9 +2768,9 @@ class DesktopTasksController( // TODO: 362720497 - double check background tasks are also removed. desksOrganizer.removeDesk(wct, deskId) } - if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.RemoveDesk( token = transition, @@ -2743,6 +2891,12 @@ class DesktopTasksController( taskTop: Float, dragStartState: DragStartState, ): DesktopModeVisualIndicator.IndicatorType { + // If the visual indicator has the wrong start state, it was never cleared from a previous + // drag event and needs to be cleared + if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) { + Slog.e(TAG, "Visual indicator from previous motion event was never released") + releaseVisualIndicator() + } // If the visual indicator does not exist, create it. val indicator = visualIndicator 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/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 5ae1fca73d4e..95cc1e68ac11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -106,7 +106,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH * @param position Position of the task when transition is started * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, + public IBinder startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, Function0<Unit> onAnimationEndCallback) { mPosition = position; @@ -114,6 +114,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), wct, this); mPendingTransitionTokens.add(token); + return token; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt index 8c4fd9db050f..9dec96933ee5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -42,4 +42,7 @@ sealed class DeskTransition { val deskId: Int, val enterTaskId: Int, ) : DeskTransition() + + /** A transition to deactivate a desk. */ + data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 547890a6200a..0f2f3711a9a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -27,6 +27,9 @@ interface DesksOrganizer { /** Activates the given desk, making it visible in its display. */ fun activateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Deactivates the given desk, removing it as the default launch container for new tasks. */ + fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Removes the given desk and its desktop windows. */ fun removeDesk(wct: WindowContainerTransaction, deskId: Int) @@ -37,6 +40,9 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Whether the change is for the given desk id. */ + fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean + /** * Returns the desk id in which the task in the given change is located at the end of a * transition, if any. 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 6d88c3310a63..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 @@ -17,9 +17,11 @@ package com.android.wm.shell.desktopmode.multidesks import android.os.IBinder import android.view.WindowManager.TRANSIT_CLOSE +import android.window.DesktopExperienceFlags import android.window.TransitionInfo -import com.android.window.flags.Flags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE /** * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It @@ -33,7 +35,7 @@ class DesksTransitionObserver( /** Adds a pending desk transition to be tracked. */ fun addPendingTransition(transition: DeskTransition) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return deskTransitions[transition.token] = transition } @@ -42,8 +44,9 @@ class DesksTransitionObserver( * observer. */ fun onTransitionReady(transition: IBinder, info: TransitionInfo) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return val deskTransition = deskTransitions.remove(transition) ?: return + logD("Desk transition ready: %s", deskTransition) val desktopRepository = desktopUserRepositories.current when (deskTransition) { is DeskTransition.RemoveDesk -> { @@ -88,6 +91,42 @@ class DesksTransitionObserver( ) } } + is DeskTransition.DeactivateDesk -> { + var visibleDeactivation = false + for (change in info.changes) { + val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) + if (isDeskChange) { + visibleDeactivation = true + continue + } + val taskId = change.taskInfo?.taskId ?: continue + val removedFromDesk = + desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && + desksOrganizer.getDeskAtEnd(change) == null + if (removedFromDesk) { + desktopRepository.removeTaskFromDesk( + deskId = deskTransition.deskId, + taskId = taskId, + ) + } + } + // 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) + } } } + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + private const val TAG = "DesksTransitionObserver" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 5cda76e2f3e0..339932cabd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -23,12 +23,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.DesktopExperienceFlags import android.window.TransitionInfo import android.window.WindowContainerTransaction import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -47,7 +47,7 @@ class RootTaskDesksOrganizer( @VisibleForTesting val roots = SparseArray<DeskRoot>() init { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { shellInit.addInitCallback( { shellCommandHandler.addDumpCallback(this::dump, this) }, this, @@ -83,6 +83,16 @@ class RootTaskDesksOrganizer( ) } + override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("deactivateDesk %d", deskId) + val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ null, + /* activityTypes= */ null, + ) + } + override fun moveTaskToDesk( wct: WindowContainerTransaction, deskId: Int, @@ -93,6 +103,9 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = + roots.contains(deskId) && change.taskInfo?.taskId == deskId + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = change.taskInfo?.parentTaskId?.takeIf { it in roots } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 5a89451ffdbc..0507e59c06e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.shared.annotations.ShellMainThread @@ -58,7 +58,7 @@ class DesktopRepositoryInitializerImpl( repository.addDesk( displayId = persistentDesktop.displayId, deskId = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { persistentDesktop.desktopId } else { // When disabled, desk ids are always the display id. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index da3181096d98..cef18f55b86d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -145,7 +145,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start an exit-via-expand from Pip transition/animation. */ - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { // Default implementation does nothing. } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index e17587ff18bc..df7a25af8376 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -35,6 +35,10 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import java.util.Optional; /** * Scheduler for Shell initiated PiP transitions and animations. @@ -47,6 +51,7 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private final PipDesktopState mPipDesktopState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; @@ -59,12 +64,14 @@ public class PipScheduler { PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; mPipDesktopState = pipDesktopState; + mSplitScreenControllerOptional = splitScreenControllerOptional; mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); @@ -96,10 +103,23 @@ public class PipScheduler { public void scheduleExitPipViaExpand() { mMainExecutor.execute(() -> { if (!mPipTransitionState.isInPip()) return; - WindowContainerTransaction wct = getExitPipViaExpandTransaction(); - if (wct != null) { - mPipTransitionController.startExpandTransition(wct); - } + + final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction(); + if (expandWct == null) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSplitScreenControllerOptional.ifPresent(splitScreenController -> { + int lastParentTaskId = mPipTransitionState.getPipTaskInfo() + .lastParentTaskIdBeforePip; + if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) { + splitScreenController.prepareEnterSplitScreen(wct, + null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED); + } + }); + + boolean toSplit = !wct.isEmpty(); + wct.merge(expandWct, true /* transfer */); + mPipTransitionController.startExpandTransition(wct, toSplit); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 9adaa3614a0f..e7bffe3bc4bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip2.phone; -import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Surface.ROTATION_0; @@ -29,7 +28,13 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; import static com.android.wm.shell.transition.Transitions.transitTypeToString; @@ -45,7 +50,6 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -70,11 +74,14 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; -import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.transition.PipExpandHandler; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + /** * Implementation of transitions for PiP on phone. */ @@ -130,6 +137,7 @@ public class PipTransition extends PipTransitionController implements // // Internal state and relevant cached info // + private final PipExpandHandler mExpandHandler; private Transitions.TransitionFinishCallback mFinishCallback; @@ -151,6 +159,7 @@ public class PipTransition extends PipTransitionController implements PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -165,6 +174,9 @@ public class PipTransition extends PipTransitionController implements mDisplayController = displayController; mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); mPipDesktopState = pipDesktopState; + + mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm, + pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional); } @Override @@ -184,10 +196,11 @@ public class PipTransition extends PipTransitionController implements // @Override - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { if (out == null) return; mPipTransitionState.setState(PipTransitionState.EXITING_PIP); - mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT + : TRANSIT_EXIT_PIP, out, this); } @Override @@ -239,10 +252,11 @@ public class PipTransition extends PipTransitionController implements @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Just jump-cut the current animation if any, but do not merge. if (info.getType() == TRANSIT_EXIT_PIP) { end(); } + mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); } @Override @@ -290,7 +304,8 @@ public class PipTransition extends PipTransitionController implements finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; - return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); + return mExpandHandler.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); @@ -436,7 +451,7 @@ public class PipTransition extends PipTransitionController implements (destinationBounds.height() - overlaySize) / 2f); } - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -496,7 +511,7 @@ public class PipTransition extends PipTransitionController implements final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange); - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -585,27 +600,6 @@ public class PipTransition extends PipTransitionController implements endBounds.top + activityEndOffset.y); } - private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { - final Rect endBounds = outPipTaskChange.getEndAbsBounds(); - final int width = endBounds.width(); - final int height = endBounds.height(); - final int left = endBounds.left; - final int top = endBounds.top; - int newTop, newLeft; - - if (delta == Surface.ROTATION_90) { - newLeft = top; - newTop = -(left + width); - } else { - newLeft = -(height + top); - newTop = left; - } - // Modify the endBounds, rotating and placing them potentially off-screen, so that - // as we translate and rotate around the origin, we place them right into the target. - endBounds.set(newLeft, newTop, newLeft + height, newTop + width); - } - - private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -633,83 +627,6 @@ public class PipTransition extends PipTransitionController implements return true; } - private boolean startExpandAnimation(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); - - TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); - if (pipChange == null) { - // pipChange is null, check to see if we've reparented the PIP activity for - // the multi activity case. If so we should use the activity leash instead - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() == null - && change.getLastParent() != null - && change.getLastParent().equals(pipToken)) { - pipChange = change; - break; - } - } - - // failsafe - if (pipChange == null) { - return false; - } - } - mFinishCallback = finishCallback; - - // The parent change if we were in a multi-activity PiP; null if single activity PiP. - final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null - ? getChangeByToken(info, pipChange.getParent()) : null; - if (parentBeforePip != null) { - // For multi activity, we need to manually set the leash layer - startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); - } - - final Rect startBounds = pipChange.getStartAbsBounds(); - final Rect endBounds = pipChange.getEndAbsBounds(); - final SurfaceControl pipLeash = getLeash(pipChange); - - PictureInPictureParams params = null; - if (pipChange.getTaskInfo() != null) { - // single activity - params = getPipParams(pipChange); - } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { - // multi activity - params = getPipParams(parentBeforePip); - } - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, - startBounds); - - // We define delta = startRotation - endRotation, so we need to flip the sign. - final int delta = -getFixedRotationDelta(info, pipChange); - if (delta != ROTATION_0) { - // Update PiP target change in place to prepare for fixed rotation; - handleExpandFixedRotation(pipChange, delta); - } - - PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, startBounds, endBounds, - sourceRectHint, delta); - animator.setAnimationEndCallback(() -> { - if (parentBeforePip != null) { - // TODO b/377362511: Animate local leash instead to also handle letterbox case. - // For multi-activity, set the crop to be null - finishTransaction.setCrop(pipLeash, null); - } - finishTransition(); - }); - cacheAndStartTransitionAnimator(animator); - - // Save the PiP bounds in case, we re-enter the PiP with the same component. - float snapFraction = mPipBoundsAlgorithm.getSnapFraction( - mPipBoundsState.getBounds()); - mPipBoundsState.saveReentryState(snapFraction); - - return true; - } - private boolean startRemoveAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -743,29 +660,6 @@ public class PipTransition extends PipTransitionController implements // Various helpers to resolve transition requests and infos // - @Nullable - private TransitionInfo.Change getPipChange(TransitionInfo info) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { - return change; - } - } - return null; - } - - @Nullable - private TransitionInfo.Change getChangeByToken(TransitionInfo info, - WindowContainerToken token) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getToken().equals(token)) { - return change; - } - } - return null; - } - @NonNull private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change pipTaskChange, @@ -789,8 +683,8 @@ public class PipTransition extends PipTransitionController implements Rect cutoutInsets = parentBeforePip != null ? parentBeforePip.getTaskInfo().displayCutoutInsets : pipTaskChange.getTaskInfo().displayCutoutInsets; - if (cutoutInsets != null - && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) { + if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange, + mPipDisplayLayoutState) == ROTATION_90) { adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top); } if (mPipDesktopState.isDesktopWindowingPipEnabled()) { @@ -807,25 +701,6 @@ public class PipTransition extends PipTransitionController implements return adjustedSourceRectHint; } - @Surface.Rotation - private int getFixedRotationDelta(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change pipChange) { - TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - int startRotation = pipChange.getStartRotation(); - if (pipChange.getEndRotation() != ROTATION_UNDEFINED - && startRotation != pipChange.getEndRotation()) { - // If PiP change was collected along with the display change and the orientation change - // happened in sync with the PiP change, then do not treat this as fixed-rotation case. - return ROTATION_0; - } - - int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); - int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; - return delta; - } - private void prepareOtherTargetTransforms(TransitionInfo info, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { @@ -1012,20 +887,6 @@ public class PipTransition extends PipTransitionController implements mTransitionAnimator.start(); } - @NonNull - private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { - return pipChange.getTaskInfo().pictureInPictureParams != null - ? pipChange.getTaskInfo().pictureInPictureParams - : new PictureInPictureParams.Builder().build(); - } - - @NonNull - private static SurfaceControl getLeash(TransitionInfo.Change change) { - SurfaceControl leash = change.getLeash(); - Preconditions.checkNotNull(leash, "Leash is null for change=" + change); - return leash; - } - // // Miscellaneous callbacks and listeners // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 8805cbb0dfbd..18c9a705dcf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -314,7 +314,8 @@ public class PipTransitionState { mSwipePipToHomeAppBounds.setEmpty(); } - @Nullable WindowContainerToken getPipTaskToken() { + @Nullable + public WindowContainerToken getPipTaskToken() { return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java new file mode 100644 index 000000000000..db4942b2fb95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java @@ -0,0 +1,331 @@ +/* + * 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.pip2.phone.transition; + +import static android.view.Surface.ROTATION_0; + +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLog; +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.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +public class PipExpandHandler implements Transitions.TransitionHandler { + private final Context mContext; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipTransitionState mPipTransitionState; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; + + @Nullable + private Transitions.TransitionFinishCallback mFinishCallback; + @Nullable + private ValueAnimator mTransitionAnimator; + + private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier; + + public PipExpandHandler(Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTransitionState pipTransitionState, + PipDisplayLayoutState pipDisplayLayoutState, + Optional<SplitScreenController> splitScreenControllerOptional) { + mContext = context; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipTransitionState = pipTransitionState; + mPipDisplayLayoutState = pipDisplayLayoutState; + mSplitScreenControllerOptional = splitScreenControllerOptional; + + mPipExpandAnimatorSupplier = PipExpandAnimator::new; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // All Exit-via-Expand from PiP transitions are Shell initiated. + return null; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + switch (info.getType()) { + case TRANSIT_EXIT_PIP: + return startExpandAnimation(info, startTransaction, finishTransaction, + finishCallback); + case TRANSIT_EXIT_PIP_TO_SPLIT: + return startExpandToSplitAnimation(info, startTransaction, finishTransaction, + finishCallback); + } + return false; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + + /** + * Ends the animation if such is running in the context of expanding out of PiP. + */ + public void end() { + if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) { + mTransitionAnimator.end(); + mTransitionAnimator = null; + } + } + + private boolean startExpandAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); + if (pipChange == null) { + // pipChange is null, check to see if we've reparented the PIP activity for + // the multi activity case. If so we should use the activity leash instead + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + + // failsafe + if (pipChange == null) { + return false; + } + } + mFinishCallback = finishCallback; + + // The parent change if we were in a multi-activity PiP; null if single activity PiP. + final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null + ? getChangeByToken(info, pipChange.getParent()) : null; + if (parentBeforePip != null) { + // For multi activity, we need to manually set the leash layer + startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); + } + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + final SurfaceControl pipLeash = getLeash(pipChange); + + PictureInPictureParams params = null; + if (pipChange.getTaskInfo() != null) { + // single activity + params = getPipParams(pipChange); + } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { + // multi activity + params = getPipParams(parentBeforePip); + } + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, + startBounds); + + // We define delta = startRotation - endRotation, so we need to flip the sign. + final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); + if (delta != ROTATION_0) { + // Update PiP target change in place to prepare for fixed rotation; + handleExpandFixedRotation(pipChange, delta); + } + + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + sourceRectHint, delta); + animator.setAnimationEndCallback(() -> { + if (parentBeforePip != null) { + // TODO b/377362511: Animate local leash instead to also handle letterbox case. + // For multi-activity, set the crop to be null + finishTransaction.setCrop(pipLeash, null); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP + // and the lastParentBeforePip is still in one of the split-stages. + // + // This means we should be animating the PiP activity leash, since we do the reparenting + // of the PiP activity back to its original task in startWCT. + TransitionInfo.Change pipChange = null; + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + // failsafe + if (pipChange == null || pipChange.getLeash() == null) { + return false; + } + mFinishCallback = finishCallback; + + // Get the original parent before PiP. If original task hosting the PiP activity was + // already visible, then it's not participating in this transition; in that case, + // parentBeforePip would be null. + final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent()); + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + if (parentBeforePip != null) { + // Since we have the parent task amongst the targets, all PiP activity + // leash translations will be relative to the original task, NOT the root leash. + startBounds.offset(-parentBeforePip.getStartAbsBounds().left, + -parentBeforePip.getStartAbsBounds().top); + endBounds.offset(-parentBeforePip.getEndAbsBounds().left, + -parentBeforePip.getEndAbsBounds().top); + } + + final SurfaceControl pipLeash = pipChange.getLeash(); + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + null /* srcRectHint */, ROTATION_0 /* delta */); + + + mSplitScreenControllerOptional.ifPresent(splitController -> { + splitController.finishEnterSplitScreen(finishTransaction); + }); + + animator.setAnimationEndCallback(() -> { + if (parentBeforePip == null) { + // After PipExpandAnimator is done modifying finishTransaction, we need to make + // sure PiP activity leash is offset at origin relative to its task as we reparent + // targets back from the transition root leash. + finishTransaction.setPosition(pipLeash, 0, 0); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private void finishTransition() { + final int currentState = mPipTransitionState.getState(); + if (currentState != PipTransitionState.EXITING_PIP) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Unexpected state %s as we are finishing an exit-via-expand transition", + mPipTransitionState); + } + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + + if (mFinishCallback != null) { + // Need to unset mFinishCallback first because onTransitionFinished can re-enter this + // handler if there is a pending PiP animation. + final Transitions.TransitionFinishCallback finishCallback = mFinishCallback; + mFinishCallback = null; + finishCallback.onTransitionFinished(null /* finishWct */); + } + } + + private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); + final int width = endBounds.width(); + final int height = endBounds.height(); + final int left = endBounds.left; + final int top = endBounds.top; + int newTop, newLeft; + + if (delta == Surface.ROTATION_90) { + newLeft = top; + newTop = -(left + width); + } else { + newLeft = -(height + top); + newTop = left; + } + // Modify the endBounds, rotating and placing them potentially off-screen, so that + // as we translate and rotate around the origin, we place them right into the target. + endBounds.set(newLeft, newTop, newLeft + height, newTop + width); + } + + private void saveReentryState() { + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsState.getBounds()); + mPipBoundsState.saveReentryState(snapFraction); + } + + private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) { + mTransitionAnimator = animator; + mTransitionAnimator.start(); + } + + @VisibleForTesting + interface PipExpandAnimatorSupplier { + PipExpandAnimator get(Context context, + @NonNull SurfaceControl leash, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + @Nullable Rect sourceRectHint, + @Surface.Rotation int rotation); + } + + @VisibleForTesting + void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) { + mPipExpandAnimatorSupplier = supplier; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java new file mode 100644 index 000000000000..01cda6c91108 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java @@ -0,0 +1,133 @@ +/* + * 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.pip2.phone.transition; + +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Surface.ROTATION_0; + +import android.annotation.NonNull; +import android.app.PictureInPictureParams; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; + +/** + * A set of utility methods to help resolve PiP transitions. + */ +public class PipTransitionUtils { + + /** + * @return change for a pinned mode task; null if no such task is in the list of changes. + */ + @Nullable + public static TransitionInfo.Change getPipChange(TransitionInfo info) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { + return change; + } + } + return null; + } + + /** + * @return change for a task with the provided token; null if no task with such token found. + */ + @Nullable + public static TransitionInfo.Change getChangeByToken(TransitionInfo info, + WindowContainerToken token) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getToken().equals(token)) { + return change; + } + } + return null; + } + + /** + * @return the leash to interact with the container this change represents. + * @throws NullPointerException if the leash is null. + */ + @NonNull + public static SurfaceControl getLeash(TransitionInfo.Change change) { + SurfaceControl leash = change.getLeash(); + Preconditions.checkNotNull(leash, "Leash is null for change=" + change); + return leash; + } + + /** + * Get the rotation delta in a potential fixed rotation transition. + * + * Whenever PiP participates in fixed rotation, its actual orientation isn't updated + * in the initial transition as per the async rotation convention. + * + * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already. + * @param pipDisplayLayoutState display layout state that PiP component keeps track of. + */ + @Surface.Rotation + public static int getFixedRotationDelta(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipChange, + @NonNull PipDisplayLayoutState pipDisplayLayoutState) { + TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + if (pipChange.getEndRotation() != ROTATION_UNDEFINED + && startRotation != pipChange.getEndRotation()) { + // If PiP change was collected along with the display change and the orientation change + // happened in sync with the PiP change, then do not treat this as fixed-rotation case. + return ROTATION_0; + } + + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation(); + int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + return delta; + } + + /** + * Gets a change amongst the transition targets that is in a different final orientation than + * the display, signalling a potential fixed rotation transition. + */ + @Nullable + public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + + /** + * @return {@link PictureInPictureParams} provided by the client from the PiP change. + */ + @NonNull + public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { + return pipChange.getTaskInfo().pictureInPictureParams != null + ? pipChange.getTaskInfo().pictureInPictureParams + : new PictureInPictureParams.Builder().build(); + } +} 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/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 8510441c0557..ed9b97d264f7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -55,6 +55,8 @@ import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -894,12 +896,12 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.isActiveTask(taskId)).isFalse() - assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) + assertThat(listener.activeChangesOnThirdDisplay).isEqualTo(2) } @Test @@ -917,7 +919,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { fun removeTask_updatesTaskVisibility() { repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY) val taskId = 1 - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) @@ -1106,6 +1108,30 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setTaskInFullImmersiveState_inDesk_savedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + assertThat(repo.isTaskInFullImmersiveState(6)).isFalse() + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskInFullImmersiveState_inDesk_removedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = false) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() { repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) @@ -1274,14 +1300,146 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8)) } + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskActive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskInactive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setDeskInactive(deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDeskIdForTask() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + assertThat(repo.getDeskIdForTask(10)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeMaximize() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeMaximize(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeMaximize(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeFullImmersive(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeFullImmersive(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromZOrderList() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getFreeformTasksIdsInDeskInZOrder(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromMinimized() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.minimizeTaskInDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getMinimizedTaskIdsInDesk(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromActiveTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isActiveTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromVisibleTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isVisibleTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeTaskFromDesk_updatesPersistence() = runTest { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + clearInvocations(persistentRepository) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + verify(persistentRepository) + .addOrUpdateDesktop( + userId = eq(DEFAULT_USER_ID), + desktopId = eq(6), + visibleTasks = any(), + minimizedTasks = any(), + freeformTasksInZOrder = any(), + ) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 + var activeChangesOnThirdDisplay = 0 override fun onActiveTasksChanged(displayId: Int) { when (displayId) { DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++ SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++ + THIRD_DISPLAY -> activeChangesOnThirdDisplay++ else -> fail("Active task listener received unexpected display id: $displayId") } } @@ -1290,9 +1448,11 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { class TestVisibilityListener : DesktopRepository.VisibleTasksListener { var visibleTasksCountOnDefaultDisplay = 0 var visibleTasksCountOnSecondaryDisplay = 0 + var visibleTasksCountOnThirdDisplay = 0 var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 + var visibleChangesOnThirdDisplay = 0 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { @@ -1304,6 +1464,10 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { visibleTasksCountOnSecondaryDisplay = visibleTasksCount visibleChangesOnSecondaryDisplay++ } + THIRD_DISPLAY -> { + visibleTasksCountOnThirdDisplay = visibleTasksCount + visibleChangesOnThirdDisplay++ + } else -> fail("Visible task listener received unexpected display id: $displayId") } } 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 08a4bb2db1ee..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 @@ -327,8 +327,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext)) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() } whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } + whenever(exitDesktopTransitionHandler.startTransition(any(), any(), any(), any())) + .thenReturn(Binder()) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext) whenever(displayController.getDisplay(anyInt())).thenReturn(display) @@ -917,7 +919,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -2039,6 +2044,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_reparentsToTaskDisplayArea() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + wct.assertHop(ReparentPredicate(token = task.token, parentToken = tda.token, toTop = true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_deactivatesDesk() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -2053,6 +2082,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val homeTask = setUpHomeTask() @@ -2078,6 +2108,60 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_homeBehindFullscreen_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -2093,9 +2177,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestExitDesktopWct() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(wct.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop but doesn't reorder home or the task - wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wct.assertReorder(wallpaperToken, toTop = false) + wct.assertWithoutHop(ReorderPredicate(homeTask.token, toTop = null)) } @Test @@ -2113,6 +2197,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { val homeTask = setUpHomeTask() val task = setUpFreeformTask() @@ -2138,6 +2223,61 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Removes wallpaper activity when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -2164,6 +2304,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) + assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode = + WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false)) + } + + @Test fun moveToFullscreen_nonExistentTask_doesNothing() { controller.moveToFullscreen(999, transitionSource = UNKNOWN) verifyExitDesktopWCTNotExecuted() @@ -2759,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() @@ -2782,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) @@ -4033,10 +4264,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wct.assertReorder(wallpaperToken, toTop = false) } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -4060,6 +4292,52 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task2, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { val freeformTask = setUpFreeformTask() @@ -6327,15 +6605,46 @@ private fun WindowContainerTransaction.assertWithoutHop( assertThat(hierarchyOps.none(predicate)).isTrue() } -private fun WindowContainerTransaction.assertReorder( +private fun WindowContainerTransaction.indexOfReorder( task: RunningTaskInfo, toTop: Boolean? = null, -) { - assertHop { hop -> +): Int { + val hop = hierarchyOps.singleOrNull(ReorderPredicate(task.token, toTop)) ?: return -1 + return hierarchyOps.indexOf(hop) +} + +private class ReorderPredicate(val token: WindowContainerToken, val toTop: Boolean? = null) : + ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = hop.type == HIERARCHY_OP_TYPE_REORDER && (toTop == null || hop.toTop == toTop) && - hop.container == task.token.asBinder() - } + hop.container == token.asBinder() +} + +private class ReparentPredicate( + val token: WindowContainerToken, + val parentToken: WindowContainerToken, + val toTop: Boolean? = null, +) : ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = + hop.isReparent && + (toTop == null || hop.toTop == toTop) && + hop.container == token.asBinder() && + hop.newParent == parentToken.asBinder() +} + +private fun WindowContainerTransaction.assertReorder( + task: RunningTaskInfo, + toTop: Boolean? = null, +) { + assertReorder(task.token, toTop) +} + +private fun WindowContainerTransaction.assertReorder( + token: WindowContainerToken, + toTop: Boolean? = null, +) { + assertHop(ReorderPredicate(token, toTop)) } private fun WindowContainerTransaction.assertReorderAt( 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 9f09e3f57927..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 @@ -20,6 +20,7 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo @@ -177,4 +178,70 @@ class DesksTransitionObserverTest : ShellTestCase() { assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId) assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId) } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDesk_updatesRepository() { + val transition = Binder() + val deskChange = Change(mock(), mock()) + whenever(mockDesksOrganizer.isDeskChange(deskChange, deskId = 5)).thenReturn(true) + 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).apply { addChange(deskChange) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithExitingTask_updatesRepository() { + val transition = Binder() + val exitingTask = createFreeformTask(DEFAULT_DISPLAY) + val exitingTaskChange = Change(mock(), mock()).apply { taskInfo = exitingTask } + whenever(mockDesksOrganizer.getDeskAtEnd(exitingTaskChange)).thenReturn(null) + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + repository.addTaskToDesk( + displayId = DEFAULT_DISPLAY, + deskId = 5, + taskId = exitingTask.taskId, + isVisible = true, + ) + assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isTrue() + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = + TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange(exitingTaskChange) + }, + ) + + 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/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 4d4b15389eca..8b10ca1a2a70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -23,11 +23,13 @@ import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat @@ -104,54 +106,45 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesRoot() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - organizer.onTaskVanished(freeformRoot) + organizer.onTaskVanished(desk.taskInfo) - assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse() + assertThat(organizer.roots.contains(desk.deskId)).isFalse() } @Test fun testDesktopWindowAppearsInDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) - assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId) + assertThat(desk.children).contains(child.taskId) } @Test fun testDesktopWindowDisappearsFromDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) organizer.onTaskVanished(child) - assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId) + assertThat(desk.children).doesNotContain(child.taskId) } @Test fun testRemoveDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.removeDesk(wct, freeformRoot.taskId) + organizer.removeDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -167,25 +160,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testActivateDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.activateDesk(wct, freeformRoot.taskId) + organizer.activateDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && hop.toTop && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -201,20 +192,18 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testMoveTaskToDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() - organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + organizer.moveTaskToDesk(wct, desk.deskId, desktopTask) assertThat( wct.hierarchyOps.any { hop -> hop.isReparent && hop.toTop && hop.container == desktopTask.token.asBinder() && - hop.newParent == freeformRoot.token.asBinder() + hop.newParent == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -240,17 +229,15 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val task = createFreeformTask().apply { parentTaskId = desk.deskId } val endDesk = organizer.getDeskAtEnd( TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } ) - assertThat(endDesk).isEqualTo(freeformRoot.taskId) + assertThat(endDesk).isEqualTo(desk.deskId) } @Test @@ -273,6 +260,47 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { assertThat(isActive).isTrue() } + @Test + fun deactivateDesk_clearsLaunchRoot() { + val wct = WindowContainerTransaction() + val desk = createDesk() + organizer.activateDesk(wct, desk.deskId) + + organizer.deactivateDesk(wct, desk.deskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && + hop.container == desk.taskInfo.token.asBinder() && + hop.windowingModes == null && + hop.activityTypes == null + } + ) + .isTrue() + } + + @Test + fun isDeskChange() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply { + taskInfo = desk.taskInfo + }, + desk.deskId, + ) + ) + .isTrue() + } + + private fun createDesk(): DeskRoot { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + return organizer.roots[freeformRoot.taskId] + } + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { var deskId: Int? = null val created: Boolean diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index 8e0381e4f933..0c1952910d1a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; @@ -26,6 +27,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -44,15 +46,19 @@ import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + /** * Unit test against {@link PipScheduler} */ @@ -77,6 +83,8 @@ public class PipSchedulerTest { @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; + @Mock private SplitScreenController mMockSplitScreenController; + @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; @@ -93,7 +101,8 @@ public class PipSchedulerTest { .thenReturn(mMockTransaction); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, mMockPipDesktopState); + mMockPipTransitionState, Optional.of(mMockSplitScreenController), + mMockPipDesktopState); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) -> @@ -119,12 +128,18 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, never()).startExpandTransition(any()); + verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean()); } @Test - public void scheduleExitPipViaExpand_exitTransitionCalled() { + public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() { setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 isn't in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(false); mPipScheduler.scheduleExitPipViaExpand(); @@ -132,7 +147,29 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, times(1)).startExpandTransition(any()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); + } + + @Test + public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() { + setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 is in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(true); + + mPipScheduler.scheduleExitPipViaExpand(); + + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + mRunnableArgumentCaptor.getValue().run(); + + // We need to both prepare the split screen with the last parent and start expanding. + verify(mMockSplitScreenController, + times(1)).prepareEnterSplitScreen(any(), any(), anyInt()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); } @Test @@ -259,4 +296,10 @@ public class PipSchedulerTest { private void setMockPipTaskToken() { when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken); } + + private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.lastParentTaskIdBeforePip = lastParentId; + return taskInfo; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java new file mode 100644 index 000000000000..2a22842eda1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java @@ -0,0 +1,188 @@ +/* + * 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.pip2.phone.transition; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.VerificationKt.times; +import static org.mockito.kotlin.VerificationKt.verify; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +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.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.TransitionInfoBuilder; +import com.android.wm.shell.util.StubTransaction; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Unit test against {@link PipExpandHandler} + */ + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipExpandHandlerTest { + @Mock private Context mMockContext; + @Mock private PipBoundsState mMockPipBoundsState; + @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PipTransitionState mMockPipTransitionState; + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; + @Mock private SplitScreenController mMockSplitScreenController; + + @Mock private IBinder mMockTransitionToken; + @Mock private TransitionRequestInfo mMockRequestInfo; + @Mock private StubTransaction mStartT; + @Mock private StubTransaction mFinishT; + @Mock private SurfaceControl mPipLeash; + + @Mock private PipExpandAnimator mMockPipExpandAnimator; + + @Surface.Rotation + private static final int DISPLAY_ROTATION = Surface.ROTATION_0; + + private static final float SNAP_FRACTION = 1.5f; + private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100); + private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000); + private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000); + + private PipExpandHandler mPipExpandHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS); + when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION); + when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION); + + mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState, + Optional.of(mMockSplitScreenController)); + mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction, + finishTransaction, baseBounds, startBounds, endBounds, + sourceRectHint, rotation) -> mMockPipExpandAnimator); + } + + @Test + public void handleRequest_returnNull() { + // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be + // returning any non-null WCT + WindowContainerTransaction wct = mPipExpandHandler.handleRequest( + mMockTransitionToken, mMockRequestInfo); + assertNull(wct); + } + + @Test + public void startAnimation_transitExit_startExpandAnimator() { + final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build()); + + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */); + final WindowContainerToken pipToken = pipTaskInfo.getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + @Test + public void startAnimation_transitExitToSplit_startExpandAnimator() { + // The task info of the task that was pinned while we were in PiP. + final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN, + new PictureInPictureParams.Builder().build()).getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + // Change representing the ActivityRecord we are animating in the multi-activity PiP case; + // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token. + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT)); + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type, + @Nullable ActivityManager.RunningTaskInfo pipTaskInfo, + @Nullable WindowContainerToken lastParent, boolean toSplit) { + final TransitionInfo info = new TransitionInfoBuilder(type) + .addChange(TRANSIT_CHANGE, pipTaskInfo).build(); + final TransitionInfo.Change pipChange = info.getChanges().getFirst(); + pipChange.setRotation(DISPLAY_ROTATION, + WindowConfiguration.ROTATION_UNDEFINED); + pipChange.setStartAbsBounds(PIP_BOUNDS); + pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS); + pipChange.setLeash(mPipLeash); + pipChange.setLastParent(lastParent); + return info; + } + + private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId, + int windowingMode, PictureInPictureParams params) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.token = mock(WindowContainerToken.class); + taskInfo.baseIntent = mock(Intent.class); + taskInfo.pictureInPictureParams = params; + return taskInfo; + } +} 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/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 0fa31c7a832e..f5e10d94452f 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -1467,8 +1467,6 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( } const StringPiece16 kAttr16 = u"attr"; - const static std::u16string kAttrPrivate16 = u"^attr-private"; - for (const PackageGroup& package_group : package_groups_) { for (const ConfiguredPackage& package_impl : package_group.packages_) { const LoadedPackage* package = package_impl.loaded_package_; @@ -1480,12 +1478,13 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( base::expected<uint32_t, NullOrIOError> resid = package->FindEntryByName(type16, entry16); if (UNLIKELY(IsIOError(resid))) { return base::unexpected(resid.error()); - } + } if (!resid.has_value() && kAttr16 == type16) { // Private attributes in libraries (such as the framework) are sometimes encoded // under the type '^attr-private' in order to leave the ID space of public 'attr' // free for future additions. Check '^attr-private' for the same name. + const static std::u16string kAttrPrivate16 = u"^attr-private"; resid = package->FindEntryByName(kAttrPrivate16, entry16); } 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/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp index b56b944955d7..af8e856a5ad6 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp @@ -35,5 +35,6 @@ android_library { "com.android.extservices", "com.android.permission", "com.android.healthfitness", + "com.android.mediaprovider", ], } 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/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp index e04af6c1ab11..6b9cbfa8ece7 100644 --- a/packages/SettingsLib/SettingsTransition/Android.bp +++ b/packages/SettingsLib/SettingsTransition/Android.bp @@ -30,5 +30,6 @@ android_library { "com.android.extservices", "com.android.permission", "com.android.healthfitness", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index 76e36dc5ff7d..bf26264a4f0e 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -32,5 +32,6 @@ android_library { "com.android.cellbroadcast", "com.android.devicelock", "com.android.healthfitness", + "com.android.mediaprovider", ], } 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/Android.bp b/packages/SystemUI/aconfig/Android.bp index 088ec136f24e..f5bff859269f 100644 --- a/packages/SystemUI/aconfig/Android.bp +++ b/packages/SystemUI/aconfig/Android.bp @@ -28,6 +28,7 @@ package { "//frameworks/libs/systemui/tracinglib:__subpackages__", "//frameworks/base/services/accessibility:__subpackages__", "//frameworks/base/services/tests:__subpackages__", + "//packages/apps/Settings:__subpackages__", "//platform_testing:__subpackages__", "//vendor:__subpackages__", "//cts:__subpackages__", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 0ccb20ce3e3f..3e241bfe6447 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2026,3 +2026,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/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index b9f9bc7e2daa..5b073e49192a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -27,9 +27,8 @@ import android.graphics.fonts.FontVariationAxis import android.text.Layout import android.util.Log import android.util.LruCache - -private const val DEFAULT_ANIMATION_DURATION: Long = 300 -private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 +import androidx.annotation.VisibleForTesting +import com.android.app.animation.Interpolators typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit @@ -76,6 +75,10 @@ class TypefaceVariantCacheImpl(var baseTypeface: Typeface, override val animatio cache.put(fvar, it) } } + + companion object { + private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 + } } /** @@ -108,25 +111,12 @@ class TextAnimator( private val typefaceCache: TypefaceVariantCache, private val invalidateCallback: () -> Unit = {}, ) { - // Following two members are for mutable for testing purposes. - public var textInterpolator = TextInterpolator(layout, typefaceCache) - public var animator = - ValueAnimator.ofFloat(1f).apply { - duration = DEFAULT_ANIMATION_DURATION - addUpdateListener { - textInterpolator.progress = it.animatedValue as Float - textInterpolator.linearProgress = - it.currentPlayTime.toFloat() / it.duration.toFloat() - invalidateCallback() - } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase() + @VisibleForTesting var textInterpolator = TextInterpolator(layout, typefaceCache) + @VisibleForTesting var createAnimator: () -> ValueAnimator = { ValueAnimator.ofFloat(1f) } - override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase() - } - ) - } + var animator: ValueAnimator? = null + + val fontVariationUtils = FontVariationUtils() sealed class PositionedGlyph { /** Mutable X coordinate of the glyph position relative from drawing offset. */ @@ -165,8 +155,6 @@ class TextAnimator( protected set } - private val fontVariationUtils = FontVariationUtils() - fun updateLayout(layout: Layout, textSize: Float = -1f) { textInterpolator.layout = layout @@ -178,9 +166,8 @@ class TextAnimator( } } - fun isRunning(): Boolean { - return animator.isRunning - } + val isRunning: Boolean + get() = animator?.isRunning ?: false /** * GlyphFilter applied just before drawing to canvas for tweaking positions and text size. @@ -237,110 +224,110 @@ class TextAnimator( fun draw(c: Canvas) = textInterpolator.draw(c) - /** - * Set text style with animation. - * - * ``` - * By passing -1 to weight, the view preserve the current weight. - * By passing -1 to textSize, the view preserve the current text size. - * By passing -1 to duration, the default text animation, 1000ms, is used. - * By passing false to animate, the text will be updated without animation. - * ``` - * - * @param fvar an optional text fontVariationSettings. - * @param textSize an optional font size. - * @param colors an optional colors array that must be the same size as numLines passed to the - * TextInterpolator - * @param strokeWidth an optional paint stroke width - * @param animate an optional boolean indicating true for showing style transition as animation, - * false for immediate style transition. True by default. - * @param duration an optional animation duration in milliseconds. This is ignored if animate is - * false. - * @param interpolator an optional time interpolator. If null is passed, last set interpolator - * will be used. This is ignored if animate is false. - */ - fun setTextStyle( - fvar: String? = "", - textSize: Float = -1f, - color: Int? = null, - strokeWidth: Float = -1f, - animate: Boolean = true, - duration: Long = -1L, - interpolator: TimeInterpolator? = null, - delay: Long = 0, - onAnimationEnd: Runnable? = null, + /** Style spec to use when rendering the font */ + data class Style( + val fVar: String? = null, + val textSize: Float? = null, + val color: Int? = null, + val strokeWidth: Float? = null, ) { - setTextStyleInternal( - fvar, - textSize, - color, - strokeWidth, - animate, - duration, - interpolator, - delay, - onAnimationEnd, - updateLayoutOnFailure = true, - ) + fun withUpdatedFVar( + fontVariationUtils: FontVariationUtils, + weight: Int = -1, + width: Int = -1, + opticalSize: Int = -1, + roundness: Int = -1, + ): Style { + return this.copy( + fVar = + fontVariationUtils.updateFontVariation( + weight = weight, + width = width, + opticalSize = opticalSize, + roundness = roundness, + ) + ) + } } - private fun setTextStyleInternal( - fvar: String?, - textSize: Float, - color: Int?, - strokeWidth: Float, - animate: Boolean, - duration: Long, - interpolator: TimeInterpolator?, - delay: Long, - onAnimationEnd: Runnable?, - updateLayoutOnFailure: Boolean, + /** Animation Spec for use when style changes should be animated */ + data class Animation( + val animate: Boolean = true, + val startDelay: Long = 0, + val duration: Long = DEFAULT_ANIMATION_DURATION, + val interpolator: TimeInterpolator = Interpolators.LINEAR, + val onAnimationEnd: Runnable? = null, ) { - try { - if (animate) { - animator.cancel() - textInterpolator.rebase() + fun configureAnimator(animator: Animator) { + animator.startDelay = startDelay + animator.duration = duration + animator.interpolator = interpolator + if (onAnimationEnd != null) { + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd.run() + } + } + ) } + } - if (textSize >= 0) { - textInterpolator.targetPaint.textSize = textSize - } - if (!fvar.isNullOrBlank()) { - textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) - } - if (color != null) { - textInterpolator.targetPaint.color = color - } - if (strokeWidth >= 0F) { - textInterpolator.targetPaint.strokeWidth = strokeWidth + companion object { + val DISABLED = Animation(animate = false) + } + } + + /** Sets the text style, optionally with animation */ + fun setTextStyle(style: Style, animation: Animation = Animation.DISABLED) { + animator?.cancel() + setTextStyleInternal(style, rebase = animation.animate) + + if (animation.animate) { + animator = buildAnimator(animation).apply { start() } + } else { + textInterpolator.progress = 1f + textInterpolator.rebase() + invalidateCallback() + } + } + + /** Builds a ValueAnimator from the specified animation parameters */ + private fun buildAnimator(animation: Animation): ValueAnimator { + return createAnimator().apply { + duration = DEFAULT_ANIMATION_DURATION + animation.configureAnimator(this) + + addUpdateListener { + textInterpolator.progress = it.animatedValue as Float + textInterpolator.linearProgress = it.currentPlayTime / it.duration.toFloat() + invalidateCallback() } - textInterpolator.onTargetPaintModified() - if (animate) { - animator.startDelay = delay - animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration - interpolator?.let { animator.interpolator = it } - if (onAnimationEnd != null) { - animator.addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - onAnimationEnd.run() - animator.removeListener(this) - } - - override fun onAnimationCancel(animation: Animator) { - animator.removeListener(this) - } - } - ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) = textInterpolator.rebase() + + override fun onAnimationCancel(animator: Animator) = textInterpolator.rebase() } - animator.start() - } else { - // No animation is requested, thus set base and target state to the same state. - textInterpolator.progress = 1f - textInterpolator.rebase() - invalidateCallback() + ) + } + } + + private fun setTextStyleInternal( + style: Style, + rebase: Boolean, + updateLayoutOnFailure: Boolean = true, + ) { + try { + if (rebase) textInterpolator.rebase() + style.color?.let { textInterpolator.targetPaint.color = it } + style.textSize?.let { textInterpolator.targetPaint.textSize = it } + style.strokeWidth?.let { textInterpolator.targetPaint.strokeWidth = it } + style.fVar?.let { + textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(it) } + textInterpolator.onTargetPaintModified() } catch (ex: IllegalArgumentException) { if (updateLayoutOnFailure) { Log.e( @@ -351,81 +338,15 @@ class TextAnimator( ) updateLayout(textInterpolator.layout) - setTextStyleInternal( - fvar, - textSize, - color, - strokeWidth, - animate, - duration, - interpolator, - delay, - onAnimationEnd, - updateLayoutOnFailure = false, - ) + setTextStyleInternal(style, rebase, updateLayoutOnFailure = false) } else { throw ex } } } - /** - * Set text style with animation. Similar as - * - * ``` - * fun setTextStyle( - * fvar: String? = "", - * textSize: Float = -1f, - * color: Int? = null, - * strokeWidth: Float = -1f, - * animate: Boolean = true, - * duration: Long = -1L, - * interpolator: TimeInterpolator? = null, - * delay: Long = 0, - * onAnimationEnd: Runnable? = null - * ) - * ``` - * - * @param weight an optional style value for `wght` in fontVariationSettings. - * @param width an optional style value for `wdth` in fontVariationSettings. - * @param opticalSize an optional style value for `opsz` in fontVariationSettings. - * @param roundness an optional style value for `ROND` in fontVariationSettings. - */ - fun setTextStyle( - weight: Int = -1, - width: Int = -1, - opticalSize: Int = -1, - roundness: Int = -1, - textSize: Float = -1f, - color: Int? = null, - strokeWidth: Float = -1f, - animate: Boolean = true, - duration: Long = -1L, - interpolator: TimeInterpolator? = null, - delay: Long = 0, - onAnimationEnd: Runnable? = null, - ) { - setTextStyleInternal( - fvar = - fontVariationUtils.updateFontVariation( - weight = weight, - width = width, - opticalSize = opticalSize, - roundness = roundness, - ), - textSize = textSize, - color = color, - strokeWidth = strokeWidth, - animate = animate, - duration = duration, - interpolator = interpolator, - delay = delay, - onAnimationEnd = onAnimationEnd, - updateLayoutOnFailure = true, - ) - } - companion object { private val TAG = TextAnimator::class.simpleName!! + const val DEFAULT_ANIMATION_DURATION = 300L } } 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/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index b76656d78cc4..4bf0ceb51784 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -366,7 +366,7 @@ constructor( fun animateCharge(isDozing: () -> Boolean) { // Skip charge animation if dozing animation is already playing. - if (textAnimator == null || textAnimator!!.isRunning()) { + if (textAnimator == null || textAnimator!!.isRunning) { return } @@ -444,29 +444,28 @@ constructor( delay: Long, onAnimationEnd: Runnable?, ) { - textAnimator?.let { - it.setTextStyle( - weight = weight, - color = color, + val style = TextAnimator.Style(color = color) + val animation = + TextAnimator.Animation( animate = animate && isAnimationEnabled, duration = duration, - interpolator = interpolator, - delay = delay, + interpolator = interpolator ?: Interpolators.LINEAR, + startDelay = delay, onAnimationEnd = onAnimationEnd, ) + textAnimator?.let { + it.setTextStyle( + style.withUpdatedFVar(it.fontVariationUtils, weight = weight), + animation, + ) it.glyphFilter = glyphFilter } ?: run { // when the text animator is set, update its start values onTextAnimatorInitialized = { textAnimator -> textAnimator.setTextStyle( - weight = weight, - color = color, - animate = false, - duration = duration, - interpolator = interpolator, - delay = delay, - onAnimationEnd = onAnimationEnd, + style.withUpdatedFVar(textAnimator.fontVariationUtils, weight = weight), + animation.copy(animate = false), ) textAnimator.glyphFilter = glyphFilter } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index a5adfa2a1ac6..0b7ea1a335ef 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -21,6 +21,7 @@ import android.view.ViewGroup import android.view.animation.Interpolator import android.widget.RelativeLayout import androidx.annotation.VisibleForTesting +import com.android.systemui.animation.TextAnimator import com.android.systemui.customization.R import com.android.systemui.log.core.Logger import com.android.systemui.plugins.clocks.AlarmData @@ -65,7 +66,7 @@ data class DigitalAlignment( data class FontTextStyle( val lineHeight: Float? = null, val fontSizeScale: Float? = null, - val transitionDuration: Long = -1L, + val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION, val transitionInterpolator: Interpolator? = null, ) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 92fa6b5be1ed..8317aa39ef2b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -33,7 +33,9 @@ import android.util.MathUtils import android.util.TypedValue import android.view.View.MeasureSpec.EXACTLY import android.view.animation.Interpolator +import android.view.animation.PathInterpolator import android.widget.TextView +import com.android.app.animation.Interpolators import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.GSFAxes import com.android.systemui.animation.TextAnimator @@ -84,17 +86,20 @@ open class SimpleDigitalClockTextView( else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS) } - private var lsFontVariation = - if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar() - else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar() + private var lsFontVariation: String + private var aodFontVariation: String + private var fidgetFontVariation: String - private var aodFontVariation = run { + init { val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS - (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar() - } + val lsFontAxes = + if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS) + else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS) - // TODO(b/374306512): Fidget endpoint to spec - private var fidgetFontVariation = aodFontVariation + lsFontVariation = lsFontAxes.toFVar() + aodFontVariation = (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar() + fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar() + } private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1 @@ -121,7 +126,7 @@ open class SimpleDigitalClockTextView( protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: ClockLogger.INIT_LOGGER - private var aodDozingInterpolator: Interpolator? = null + private var aodDozingInterpolator: Interpolator = Interpolators.LINEAR @VisibleForTesting lateinit var textAnimator: TextAnimator @@ -149,7 +154,7 @@ open class SimpleDigitalClockTextView( lockscreenColor = color lockScreenPaint.color = lockscreenColor if (dozeFraction < 1f) { - textAnimator.setTextStyle(color = lockscreenColor, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(color = lockscreenColor)) } invalidate() } @@ -157,10 +162,8 @@ open class SimpleDigitalClockTextView( fun updateAxes(lsAxes: List<ClockFontAxisSetting>) { lsFontVariation = lsAxes.toFVar() aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar() - logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) { - str1 = lsFontVariation - str2 = aodFontVariation - } + fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar() + logger.updateAxes(lsFontVariation, aodFontVariation) lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface @@ -168,13 +171,28 @@ open class SimpleDigitalClockTextView( lockScreenPaint.getTextBounds(text, 0, text.length, textBounds) targetTextBounds.set(textBounds) - textAnimator.setTextStyle(fvar = lsFontVariation, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation)) measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) recomputeMaxSingleDigitSizes() requestLayout() invalidate() } + fun buildFidgetVariation(axes: List<ClockFontAxisSetting>): List<ClockFontAxisSetting> { + val result = mutableListOf<ClockFontAxisSetting>() + for (axis in axes) { + result.add( + FIDGET_DISTS.get(axis.key)?.let { (dist, midpoint) -> + ClockFontAxisSetting( + axis.key, + axis.value + dist * if (axis.value > midpoint) -1 else 1, + ) + } ?: axis + ) + } + return result + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { logger.onMeasure() super.onMeasure(widthMeasureSpec, heightMeasureSpec) @@ -245,40 +263,54 @@ open class SimpleDigitalClockTextView( fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { if (!this::textAnimator.isInitialized) return + logger.animateDoze() textAnimator.setTextStyle( - animate = isAnimated && isAnimationEnabled, - color = if (isDozing) AOD_COLOR else lockscreenColor, - textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, - fvar = if (isDozing) aodFontVariation else lsFontVariation, - duration = aodStyle.transitionDuration, - interpolator = aodDozingInterpolator, + TextAnimator.Style( + fVar = if (isDozing) aodFontVariation else lsFontVariation, + color = if (isDozing) AOD_COLOR else lockscreenColor, + textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, + ), + TextAnimator.Animation( + animate = isAnimated && isAnimationEnabled, + duration = aodStyle.transitionDuration, + interpolator = aodDozingInterpolator, + ), ) updateTextBoundsForTextAnimator() } fun animateCharge() { - if (!this::textAnimator.isInitialized || textAnimator.isRunning()) { + if (!this::textAnimator.isInitialized || textAnimator.isRunning) { // Skip charge animation if dozing animation is already playing. return } - logger.d("animateCharge()") + logger.animateCharge() + + val lsStyle = TextAnimator.Style(fVar = lsFontVariation) + val aodStyle = TextAnimator.Style(fVar = aodFontVariation) + textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation, - animate = isAnimationEnabled, - onAnimationEnd = - Runnable { + if (dozeFraction == 0f) aodStyle else lsStyle, + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = CHARGE_ANIMATION_DURATION, + onAnimationEnd = { textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation, - animate = isAnimationEnabled, + if (dozeFraction == 0f) lsStyle else aodStyle, + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = CHARGE_ANIMATION_DURATION, + ), ) updateTextBoundsForTextAnimator() }, + ), ) updateTextBoundsForTextAnimator() } fun animateFidget(x: Float, y: Float) { - if (!this::textAnimator.isInitialized || textAnimator.isRunning()) { + if (!this::textAnimator.isInitialized || textAnimator.isRunning) { // Skip fidget animation if other animation is already playing. return } @@ -286,19 +318,25 @@ open class SimpleDigitalClockTextView( logger.animateFidget(x, y) clockCtx.vibrator?.vibrate(FIDGET_HAPTICS) - // TODO(b/374306512): Duplicated charge animation as placeholder. Implement final version - // when we have a complete spec. May require additional code to animate individual digits. + // TODO(b/374306512): Delay each glyph's animation based on x/y position textAnimator.setTextStyle( - fvar = fidgetFontVariation, - animate = isAnimationEnabled, - onAnimationEnd = - Runnable { + TextAnimator.Style(fVar = fidgetFontVariation), + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = FIDGET_ANIMATION_DURATION, + interpolator = FIDGET_INTERPOLATOR, + onAnimationEnd = { textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation, - animate = isAnimationEnabled, + TextAnimator.Style(fVar = lsFontVariation), + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = FIDGET_ANIMATION_DURATION, + interpolator = FIDGET_INTERPOLATOR, + ), ) updateTextBoundsForTextAnimator() }, + ), ) updateTextBoundsForTextAnimator() } @@ -329,42 +367,20 @@ open class SimpleDigitalClockTextView( } private fun getInterpolatedTextBounds(): Rect { - val interpolatedTextBounds = Rect() - if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) { - interpolatedTextBounds.left = - MathUtils.lerp( - prevTextBounds.left, - targetTextBounds.left, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.right = - MathUtils.lerp( - prevTextBounds.right, - targetTextBounds.right, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.top = - MathUtils.lerp( - prevTextBounds.top, - targetTextBounds.top, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.bottom = - MathUtils.lerp( - prevTextBounds.bottom, - targetTextBounds.bottom, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - } else { - interpolatedTextBounds.set(targetTextBounds) + val progress = textAnimator.animator?.let { it.animatedValue as Float } ?: 1f + if (!textAnimator.isRunning || progress >= 1f) { + return Rect(targetTextBounds) } + + val interpolatedTextBounds = Rect() + interpolatedTextBounds.left = + MathUtils.lerp(prevTextBounds.left, targetTextBounds.left, progress).toInt() + interpolatedTextBounds.right = + MathUtils.lerp(prevTextBounds.right, targetTextBounds.right, progress).toInt() + interpolatedTextBounds.top = + MathUtils.lerp(prevTextBounds.top, targetTextBounds.top, progress).toInt() + interpolatedTextBounds.bottom = + MathUtils.lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress).toInt() return interpolatedTextBounds } @@ -471,7 +487,7 @@ open class SimpleDigitalClockTextView( textStyle.lineHeight?.let { lineHeight = it.toInt() } this.aodStyle = aodStyle ?: textStyle.copy() - this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it } + aodDozingInterpolator = this.aodStyle.transitionInterpolator ?: Interpolators.LINEAR lockScreenPaint.strokeWidth = textBorderWidth measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) setInterpolatorPaint() @@ -500,7 +516,7 @@ open class SimpleDigitalClockTextView( recomputeMaxSingleDigitSizes() if (this::textAnimator.isInitialized) { - textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(textSize = lockScreenPaint.textSize)) } } @@ -525,10 +541,11 @@ open class SimpleDigitalClockTextView( textAnimator.textInterpolator.targetPaint.set(lockScreenPaint) textAnimator.textInterpolator.onTargetPaintModified() textAnimator.setTextStyle( - fvar = lsFontVariation, - textSize = lockScreenPaint.textSize, - color = lockscreenColor, - animate = false, + TextAnimator.Style( + fVar = lsFontVariation, + textSize = lockScreenPaint.textSize, + color = lockscreenColor, + ) ) } } @@ -572,6 +589,17 @@ open class SimpleDigitalClockTextView( .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43) .compose() + val CHARGE_ANIMATION_DURATION = 500L + val FIDGET_ANIMATION_DURATION = 250L + val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) + val FIDGET_DISTS = + mapOf( + GSFAxes.WEIGHT to Pair(200f, 500f), + GSFAxes.WIDTH to Pair(30f, 75f), + GSFAxes.ROUND to Pair(0f, 50f), + GSFAxes.SLANT to Pair(0f, -5f), + ) + val AOD_COLOR = Color.WHITE val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f) val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt index d6ba98d65d15..441f807a8ec8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.core.Logger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() { assertThat(latest).isFalse() } + @Test + fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND) + fakeSystemClock.setCurrentTimeMillis(5000) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000) + } + + @Test + fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + fakeSystemClock.setCurrentTimeMillis(5000) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() + } + + @Test + fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + fakeSystemClock.setCurrentTimeMillis(5000) + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + // WHEN the app becomes visible + fakeSystemClock.setCurrentTimeMillis(7000) + listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) + + // THEN the status and lastAppVisibleTime are updated + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) + + // WHEN the app is no longer visible + listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING) + + // THEN the lastAppVisibleTime is preserved + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) + + // WHEN the app is visible again + fakeSystemClock.setCurrentTimeMillis(9000) + listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) + + // THEN the lastAppVisibleTime is updated + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) + } + + @Test + fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() = + kosmos.runTest { + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + + // WHEN another UID becomes foreground + listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND) + + // THEN this UID still stays not visible + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + + @Test + fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + whenever(activityManager.addOnUidImportanceListener(any(), any())) + .thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + + /** Regression test for b/216248574. */ + @Test + fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + companion object { private const val THIS_UID = 558 private const val LOG_TAG = "LogTag" 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/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/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/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java index 93ba8e1317fa..064fd485dab4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.util.Log; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -52,6 +53,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @SmallTest +@FlakyTest(bugId = 395832204) @RunWith(AndroidJUnit4.class) public class PluginInstanceTest extends SysuiTestCase { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index cabe4afdea60..5d1950670777 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = null, promotedContent = PROMOTED_CONTENT, ), - 32L, + creationTime = 32L, ) val latest by collectLastValue(underTest.notificationChip) @@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ) - val underTest = factory.create(startingNotif, 123L) + val underTest = factory.create(startingNotif, creationTime = 123L) val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() @@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test - fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() = + fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = true + fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( @@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test - fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() = + fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( @@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun notificationChip_updatesWhenAppIsVisible() = kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(9000) + val underTest = factory.create( activeNotificationModel( @@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) - activityManagerRepository.fake.setIsAppVisible(UID, false) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() - activityManagerRepository.fake.setIsAppVisible(UID, true) + fakeSystemClock.setCurrentTimeMillis(11000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) - activityManagerRepository.fake.setIsAppVisible(UID, false) + fakeSystemClock.setCurrentTimeMillis(13000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) + + fakeSystemClock.setCurrentTimeMillis(15000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) + assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000) } - // Note: This test is theoretically impossible because the notification key should contain the - // UID, so if the UID changes then the key would also change and a new interactor would be - // created. But, test it just in case. @Test - fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() = + fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false - val hiddenUid = 100 - val shownUid = 101 + val originalUid = 100 + val newUid = 101 val underTest = factory.create( activeNotificationModel( key = "notif", - uid = hiddenUid, + uid = originalUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ), @@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true + val newPromotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.shortCriticalText = "Arrived" + } + val newPromotedContent = newPromotedContentBuilder.build() underTest.setNotification( activeNotificationModel( key = "notif", - uid = shownUid, + uid = newUid, statusBarChipIcon = mock(), - promotedContent = PROMOTED_CONTENT, + promotedContent = newPromotedContent, ) ) - // THEN we re-fetch the app visibility state with the new UID + // THEN we do update other fields like promoted content + assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent) + + // THEN we don't fetch the app visibility state for the new UID + assertThat(latest!!.isAppVisible).isFalse() + + // AND don't listen to updates for the new UID + activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false) + activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true) + assertThat(latest!!.isAppVisible).isFalse() + + // AND we still use updates from the old UID + // TODO(b/364653005): This particular behavior isn't great, can we do better? + activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index d8e4cd927bec..7ed2bd38bcd2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -333,7 +333,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_sortedBasedOnFirstAppearanceTime() = + fun shownNotificationChips_sortedByFirstAppearanceTime() = kosmos.runTest { val latest by collectLastValue(underTest.shownNotificationChips) @@ -349,8 +349,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), ) setNotifs(listOf(notif1)) - assertThat(latest).hasSize(1) - assertThat(latest!![0].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() // WHEN we add notif2 at t=2000 fakeSystemClock.advanceTime(1000) @@ -362,26 +361,20 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { ) setNotifs(listOf(notif1, notif2)) - // THEN notif2 is ranked above notif1 because it appeared later - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + // THEN notif2 is ranked above notif1 because notif2 appeared later + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 and notif2 swap places setNotifs(listOf(notif2, notif1)) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 and notif2 swap places again setNotifs(listOf(notif1, notif2)) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 gets an update val notif1NewPromotedContent = @@ -400,9 +393,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { ) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 disappears and then reappears fakeSystemClock.advanceTime(1000) @@ -413,9 +404,238 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { setNotifs(listOf(notif2, notif1)) // THEN notif1 is now ranked first - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif1") - assertThat(latest!![1].key).isEqualTo("notif2") + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_sortedByLastAppVisibleTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2's app becomes visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + + // THEN notif2 is no longer shown + assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() + + // WHEN notif2's app is no longer visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN the app associated with notif1 becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) + + // THEN notif1 is now ranked above notif2 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_newNotificationTakesPriorityOverLastAppVisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300) + + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2's app becomes visible then not visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN a new notif3 appears + fakeSystemClock.advanceTime(1000) + val notif3 = + activeNotificationModel( + key = notif3Info.key, + uid = notif3Info.uid, + statusBarChipIcon = notif3Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif3Info.key).build(), + ) + setNotifs(listOf(notif1, notif2, notif3)) + + // THEN notif3 is ranked above everything else + // AND notif2 is still before notif1 because it was more recently visible + assertThat(latest!!.map { it.key }) + .containsExactly("notif3", "notif2", "notif1") + .inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_fullSort() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300) + + // First, add notif1 at t=1000 + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + setNotifs(listOf(notif1)) + + // WHEN we add notif2 at t=2000 + fakeSystemClock.advanceTime(1000) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + + // THEN notif2 is ranked above notif1 because notif2 appeared later + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN notif2's app becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN the app associated with notif1 becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) + + // THEN notif1 is ranked above notif2 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2 gets an update + val notif2NewPromotedContent = + PromotedNotificationContentModel.Builder("notif2").apply { + this.shortCriticalText = "Arrived" + } + setNotifs( + listOf( + notif1, + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = notif2NewPromotedContent.build(), + ), + ) + ) + + // THEN notif1 is still ranked above notif2 to preserve chip ordering + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN a new notification appears + fakeSystemClock.advanceTime(1000) + val notif3 = + activeNotificationModel( + key = notif3Info.key, + uid = notif3Info.uid, + statusBarChipIcon = notif3Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif3Info.key).build(), + ) + setNotifs(listOf(notif1, notif2, notif3)) + + // THEN it's ranked first because it's new + assertThat(latest!!.map { it.key }) + .containsExactly("notif3", "notif1", "notif2") + .inOrder() + + // WHEN notif2 becomes visible then un-visible again + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN it moves to the front + assertThat(latest!!.map { it.key }) + .containsExactly("notif2", "notif3", "notif1") + .inOrder() + + // WHEN notif1 disappears and then reappears + fakeSystemClock.advanceTime(1000) + setNotifs(listOf(notif2, notif3)) + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif3").inOrder() + + fakeSystemClock.advanceTime(1000) + setNotifs(listOf(notif2, notif1, notif3)) + + // THEN notif1 is now ranked first + assertThat(latest!!.map { it.key }) + .containsExactly("notif1", "notif2", "notif3") + .inOrder() } @Test @@ -495,4 +715,6 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { .apply { notifs.forEach { addIndividualNotif(it) } } .build() } + + private data class NotifInfo(val key: String, val icon: StatusBarIconView, val uid: Int) } 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/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt index 9a837446a802..3ed321e48cd3 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt @@ -83,6 +83,13 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } + fun updateAxes(lsFVar: String, aodFVar: String) { + i({ "updateAxes(LS = $str1, AOD = $str2)" }) { + str1 = lsFVar + str2 = aodFVar + } + } + fun addView(child: View) { d({ "addView($str1 @$int1)" }) { str1 = child::class.simpleName!! @@ -90,6 +97,14 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } + fun animateDoze() { + d("animateDoze()") + } + + fun animateCharge() { + d("animateCharge()") + } + fun animateFidget(x: Float, y: Float) { d({ "animateFidget($str1, $str2)" }) { str1 = x.toString() 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..6a4a0b7893d9 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 --> 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/systemui/activity/data/model/AppVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt new file mode 100644 index 000000000000..2d21d655561f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt @@ -0,0 +1,29 @@ +/* + * 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.activity.data.model + +/** Describes an app's previous and current visibility to the user. */ +data class AppVisibilityModel( + /** True if the app is currently visible to the user and false otherwise. */ + val isAppCurrentlyVisible: Boolean = false, + /** + * The last time this app became visible to the user, in + * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't + * become visible since the flow started collection. + */ + val lastAppVisibleTime: Long? = null, +) diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt index 94614b70beda..11831eabf19d 100644 --- a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt @@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.core.Logger +import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.scan /** Repository for interfacing with [ActivityManager]. */ interface ActivityManagerRepository { + + /** + * Given a UID, creates a flow that emits details about when the process with the given UID was + * and is visible to the user. + * + * @param identifyingLogTag a tag identifying who created this flow, used for logging. + */ + fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> + /** * Given a UID, creates a flow that emits true when the process with the given UID is visible to * the user and false otherwise. @@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl @Inject constructor( @Background private val backgroundContext: CoroutineContext, + private val systemClock: SystemClock, private val activityManager: ActivityManager, ) : ActivityManagerRepository { + + override fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> { + return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag) + .distinctUntilChanged() + .scan(initial = AppVisibilityModel()) { + oldState: AppVisibilityModel, + newIsVisible: Boolean -> + if (newIsVisible) { + val lastAppVisibleTime = systemClock.currentTimeMillis() + logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) { + str1 = identifyingLogTag + long1 = lastAppVisibleTime + } + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = lastAppVisibleTime, + ) + } else { + // Reset the current status while maintaining the lastAppVisibleTime + oldState.copy(isAppCurrentlyVisible = false) + } + } + .distinctUntilChanged() + } + override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, 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/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/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java index c6bcab48fa68..75cb8ddca484 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java @@ -589,8 +589,10 @@ public class InternetDialogDelegateLegacy implements } mSecondaryMobileNetworkLayout = mDialogView.findViewById( R.id.secondary_mobile_network_layout); - mSecondaryMobileNetworkLayout.setOnClickListener( - this::onClickConnectedSecondarySub); + if (mCanConfigMobileData) { + mSecondaryMobileNetworkLayout.setOnClickListener( + this::onClickConnectedSecondarySub); + } mSecondaryMobileNetworkLayout.setBackground(mBackgroundOn); TextView mSecondaryMobileTitleText = mDialogView.requireViewById( @@ -623,6 +625,8 @@ public class InternetDialogDelegateLegacy implements mDialogView.requireViewById(R.id.secondary_settings_icon); mSecondaryMobileSettingsIcon.setColorFilter( dialog.getContext().getColor(R.color.connected_network_primary_color)); + mSecondaryMobileSettingsIcon.setVisibility(mCanConfigMobileData ? + View.VISIBLE : View.INVISIBLE); // set secondary visual for default data sub mMobileNetworkLayout.setBackground(mBackgroundOff); 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..a270ac5beb0f 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 @@ -244,6 +244,7 @@ constructor( ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene + if ( !validateSceneChange( from = currentSceneKey, @@ -523,14 +524,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 +599,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/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index d00585858ccb..beb40ff04cc8 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,38 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } + 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 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/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/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt index b1af811178e4..d8c3e2546a8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.activity.data.repository.ActivityManagerRepository import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -30,8 +31,6 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map /** * Interactor representing a single notification's status bar chip. @@ -53,6 +52,7 @@ constructor( @StatusBarChipsLog private val logBuffer: LogBuffer, ) { private val key = startingModel.key + private val uid = startingModel.uid private val logger = Logger(logBuffer, "Notif".pad()) // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the // top-level tag. It should instead be provided as the first string in each log message. @@ -88,28 +88,36 @@ constructor( } return } + + if (model.uid != uid) { + logger.e({ + "$str1: received model with different uid, which shouldn't happen. " + + "Original UID: $int1, New UID: $int2. " + + "Proceeding as usual, but app visibility changes will be for *old* UID." + }) { + str1 = extraLogTag + int1 = uid + int2 = model.uid + } + } _notificationModel.value = model } - private val uid: Flow<Int> = _notificationModel.map { it.uid } - - /** True if the application managing the notification is visible to the user. */ - private val isAppVisible: Flow<Boolean> = - uid.flatMapLatest { currentUid -> - activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag) - } + /** Details about when the app managing the notification was & is visible to the user. */ + private val appVisibility: Flow<AppVisibilityModel> = + activityManagerRepository.createAppVisibilityFlow(uid, logger, extraLogTag) /** * Emits this notification's status bar chip, or null if this notification shouldn't show a * status bar chip. */ val notificationChip: Flow<NotificationChipModel?> = - combine(_notificationModel, isAppVisible) { notif, isAppVisible -> - notif.toNotificationChipModel(isAppVisible) + combine(_notificationModel, appVisibility) { notif, appVisibility -> + notif.toNotificationChipModel(appVisibility) } private fun ActiveNotificationModel.toNotificationChipModel( - isVisible: Boolean + appVisibility: AppVisibilityModel ): NotificationChipModel? { val promotedContent = this.promotedContent if (promotedContent == null) { @@ -134,11 +142,13 @@ constructor( } return NotificationChipModel( - key, - appName, - statusBarChipIconView, - promotedContent, - isVisible, + key = key, + appName = appName, + statusBarChipIconView = statusBarChipIconView, + promotedContent = promotedContent, + creationTime = creationTime, + isAppVisible = appVisibility.isAppCurrentlyVisible, + lastAppVisibleTime = appVisibility.lastAppVisibleTime, ) } 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 c26d10311f1e..d20a2d18a7e7 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 @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import javax.inject.Inject +import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -39,9 +40,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton @@ -132,9 +135,6 @@ constructor( } interactor.setNotification(notif) } - logger.d({ "Interactors: $str1" }) { - str1 = promotedNotificationInteractorMap.keys.joinToString(separator = " /// ") - } promotedNotificationInteractors.value = promotedNotificationInteractorMap.values.toList() } @@ -148,23 +148,20 @@ constructor( private val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { // For all our current interactors... - promotedNotificationInteractors.flatMapLatest { intrs -> - // Stable-sort the promoted notifications by when they first appeared so that: - // 1) The chips don't switch places if the older chip gets a notification update. - // 2) The chips don't switch places when the second chip is tapped. (Whichever - // notification is showing heads-up is considered to be the top notification, which - // means tapping the second chip would move it to be the first chip if we didn't - // sort by appearance time here.) - // 3) Older chips get hidden if there's not enough room for all chips. - val interactors = intrs.sortedByDescending { it.creationTime } + // TODO(b/364653005): When a promoted notification is added or removed, each individual + // interactor's [notificationChip] flow becomes un-collected then re-collected, which + // can cause some flows to remove then add callbacks when they don't need to. Is there a + // better structure for this? Maybe Channels or a StateFlow with a short timeout? + promotedNotificationInteractors.flatMapLatest { interactors -> if (interactors.isNotEmpty()) { // Combine each interactor's [notificationChip] flow... val allNotificationChips: List<Flow<NotificationChipModel?>> = interactors.map { interactor -> interactor.notificationChip } combine(allNotificationChips) { - // ... and emit just the non-null chips - it.filterNotNull() - } + // ... and emit just the non-null & sorted chips + it.filterNotNull().sortedWith(chipComparator) + } + .logSort() } else { flowOf(emptyList()) } @@ -181,4 +178,35 @@ constructor( // out-of-sync (like a timer that's slightly off) chipsList.filter { !it.isAppVisible } } + + /* + Stable sort the promoted notifications by two criteria: + Criteria #1: Whichever app was most recently visible has higher ranking. + - Reasoning: If a user opened the app to see additional information, that's + likely the most important ongoing notification. + Criteria #2: Whichever notification first appeared more recently has higher ranking. + - Reasoning: Older chips get hidden if there's not enough room for all chips. + This semi-stable ordering ensures: + 1) The chips don't switch places if the older chip gets a notification update. + 2) The chips don't switch places when the second chip is tapped. (Whichever + notification is showing heads-up is considered to be the top notification, which + means tapping the second chip would move it to be the first chip if we didn't + sort by appearance time here.) + */ + private val chipComparator = + compareByDescending<NotificationChipModel> { + max(it.creationTime, it.lastAppVisibleTime ?: Long.MIN_VALUE) + } + + private fun Flow<List<NotificationChipModel>>.logSort(): Flow<List<NotificationChipModel>> { + return this.distinctUntilChanged().onEach { chips -> + val logString = + chips.joinToString { + "{key=${it.key}. " + + "lastVisibleAppTime=${it.lastAppVisibleTime}. " + + "creationTime=${it.creationTime}}" + } + logger.d({ "Sorted chips: $str1" }) { str1 = logString } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index 97c37628f2e1..1f2079d83e6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -26,6 +26,13 @@ data class NotificationChipModel( val appName: String, val statusBarChipIconView: StatusBarIconView?, val promotedContent: PromotedNotificationContentModel, + /** The time when the notification first appeared as promoted. */ + val creationTime: Long, /** True if the app managing this notification is currently visible to the user. */ val isAppVisible: Boolean, + /** + * The time when the app managing this notification last appeared as visible, or null if the app + * hasn't become visible since the notification became promoted. + */ + val lastAppVisibleTime: Long?, ) 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/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index d1d1ea9b5ff4..fd334447e13a 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 @@ -3227,11 +3227,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 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/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/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/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt index dcf38800bb01..14a81b3f8bfb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt @@ -30,7 +30,6 @@ import kotlin.math.ceil import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Mockito.eq import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -60,10 +59,11 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), TextAnimator.Animation()) // If animation is requested, the base state should be rebased and the target state should // be updated. @@ -90,10 +90,11 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = false) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400")) // If animation is not requested, the progress should be 1 which is end of animation and the // base state is rebased to target state by calling rebase. @@ -118,23 +119,24 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } textAnimator.setTextStyle( - weight = 400, - animate = true, - onAnimationEnd = animationEndCallback, + TextAnimator.Style("'wght' 400"), + TextAnimator.Animation(animate = true, onAnimationEnd = animationEndCallback), ) // Verify animationEnd callback has been added. val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java) - verify(valueAnimator).addListener(captor.capture()) - captor.value.onAnimationEnd(valueAnimator) + verify(valueAnimator, times(2)).addListener(captor.capture()) + for (callback in captor.allValues) { + callback.onAnimationEnd(valueAnimator) + } // Verify animationEnd callback has been invoked and removed. verify(animationEndCallback).run() - verify(valueAnimator).removeListener(eq(captor.value)) } @Test @@ -148,18 +150,20 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = true) + val animation = TextAnimator.Animation(animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation) val prevTypeface = paint.typeface - textAnimator.setTextStyle(weight = 700, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 700"), animation) assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface) - textAnimator.setTextStyle(weight = 400, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation) assertThat(paint.typeface).isSameInstanceAs(prevTypeface) } 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 e076418f2630..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 @@ -20,9 +20,10 @@ import android.view.LayoutInflater import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators -import com.android.systemui.customization.R import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.FontVariationUtils import com.android.systemui.animation.TextAnimator +import com.android.systemui.customization.R import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule @@ -32,7 +33,9 @@ import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @SmallTest @@ -46,6 +49,7 @@ class AnimatableClockViewTest : SysuiTestCase() { @Before fun setUp() { val layoutInflater = LayoutInflater.from(context) + whenever(mockTextAnimator.fontVariationUtils).thenReturn(FontVariationUtils()) clockView = layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView clockView.textAnimatorFactory = { _, _ -> mockTextAnimator } @@ -57,18 +61,19 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) + verify(mockTextAnimator).fontVariationUtils verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator) .setTextStyle( - weight = 300, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = false, - duration = 833L, - interpolator = Interpolators.EMPHASIZED_DECELERATE, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)), + eq( + TextAnimator.Animation( + animate = false, + duration = 833L, + interpolator = Interpolators.EMPHASIZED_DECELERATE, + onAnimationEnd = null, + ) + ), ) verifyNoMoreInteractions(mockTextAnimator) } @@ -79,30 +84,24 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() + verify(mockTextAnimator, times(2)).fontVariationUtils verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator) .setTextStyle( - weight = 100, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = false, - duration = 0L, - interpolator = null, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 100", color = 200)), + eq(TextAnimator.Animation(animate = false, duration = 0)), ) + verify(mockTextAnimator) .setTextStyle( - weight = 300, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = true, - duration = 833L, - interpolator = Interpolators.EMPHASIZED_DECELERATE, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)), + eq( + TextAnimator.Animation( + animate = true, + duration = 833L, + interpolator = Interpolators.EMPHASIZED_DECELERATE, + ) + ), ) verifyNoMoreInteractions(mockTextAnimator) } 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/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/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/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/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt index a6e71333c816..5dc28bea9bc4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt @@ -17,33 +17,77 @@ package com.android.systemui.activity.data.repository import android.app.activityManager +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.core.Logger +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.fakeSystemClock +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() } +val Kosmos.activityManagerRepository by + Kosmos.Fixture { FakeActivityManagerRepository(fakeSystemClock) } val Kosmos.realActivityManagerRepository by - Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) } + Kosmos.Fixture { + ActivityManagerRepositoryImpl(testDispatcher, fakeSystemClock, activityManager) + } -class FakeActivityManagerRepository : ActivityManagerRepository { - private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>() +class FakeActivityManagerRepository(private val systemClock: SystemClock) : + ActivityManagerRepository { + private val isVisibleFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>() + private val appVisibilityFlows = + mutableMapOf<Int, MutableList<MutableStateFlow<AppVisibilityModel>>>() var startingIsAppVisibleValue = false + override fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> { + val newFlow = + MutableStateFlow( + if (startingIsAppVisibleValue) { + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = systemClock.currentTimeMillis(), + ) + } else { + AppVisibilityModel(isAppCurrentlyVisible = false, lastAppVisibleTime = null) + } + ) + appVisibilityFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) + return newFlow + } + override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): MutableStateFlow<Boolean> { val newFlow = MutableStateFlow(startingIsAppVisibleValue) - uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) + isVisibleFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) return newFlow } fun setIsAppVisible(uid: Int, isAppVisible: Boolean) { - uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible } + isVisibleFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible } + appVisibilityFlows[uid]?.forEach { stateFlow -> + stateFlow.value = + if (isAppVisible) { + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = systemClock.currentTimeMillis(), + ) + } else { + AppVisibilityModel( + isAppCurrentlyVisible = false, + stateFlow.value.lastAppVisibleTime, + ) + } + } } } 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/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/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt index 703d6ad83eac..a209ec9d0c9c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt @@ -31,3 +31,6 @@ val Kosmos.systemClock by } val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() } + +val SystemClock.fake + get() = this as FakeSystemClock 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..183990c01072 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; } } @@ -1680,10 +1688,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); } } @@ -1930,10 +1939,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 b6a3f4041b13..0aa7227ac7e6 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2587,6 +2587,11 @@ public final class DisplayManagerService extends SystemService { sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED); } + private void handleLogicalDisplayCommittedStateChangedLocked(@NonNull LogicalDisplay display) { + sendDisplayEventIfEnabledLocked(display, + DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED); + } + private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) { mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked() .mDisplayDeviceConfig); @@ -4165,6 +4170,9 @@ public final class DisplayManagerService extends SystemService { case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED: handleLogicalDisplayStateChangedLocked(display); break; + case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED: + handleLogicalDisplayCommittedStateChangedLocked(display); + break; } } @@ -4419,6 +4427,9 @@ public final class DisplayManagerService extends SystemService { case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED: return (mask & DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0; + case DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + return (mask & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0; default: // This should never happen. Slog.e(TAG, "Unknown display event " + event); diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 02db051dff57..872f33484951 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -91,6 +91,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 1 << 8; public static final int LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED = 1 << 9; public static final int LOGICAL_DISPLAY_EVENT_STATE_CHANGED = 1 << 10; + public static final int LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED = 1 << 11; + public static final int DISPLAY_GROUP_EVENT_ADDED = 1; public static final int DISPLAY_GROUP_EVENT_CHANGED = 2; @@ -810,7 +812,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { int logicalDisplayEventMask = mLogicalDisplaysToUpdate .get(displayId, LOGICAL_DISPLAY_EVENT_BASE); boolean hasBasicInfoChanged = - !mTempDisplayInfo.equals(newDisplayInfo, /* compareRefreshRate */ false); + !mTempDisplayInfo.equals(newDisplayInfo, /* compareOnlyBasicChanges */ true); // The display is no longer valid and needs to be removed. if (!display.isValidLocked()) { // Remove from group @@ -930,6 +932,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); @@ -961,6 +964,11 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { && mTempDisplayInfo.state != newDisplayInfo.state) { mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED; } + + if (mFlags.isCommittedStateSeparateEventEnabled() + && mTempDisplayInfo.committedState != newDisplayInfo.committedState) { + mask |= LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED; + } return mask; } /** @@ -1360,6 +1368,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return "disconnected"; case LOGICAL_DISPLAY_EVENT_STATE_CHANGED: return "state_changed"; + case LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED: + return "committed_state_changed"; case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED: return "refresh_rate_changed"; case LOGICAL_DISPLAY_EVENT_BASIC_CHANGED: diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index bc5d90599b41..e4b595ab7c55 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -280,6 +280,11 @@ public class DisplayManagerFlags { Flags::refreshRateEventForForegroundApps ); + private final FlagState mCommittedStateSeparateEvent = new FlagState( + Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT, + Flags::committedStateSeparateEvent + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -603,6 +608,14 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if the flag for having a separate event for display's committed state + * is enabled + */ + public boolean isCommittedStateSeparateEventEnabled() { + return mCommittedStateSeparateEvent.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -659,6 +672,7 @@ public class DisplayManagerFlags { pw.println(" " + mBaseDensityForExternalDisplays); pw.println(" " + mFramerateOverrideTriggersRrCallbacks); pw.println(" " + mRefreshRateEventForForegroundApps); + pw.println(" " + mCommittedStateSeparateEvent); } private static class FlagState { 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 8211febade60..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 { @@ -509,3 +508,14 @@ flag { bug: "293651324" is_fixed_read_only: false } + +flag { + name: "committed_state_separate_event" + namespace: "display_manager" + description: "Move Display committed state into a separate event" + bug: "342192387" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} 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/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..70044bd813ce 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -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/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 4cf439611852..91a2843ccaf7 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -120,6 +120,8 @@ public class MediaQualityService extends SystemService { private final Object mPictureProfileLock = new Object(); // A global lock for sound profile objects. private final Object mSoundProfileLock = new Object(); + // A global lock for user state objects. + private final Object mUserStateLock = new Object(); // A global lock for ambient backlight objects. private final Object mAmbientBacklightLock = new Object(); @@ -181,7 +183,6 @@ public class MediaQualityService extends SystemService { publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } - // TODO: Add additional APIs. b/373951081 private final class BinderService extends IMediaQualityManager.Stub { @GuardedBy("mPictureProfileLock") @@ -269,12 +270,13 @@ public class MediaQualityService extends SystemService { mMqManagerNotifier.notifyOnPictureProfileError(id, PictureProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnPictureProfileRemoved( + mPictureProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mPictureProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); } - mMqManagerNotifier.notifyOnPictureProfileRemoved( - mPictureProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mPictureProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); } } } @@ -520,12 +522,13 @@ public class MediaQualityService extends SystemService { mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnSoundProfileRemoved( + mSoundProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mSoundProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); } - mMqManagerNotifier.notifyOnSoundProfileRemoved( - mSoundProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mSoundProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); } } } @@ -684,24 +687,22 @@ public class MediaQualityService extends SystemService { mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; } - //TODO: need lock here? @Override public void registerPictureProfileCallback(final IPictureProfileCallback callback) { int callingPid = Binder.getCallingPid(); int callingUid = Binder.getCallingUid(); - UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + UserState userState = getOrCreateUserState(Binder.getCallingUid()); userState.mPictureProfileCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); } - //TODO: need lock here? @Override public void registerSoundProfileCallback(final ISoundProfileCallback callback) { int callingPid = Binder.getCallingPid(); int callingUid = Binder.getCallingUid(); - UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + UserState userState = getOrCreateUserState(Binder.getCallingUid()); userState.mSoundProfileCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); } @@ -1060,7 +1061,7 @@ public class MediaQualityService extends SystemService { synchronized (mPictureProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); - UserState userState = getOrCreateUserStateLocked(userId); + UserState userState = getOrCreateUserState(userId); userState.mPictureProfileCallbackPidUidMap.remove(callback); } } @@ -1074,7 +1075,7 @@ public class MediaQualityService extends SystemService { synchronized (mSoundProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); - UserState userState = getOrCreateUserStateLocked(userId); + UserState userState = getOrCreateUserState(userId); userState.mSoundProfileCallbackPidUidMap.remove(callback); } } @@ -1100,19 +1101,23 @@ public class MediaQualityService extends SystemService { } } - //TODO: used by both picture and sound. can i add both locks? - private UserState getOrCreateUserStateLocked(int userId) { - UserState userState = getUserStateLocked(userId); + @GuardedBy("mUserStateLock") + private UserState getOrCreateUserState(int userId) { + UserState userState = getUserState(userId); if (userState == null) { userState = new UserState(mContext, userId); - mUserStates.put(userId, userState); + synchronized (mUserStateLock) { + mUserStates.put(userId, userState); + } } return userState; } - //TODO: used by both picture and sound. can i add both locks? - private UserState getUserStateLocked(int userId) { - return mUserStates.get(userId); + @GuardedBy("mUserStateLock") + private UserState getUserState(int userId) { + synchronized (mUserStateLock) { + return mUserStates.get(userId); + } } private final class MqDatabaseUtils { @@ -1266,7 +1271,7 @@ public class MediaQualityService extends SystemService { private void notifyPictureProfileHelper(int mode, String profileId, PictureProfile profile, Integer errorCode, List<ParameterCapability> paramCaps, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM); int n = userState.mPictureProfileCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { @@ -1351,7 +1356,7 @@ public class MediaQualityService extends SystemService { private void notifySoundProfileHelper(int mode, String profileId, SoundProfile profile, Integer errorCode, List<ParameterCapability> paramCaps, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM); int n = userState.mSoundProfileCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index d538bb876b64..c3af578de369 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -176,16 +176,13 @@ public class BackgroundInstallControlService extends SystemService { if (Flags.bicClient()) { mService.enforceCallerPermissions(); } - if (!Build.IS_DEBUGGABLE) { - return mService.getBackgroundInstalledPackages(flags, userId); - } // The debug.transparency.bg-install-apps (only works for debuggable builds) // is used to set mock list of background installed apps for testing. // The list of apps' names is delimited by ",". // TODO: Remove after migrating test to new background install method using // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905 String propertyString = SystemProperties.get("debug.transparency.bg-install-apps"); - if (TextUtils.isEmpty(propertyString)) { + if (TextUtils.isEmpty(propertyString) || !Build.IS_DEBUGGABLE) { return mService.getBackgroundInstalledPackages(flags, userId); } else { return mService.getMockBackgroundInstalledPackages(propertyString); @@ -219,10 +216,27 @@ public class BackgroundInstallControlService extends SystemService { PackageManager.PackageInfoFlags.of(flags), userId); initBackgroundInstalledPackages(); + if(Build.IS_DEBUGGABLE) { + StringBuilder sb = new StringBuilder(); + sb.append("Tracked background installed package size: ") + .append(mBackgroundInstalledPackages.size()) + .append("\n"); + for (int i = 0; i < mBackgroundInstalledPackages.size(); ++i) { + int installingUserId = mBackgroundInstalledPackages.keyAt(i); + mBackgroundInstalledPackages.get(installingUserId).forEach(pkgName -> + sb.append("userId: ").append(installingUserId) + .append(", name: ").append(pkgName).append("\n")); + } + Slog.d(TAG, "Tracked background installed package: " + sb.toString()); + } + ListIterator<PackageInfo> iter = packages.listIterator(); while (iter.hasNext()) { String packageName = iter.next().packageName; if (!mBackgroundInstalledPackages.contains(userId, packageName)) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, packageName + " is not tracked, removing"); + } iter.remove(); } } @@ -284,6 +298,9 @@ public class BackgroundInstallControlService extends SystemService { } void handlePackageAdd(String packageName, int userId) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd: checking " + packageName); + } ApplicationInfo appInfo = null; try { appInfo = @@ -302,7 +319,7 @@ public class BackgroundInstallControlService extends SystemService { installerPackageName = installInfo.getInstallingPackageName(); initiatingPackageName = installInfo.getInitiatingPackageName(); } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Package's installer not found " + packageName); + Slog.w(TAG, "Package's installer not found: " + packageName); return; } @@ -314,6 +331,10 @@ public class BackgroundInstallControlService extends SystemService { VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT, userId) != PERMISSION_GRANTED) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd " + packageName + ": installer doesn't " + + "have INSTALL_PACKAGES permission, skipping"); + } return; } @@ -324,6 +345,10 @@ public class BackgroundInstallControlService extends SystemService { if (installedByAdb(initiatingPackageName) || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd " + packageName + ": is installed by ADB or was " + + "foreground installation, skipping"); + } return; } 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/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 8fae875eb29b..e3eced252d1f 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -3379,7 +3379,7 @@ public final class PowerManagerService extends SystemService } changed = sleepPowerGroupLocked(powerGroup, time, PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID); - } else if (shouldNapAtBedTimeLocked()) { + } else if (shouldNapAtBedTimeLocked(powerGroup)) { changed = dreamPowerGroupLocked(powerGroup, time, Process.SYSTEM_UID, /* allowWake= */ false); } else { @@ -3395,7 +3395,10 @@ public final class PowerManagerService extends SystemService * activity timeout has expired and it's bedtime. */ @GuardedBy("mLock") - private boolean shouldNapAtBedTimeLocked() { + private boolean shouldNapAtBedTimeLocked(PowerGroup powerGroup) { + if (!powerGroup.supportsSandmanLocked()) { + return false; + } return mDreamsActivateOnSleepSetting || (mDreamsActivateOnDockSetting && mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) @@ -3617,9 +3620,10 @@ public final class PowerManagerService extends SystemService if (!mDreamsDisabledByAmbientModeSuppressionConfig) { return; } + final PowerGroup defaultPowerGroup = mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP); if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting - && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked( - mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) { + && shouldNapAtBedTimeLocked(defaultPowerGroup) + && isItBedTimeYetLocked(defaultPowerGroup)) { napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true); } else if (isSuppressed) { mDirty |= DIRTY_SETTINGS; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 6a5adca91e39..b607b0fce9ab 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -145,6 +145,7 @@ import android.util.SparseIntArray; import android.view.Display; import android.webkit.URLUtil; import android.window.ActivityWindowInfo; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -2916,6 +2917,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { /** The helper to calculate whether a container is opaque. */ static class OpaqueContainerHelper implements Predicate<ActivityRecord> { + private final boolean mEnableMultipleDesktopsBackend = + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue(); private ActivityRecord mStarting; private boolean mIgnoringInvisibleActivity; private boolean mIgnoringKeyguard; @@ -2938,7 +2941,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mIgnoringKeyguard = ignoringKeyguard; final boolean isOpaque; - if (!Flags.enableMultipleDesktopsBackend()) { + if (!mEnableMultipleDesktopsBackend) { isOpaque = container.getActivity(this, true /* traverseTopToBottom */, null /* boundary */) != null; } else { @@ -2949,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/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 3abab8bf62c2..bf9883c76a06 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -166,6 +166,7 @@ import android.view.InsetsState; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import android.window.ITaskOrganizer; import android.window.PictureInPictureSurfaceTransaction; @@ -2378,7 +2379,7 @@ class Task extends TaskFragment { // configurations and let its parent (organized task) to control it; final Task rootTask = getRootTask(); boolean shouldInheritBounds = rootTask != this && rootTask.isOrganized(); - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { // Only inherit from organized parent when this task is not organized. shouldInheritBounds &= !isOrganized(); } 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/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 7d3cd8a8a9ae..38de7ce013c2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -34,6 +34,7 @@ import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REM import static com.android.server.display.DisplayDeviceInfo.DIFF_EVERYTHING; import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED; +import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED; @@ -1180,13 +1181,21 @@ public class LogicalDisplayMapperTest { assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + // Change the display committed state + when(mFlagsMock.isCommittedStateSeparateEventEnabled()).thenReturn(true); + newDisplayInfo = new DisplayInfo(); + newDisplayInfo.committedState = STATE_OFF; + assertEquals(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED, + mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); // Change multiple properties newDisplayInfo = new DisplayInfo(); newDisplayInfo.refreshRateOverride = 30; newDisplayInfo.state = STATE_OFF; + newDisplayInfo.committedState = STATE_OFF; assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED - | LOGICAL_DISPLAY_EVENT_STATE_CHANGED, + | LOGICAL_DISPLAY_EVENT_STATE_CHANGED + | LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); } diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 29a17e1c85ab..ff6796561926 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -2501,6 +2501,49 @@ public class PowerManagerServiceTest { } @Test + public void testMultiDisplay_twoDisplays_onlyDefaultDisplayCanDream() { + final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; + final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1; + final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener = + new AtomicReference<>(); + doAnswer((Answer<Void>) invocation -> { + listener.set(invocation.getArgument(0)); + return null; + }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any()); + final DisplayInfo info = new DisplayInfo(); + info.displayGroupId = nonDefaultDisplayGroupId; + when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info); + when(mBatteryManagerInternalMock.isPowered(anyInt())).thenReturn(true); + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1); + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + setMinimumScreenOffTimeoutConfig(5); + createService(); + startSystem(); + + listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId); + + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_AWAKE); + assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo( + WAKEFULNESS_AWAKE); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + + advanceTime(15000); + + // Only the default display group is dreaming. + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_DREAMING); + assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo( + WAKEFULNESS_DOZING); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + } + + @Test public void testMultiDisplay_addNewDisplay_becomeGloballyAwakeButDefaultRemainsDozing() { final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1; diff --git a/services/tests/powerstatstests/res/raw/battery-history.zip b/services/tests/powerstatstests/res/raw/battery-history.zip Binary files differnew file mode 100644 index 000000000000..ed82ac0f79cc --- /dev/null +++ b/services/tests/powerstatstests/res/raw/battery-history.zip diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java new file mode 100644 index 000000000000..8fc8c9f677a6 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.res.Resources; +import android.os.BatteryConsumer; +import android.os.BatteryUsageStats; +import android.os.BatteryUsageStatsQuery; +import android.os.ConditionVariable; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; + +import com.android.internal.os.Clock; +import com.android.internal.os.CpuScalingPolicies; +import com.android.internal.os.CpuScalingPolicyReader; +import com.android.internal.os.MonotonicClock; +import com.android.internal.os.PowerProfile; +import com.android.server.power.stats.processor.MultiStatePowerAttributor; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test") +@Ignore("Performance experiment. Comment out @Ignore to run") +public class BatteryUsageStatsProviderPerfTest { + @Rule + public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private final Clock mClock = new MockClock(); + private MonotonicClock mMonotonicClock; + private PowerProfile mPowerProfile; + private CpuScalingPolicies mCpuScalingPolicies; + private File mDirectory; + private Handler mHandler; + private MockBatteryStatsImpl mBatteryStats; + + @Before + public void setup() throws Exception { + Context context = InstrumentationRegistry.getContext(); + mPowerProfile = new PowerProfile(context); + mCpuScalingPolicies = new CpuScalingPolicyReader().read(); + + HandlerThread mHandlerThread = new HandlerThread("batterystats-handler"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + // Extract accumulated battery history to ensure consistent iterations + mDirectory = Files.createTempDirectory("BatteryUsageStatsProviderPerfTest").toFile(); + File historyDirectory = new File(mDirectory, "battery-history"); + historyDirectory.mkdir(); + + long maxMonotonicTime = 0; + + // To recreate battery-history.zip if necessary, perform these commands: + // cd /tmp + // mkdir battery-history + // adb pull /data/system/battery-history + // zip battery-history.zip battery-history/* + // cp battery-history.zip \ + // $ANDROID_BUILD_TOP/frameworks/base/services/tests/powerstatstests/res/raw + Resources resources = context.getResources(); + int resId = resources.getIdentifier("battery-history", "raw", context.getPackageName()); + try (InputStream in = resources.openRawResource(resId)) { + try (ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry ze; + while ((ze = zis.getNextEntry()) != null) { + if (!ze.getName().endsWith(".bh")) { + continue; + } + File file = new File(mDirectory, ze.getName()); + try (OutputStream out = new FileOutputStream( + file)) { + FileUtils.copy(zis, out); + } + long timestamp = Long.parseLong(file.getName().replace(".bh", "")); + if (timestamp > maxMonotonicTime) { + maxMonotonicTime = timestamp; + } + } + } + } + + mMonotonicClock = new MonotonicClock(maxMonotonicTime + 1000000000, mClock); + mBatteryStats = new MockBatteryStatsImpl(mClock, mDirectory); + } + + @Test + public void getBatteryUsageStats_accumulated() { + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .setMaxStatsAgeMs(0) + .includePowerStateData() + .includeScreenStateData() + .includeProcessStateData() + .accumulated() + .build(); + + double expectedCpuPower = 0; + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + + waitForBackgroundThread(); + + BatteryUsageStatsProvider provider = createBatteryUsageStatsProvider(); + state.resumeTiming(); + + BatteryUsageStats stats = provider.getBatteryUsageStats(mBatteryStats, query); + waitForBackgroundThread(); + + state.pauseTiming(); + + double cpuConsumedPower = stats.getAggregateBatteryConsumer( + BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE) + .getConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU); + assertThat(cpuConsumedPower).isNonZero(); + if (expectedCpuPower == 0) { + expectedCpuPower = cpuConsumedPower; + } else { + // Verify that all iterations produce the same result + assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower); + } + state.resumeTiming(); + } + } + + private BatteryUsageStatsProvider createBatteryUsageStatsProvider() { + Context context = InstrumentationRegistry.getContext(); + + PowerStatsStore store = new PowerStatsStore(mDirectory, mHandler); + store.reset(); + + MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(context, store, + mPowerProfile, mCpuScalingPolicies, mPowerProfile::getBatteryCapacity); + return new BatteryUsageStatsProvider(context, powerAttributor, mPowerProfile, + mCpuScalingPolicies, store, 10000000, mClock, mMonotonicClock); + } + + private void waitForBackgroundThread() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} 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/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/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/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 0b3d720bf52a..1a932859b750 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -10207,6 +10207,17 @@ public class CarrierConfigManager { "carrier_supported_satellite_notification_hysteresis_sec_int"; /** + * Satellite notification display restriction reset time in seconds. + * + * The device shows a notification when it connects to a satellite. If the user interacts + * with the notification, it won't be shown again immediately. Instead, the notification + * will only reappear after below key mentioned amount of time has passed. + */ + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = + "satellite_connected_notification_throttle_millis_int"; + + /** * An integer key holds the timeout duration in seconds used to determine whether to exit * carrier-roaming NB-IOT satellite mode. * @@ -11428,6 +11439,10 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT, SatelliteManager.EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911); sDefaults.putInt(KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT, 180); + if (Flags.starlinkDataBugfix()) { + sDefaults.putLong(KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT, + TimeUnit.DAYS.toMillis(7)); + } sDefaults.putInt(KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30); sDefaults.putInt(KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT, 180); sDefaults.putInt(KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT, 600); 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/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index e24fe07f959b..9ef8b7dc9947 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -349,20 +349,22 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& value->value->Accept(&body_printer); printer->Undent(); } - printer->Println("Flag disabled values:"); - for (const auto& value : entry.flag_disabled_values) { - printer->Print("("); - printer->Print(value->config.to_string()); - printer->Print(") "); - value->value->Accept(&headline_printer); - if (options.show_sources && !value->value->GetSource().path.empty()) { - printer->Print(" src="); - printer->Print(value->value->GetSource().to_string()); + if (!entry.flag_disabled_values.empty()) { + printer->Println("Flag disabled values:"); + for (const auto& value : entry.flag_disabled_values) { + printer->Print("("); + printer->Print(value->config.to_string()); + printer->Print(") "); + value->value->Accept(&headline_printer); + if (options.show_sources && !value->value->GetSource().path.empty()) { + printer->Print(" src="); + printer->Print(value->value->GetSource().to_string()); + } + printer->Println(); + printer->Indent(); + value->value->Accept(&body_printer); + printer->Undent(); } - printer->Println(); - printer->Indent(); - value->value->Accept(&body_printer); - printer->Undent(); } printer->Undent(); } |