diff options
240 files changed, 6562 insertions, 2402 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 0298c1e627ee..251776e907d8 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -2995,6 +2995,8 @@ public class AlarmManagerService extends SystemService { pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS, Flags.startUserBeforeScheduledAlarms()); pw.println(); + pw.print(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND, Flags.acquireWakelockBeforeSend()); + pw.println(); pw.decreaseIndent(); pw.println(); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 458c1715d2b6..248f191cb8b8 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1651,9 +1651,65 @@ public class AppOpsManager { /** @hide Similar to {@link OP_CONTROL_AUDIO}, but doesn't require capabilities. */ public static final int OP_CONTROL_AUDIO_PARTIAL = AppOpEnums.APP_OP_CONTROL_AUDIO_PARTIAL; + /** + * Access coarse eye tracking data. + * + * @hide + */ + public static final int OP_EYE_TRACKING_COARSE = + AppOpEnums.APP_OP_EYE_TRACKING_COARSE; + + /** + * Access fine eye tracking data. + * + * @hide + */ + public static final int OP_EYE_TRACKING_FINE = + AppOpEnums.APP_OP_EYE_TRACKING_FINE; + + /** + * Access face tracking data. + * + * @hide + */ + public static final int OP_FACE_TRACKING = + AppOpEnums.APP_OP_FACE_TRACKING; + + /** + * Access hand tracking data. + * + * @hide + */ + public static final int OP_HAND_TRACKING = + AppOpEnums.APP_OP_HAND_TRACKING; + + /** + * Access head tracking data. + * + * @hide + */ + public static final int OP_HEAD_TRACKING = + AppOpEnums.APP_OP_HEAD_TRACKING; + + /** + * Access coarse scene tracking data. + * + * @hide + */ + public static final int OP_SCENE_UNDERSTANDING_COARSE = + AppOpEnums.APP_OP_SCENE_UNDERSTANDING_COARSE; + + /** + * Access fine scene tracking data. + * + * @hide + */ + public static final int OP_SCENE_UNDERSTANDING_FINE = + AppOpEnums.APP_OP_SCENE_UNDERSTANDING_FINE; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 156; + public static final int _NUM_OP = 163; /** * All app ops represented as strings. @@ -1813,6 +1869,13 @@ public class AppOpsManager { OPSTR_WRITE_SYSTEM_PREFERENCES, OPSTR_CONTROL_AUDIO, OPSTR_CONTROL_AUDIO_PARTIAL, + OPSTR_EYE_TRACKING_COARSE, + OPSTR_EYE_TRACKING_FINE, + OPSTR_FACE_TRACKING, + OPSTR_HAND_TRACKING, + OPSTR_HEAD_TRACKING, + OPSTR_SCENE_UNDERSTANDING_COARSE, + OPSTR_SCENE_UNDERSTANDING_FINE, }) public @interface AppOpString {} @@ -2579,6 +2642,36 @@ public class AppOpsManager { /** @hide Access to a audio playback and control APIs without capability requirements */ public static final String OPSTR_CONTROL_AUDIO_PARTIAL = "android:control_audio_partial"; + /** @hide Access coarse eye tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_EYE_TRACKING_COARSE = "android:eye_tracking_coarse"; + + /** @hide Access fine eye tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_EYE_TRACKING_FINE = "android:eye_tracking_fine"; + + /** @hide Access face tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_FACE_TRACKING = "android:face_tracking"; + + /** @hide Access hand tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_HAND_TRACKING = "android:hand_tracking"; + + /** @hide Access head tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_HEAD_TRACKING = "android:head_tracking"; + + /** @hide Access coarse scene tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_SCENE_UNDERSTANDING_COARSE = + "android:scene_understanding_coarse"; + + /** @hide Access fine scene tracking data. */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String OPSTR_SCENE_UNDERSTANDING_FINE = + "android:scene_understanding_fine"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2657,6 +2750,14 @@ public class AppOpsManager { Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE, Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE, Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE, + // Android XR + android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_COARSE : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_FINE : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_FACE_TRACKING : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_HAND_TRACKING : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_HEAD_TRACKING : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_COARSE : OP_NONE, + android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_FINE : OP_NONE, }; /** @@ -3192,6 +3293,41 @@ public class AppOpsManager { "CONTROL_AUDIO").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(), new AppOpInfo.Builder(OP_CONTROL_AUDIO_PARTIAL, OPSTR_CONTROL_AUDIO_PARTIAL, "CONTROL_AUDIO_PARTIAL").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(), + new AppOpInfo.Builder(OP_EYE_TRACKING_COARSE, OPSTR_EYE_TRACKING_COARSE, + "EYE_TRACKING_COARSE") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.EYE_TRACKING_COARSE : null) + .build(), + new AppOpInfo.Builder(OP_EYE_TRACKING_FINE, OPSTR_EYE_TRACKING_FINE, + "EYE_TRACKING_FINE") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.EYE_TRACKING_FINE : null) + .build(), + new AppOpInfo.Builder(OP_FACE_TRACKING, OPSTR_FACE_TRACKING, + "FACE_TRACKING") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.FACE_TRACKING : null) + .build(), + new AppOpInfo.Builder(OP_HAND_TRACKING, OPSTR_HAND_TRACKING, + "HAND_TRACKING") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.HAND_TRACKING : null) + .build(), + new AppOpInfo.Builder(OP_HEAD_TRACKING, OPSTR_HEAD_TRACKING, + "HEAD_TRACKING") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.HEAD_TRACKING : null) + .build(), + new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_COARSE, OPSTR_SCENE_UNDERSTANDING_COARSE, + "SCENE_UNDERSTANDING_COARSE") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.SCENE_UNDERSTANDING_COARSE : null) + .build(), + new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_FINE, OPSTR_SCENE_UNDERSTANDING_FINE, + "SCENE_UNDERSTANDING_FINE") + .setPermission(android.xr.Flags.xrManifestEntries() + ? Manifest.permission.SCENE_UNDERSTANDING_FINE : null) + .build(), }; // The number of longs needed to form a full bitmask of app ops @@ -3301,6 +3437,15 @@ public class AppOpsManager { } /** + * Returns whether the provided {@code op} is a valid op code or not. + * + * @hide + */ + public static boolean isValidOp(int op) { + return op >= 0 && op < sAppOpInfos.length; + } + + /** * @hide */ public static int strDebugOpToOp(String op) { diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index fa977c93113a..2daa52b47102 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -228,7 +228,7 @@ public final class AutomaticZenRule implements Parcelable { public AutomaticZenRule(Parcel source) { enabled = source.readInt() == ENABLED; if (source.readInt() == ENABLED) { - name = getTrimmedString(source.readString()); + name = getTrimmedString(source.readString8()); } interruptionFilter = source.readInt(); conditionId = getTrimmedUri(source.readParcelable(null, android.net.Uri.class)); @@ -238,11 +238,11 @@ public final class AutomaticZenRule implements Parcelable { source.readParcelable(null, android.content.ComponentName.class)); creationTime = source.readLong(); mZenPolicy = source.readParcelable(null, ZenPolicy.class); - mPkg = source.readString(); + mPkg = source.readString8(); mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); mAllowManualInvocation = source.readBoolean(); mIconResId = source.readInt(); - mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); + mTriggerDescription = getTrimmedString(source.readString8(), MAX_DESC_LENGTH); mType = source.readInt(); } @@ -514,7 +514,7 @@ public final class AutomaticZenRule implements Parcelable { dest.writeInt(enabled ? ENABLED : DISABLED); if (name != null) { dest.writeInt(1); - dest.writeString(name); + dest.writeString8(name); } else { dest.writeInt(0); } @@ -524,11 +524,11 @@ public final class AutomaticZenRule implements Parcelable { dest.writeParcelable(configurationActivity, 0); dest.writeLong(creationTime); dest.writeParcelable(mZenPolicy, 0); - dest.writeString(mPkg); + dest.writeString8(mPkg); dest.writeParcelable(mDeviceEffects, 0); dest.writeBoolean(mAllowManualInvocation); dest.writeInt(mIconResId); - dest.writeString(mTriggerDescription); + dest.writeString8(mTriggerDescription); dest.writeInt(mType); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index eb9feb95bf3d..8af5b1bd40f8 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -189,6 +189,7 @@ public class Instrumentation { * @param arguments Any additional arguments that were supplied when the * instrumentation was started. */ + @android.ravenwood.annotation.RavenwoodKeep public void onCreate(Bundle arguments) { } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 719e4389d92d..1b71e73db852 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -25,6 +25,9 @@ import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICO import static android.app.admin.DevicePolicyResources.UNDEFINED; import static android.graphics.drawable.Icon.TYPE_URI; import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; +import static android.util.TypedValue.COMPLEX_UNIT_PX; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static java.util.Objects.requireNonNull; @@ -6001,6 +6004,8 @@ public class Notification implements Parcelable contentView.setViewVisibility(p.mTextViewId, View.GONE); contentView.setTextViewText(p.mTextViewId, null); } + + updateExpanderAlignment(contentView, p, hasSecondLine); setHeaderlessVerticalMargins(contentView, p, hasSecondLine); // Update margins to leave space for the top line (but not for headerless views like @@ -6010,12 +6015,29 @@ public class Notification implements Parcelable int margin = getContentMarginTop(mContext, R.dimen.notification_2025_content_margin_top); contentView.setViewLayoutMargin(R.id.notification_main_column, - RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX); + RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX); } return contentView; } + private static void updateExpanderAlignment(RemoteViews contentView, + StandardTemplateParams p, boolean hasSecondLine) { + if (notificationsRedesignTemplates() && p.mHeaderless) { + if (!hasSecondLine) { + // If there's no text, let's center the expand button vertically to align things + // more nicely. This is handled separately for notifications that use a + // NotificationHeaderView, see NotificationHeaderView#centerTopLine. + contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT, + COMPLEX_UNIT_PX); + } else { + // Otherwise, just use the default height for the button to keep it top-aligned. + contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT, + COMPLEX_UNIT_PX); + } + } + } + private static void setHeaderlessVerticalMargins(RemoteViews contentView, StandardTemplateParams p, boolean hasSecondLine) { if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) { @@ -9560,7 +9582,7 @@ public class Notification implements Parcelable int marginStart = res.getDimensionPixelSize( R.dimen.notification_2025_content_margin_start); contentView.setViewLayoutMargin(R.id.title, - RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX); + RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX); } if (isLegacyHeaderless) { // Collapsed legacy messaging style has a 1-line limit. diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 7a811a1cdfb8..5b0cf1158d99 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -132,7 +132,7 @@ per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS - +per-file Presentation.java = file:/services/core/java/com/android/server/wm/OWNERS # Multitasking per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS diff --git a/core/java/android/app/Presentation.java b/core/java/android/app/Presentation.java index bdab39dcd2ac..f39e2dd8cfa2 100644 --- a/core/java/android/app/Presentation.java +++ b/core/java/android/app/Presentation.java @@ -20,6 +20,8 @@ import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; + import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -34,6 +36,8 @@ import android.view.ContextThemeWrapper; import android.view.Display; import android.view.Gravity; import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; import android.view.WindowManager; import android.view.WindowManager.LayoutParams.WindowType; @@ -277,6 +281,11 @@ public class Presentation extends Dialog { @Override public void show() { super.show(); + + WindowInsetsController controller = getWindow().getInsetsController(); + if (controller != null && enablePresentationForConnectedDisplays()) { + controller.hide(WindowInsets.Type.systemBars()); + } } /** diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 67ade79e1b94..0085e4f42397 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -143,3 +143,10 @@ flag { is_fixed_read_only: true bug: "370928384" } + +flag { + name: "device_aware_settings_override" + namespace: "virtual_devices" + description: "Settings override for virtual devices" + bug: "371801645" +} diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index c4af87116eed..bebca57125b6 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -1499,7 +1499,7 @@ public final class DisplayManagerGlobal { } @VisibleForTesting - static final class DisplayListenerDelegate { + public static final class DisplayListenerDelegate { public final DisplayListener mListener; public volatile long mInternalEventFlagsMask; @@ -1536,7 +1536,7 @@ public final class DisplayManagerGlobal { } @VisibleForTesting - boolean isEventFilterExplicit() { + public boolean isEventFilterExplicit() { return mIsEventFilterExplicit; } @@ -1892,7 +1892,7 @@ public final class DisplayManagerGlobal { } @VisibleForTesting - CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() { + public CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() { return mDisplayListeners; } } diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 953ee08800cf..5b5360e1ff01 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -485,6 +485,9 @@ public final class ContextHubManager { /** * Returns the list of ContextHubInfo objects describing the available Context Hubs. * + * To find the list of hubs that include all Hubs (including both Context Hubs and Vendor Hubs), + * use the {@link #getHubs()} method instead. + * * @return the list of ContextHubInfo objects * * @see ContextHubInfo @@ -499,8 +502,8 @@ public final class ContextHubManager { } /** - * Returns the list of HubInfo objects describing the available hubs (including ContextHub and - * VendorHub). This method is primarily used for debugging purposes as most clients care about + * Returns the list of HubInfo objects describing the available hubs (including Context Hubs and + * Vendor Hubs). This method is primarily used for debugging purposes as most clients care about * endpoints and services more than hubs. * * @return the list of HubInfo objects diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 4cbd5beb3a8c..1cf43d455be8 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -2636,7 +2636,7 @@ public class ZenModeConfig implements Parcelable { enabled = source.readInt() == 1; snoozing = source.readInt() == 1; if (source.readInt() == 1) { - name = source.readString(); + name = source.readString8(); } zenMode = source.readInt(); conditionId = source.readParcelable(null, android.net.Uri.class); @@ -2644,18 +2644,18 @@ public class ZenModeConfig implements Parcelable { component = source.readParcelable(null, android.content.ComponentName.class); configurationActivity = source.readParcelable(null, android.content.ComponentName.class); if (source.readInt() == 1) { - id = source.readString(); + id = source.readString8(); } creationTime = source.readLong(); if (source.readInt() == 1) { - enabler = source.readString(); + enabler = source.readString8(); } zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class); zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); - pkg = source.readString(); + pkg = source.readString8(); allowManualInvocation = source.readBoolean(); - iconResName = source.readString(); - triggerDescription = source.readString(); + iconResName = source.readString8(); + triggerDescription = source.readString8(); type = source.readInt(); userModifiedFields = source.readInt(); zenPolicyUserModifiedFields = source.readInt(); @@ -2703,7 +2703,7 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(snoozing ? 1 : 0); if (name != null) { dest.writeInt(1); - dest.writeString(name); + dest.writeString8(name); } else { dest.writeInt(0); } @@ -2714,23 +2714,23 @@ public class ZenModeConfig implements Parcelable { dest.writeParcelable(configurationActivity, 0); if (id != null) { dest.writeInt(1); - dest.writeString(id); + dest.writeString8(id); } else { dest.writeInt(0); } dest.writeLong(creationTime); if (enabler != null) { dest.writeInt(1); - dest.writeString(enabler); + dest.writeString8(enabler); } else { dest.writeInt(0); } dest.writeParcelable(zenPolicy, 0); dest.writeParcelable(zenDeviceEffects, 0); - dest.writeString(pkg); + dest.writeString8(pkg); dest.writeBoolean(allowManualInvocation); - dest.writeString(iconResName); - dest.writeString(triggerDescription); + dest.writeString8(iconResName); + dest.writeString8(triggerDescription); dest.writeInt(type); dest.writeInt(userModifiedFields); dest.writeInt(zenPolicyUserModifiedFields); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index f58baffb1367..4fc894ca9ff4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -789,6 +789,12 @@ interface IWindowManager in @nullable ImeTracker.Token statsToken); /** + * Updates the currently animating insets types of a remote process. + */ + @EnforcePermission("MANAGE_APP_TOKENS") + void updateDisplayWindowAnimatingTypes(int displayId, int animatingTypes); + + /** * Called to get the expected window insets. * * @return {@code true} if system bars are always consumed. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 1f8f0820ca3a..7d6d5a269b4c 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -272,6 +272,15 @@ interface IWindowSession { in @nullable ImeTracker.Token imeStatsToken); /** + * Notifies WindowState what insets types are currently running within the Window. + * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning). + * + * @param window The window that is insets animaiton is running. + * @param animatingTypes Indicates the currently animating insets types. + */ + oneway void updateAnimatingTypes(IWindow window, int animatingTypes); + + /** * Called when the system gesture exclusion has changed. */ oneway void reportSystemGestureExclusionChanged(IWindow window, in List<Rect> exclusionRects); @@ -372,14 +381,4 @@ interface IWindowSession { */ oneway void notifyImeWindowVisibilityChangedFromClient(IWindow window, boolean visible, in ImeTracker.Token statsToken); - - /** - * Notifies WindowState whether inset animations are currently running within the Window. - * This value is used by the server to vote for refresh rate. - * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning). - * - * @param window The window that is insets animaiton is running. - * @param running Indicates the insets animation state. - */ - oneway void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running); } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 0d82acd2bdf0..462c5c630759 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -211,12 +211,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } /** - * Notifies when the state of running animation is changed. The state is either "running" or - * "idle". + * Notifies when the insets types of running animation have changed. The animatingTypes + * contain all types, which have an ongoing animation. * - * @param running {@code true} if there is any animation running; {@code false} otherwise. + * @param animatingTypes the {@link InsetsType}s that are currently animating */ - default void notifyAnimationRunningStateChanged(boolean running) {} + default void updateAnimatingTypes(@InsetsType int animatingTypes) {} /** @see ViewRootImpl#isHandlingPointerEvent */ default boolean isHandlingPointerEvent() { @@ -665,6 +665,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** Set of inset types which are requested visible which are reported to the host */ private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + /** Set of insets types which are currently animating */ + private @InsetsType int mAnimatingTypes = 0; + /** Set of inset types that we have controls of */ private @InsetsType int mControllableTypes; @@ -745,9 +748,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mFrame, mFromState, mToState, RESIZE_INTERPOLATOR, ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this); if (mRunningAnimations.isEmpty()) { - mHost.notifyAnimationRunningStateChanged(true); + mHost.updateAnimatingTypes(runner.getTypes()); } mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType())); + mAnimatingTypes |= runner.getTypes(); } }; @@ -1564,9 +1568,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING); - if (mRunningAnimations.isEmpty()) { - mHost.notifyAnimationRunningStateChanged(true); - } + mAnimatingTypes |= runner.getTypes(); + mHost.updateAnimatingTypes(mAnimatingTypes); mRunningAnimations.add(new RunningAnimation(runner, animationType)); if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: " + useInsetsAnimationThread); @@ -1827,7 +1830,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation dispatchAnimationEnd(runningAnimation.runner.getAnimation()); } else { if (Flags.refactorInsetsController()) { - if (removedTypes == ime() + if ((removedTypes & ime()) != 0 && control.getAnimationType() == ANIMATION_TYPE_HIDE) { if (mHost != null) { // if the (hide) animation is cancelled, the @@ -1842,9 +1845,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation break; } } - if (mRunningAnimations.isEmpty()) { - mHost.notifyAnimationRunningStateChanged(false); + if (removedTypes > 0) { + mAnimatingTypes &= ~removedTypes; + mHost.updateAnimatingTypes(mAnimatingTypes); } + onAnimationStateChanged(removedTypes, false /* running */); } @@ -1969,14 +1974,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return animatingTypes; } - private @InsetsType int computeAnimatingTypes() { - int animatingTypes = 0; - for (int i = 0; i < mRunningAnimations.size(); i++) { - animatingTypes |= mRunningAnimations.get(i).runner.getTypes(); - } - return animatingTypes; - } - /** * Called when finishing setting requested visible types or finishing setting controls. * @@ -1989,7 +1986,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // report its requested visibility at the end of the animation, otherwise we would // lose the leash, and it would disappear during the animation // TODO(b/326377046) revisit this part and see if we can make it more general - typesToReport = mRequestedVisibleTypes | (computeAnimatingTypes() & ime()); + typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime()); } else { typesToReport = mRequestedVisibleTypes; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9498407fb33b..7fd7be8585a4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2540,11 +2540,12 @@ public final class ViewRootImpl implements ViewParent, } /** - * Notify the when the running state of a insets animation changed. + * Notify the when the animating insets types have changed. */ @VisibleForTesting - public void notifyInsetsAnimationRunningStateChanged(boolean running) { + public void updateAnimatingTypes(@InsetsType int animatingTypes) { if (sToolkitSetFrameRateReadOnlyFlagValue) { + boolean running = animatingTypes != 0; if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.instant(Trace.TRACE_TAG_VIEW, TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)", @@ -2552,7 +2553,7 @@ public final class ViewRootImpl implements ViewParent, } mInsetsAnimationRunning = running; try { - mWindowSession.notifyInsetsAnimationRunningStateChanged(mWindow, running); + mWindowSession.updateAnimatingTypes(mWindow, animatingTypes); } catch (RemoteException e) { } } diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java index 889acca4b8b1..8954df6b1aaa 100644 --- a/core/java/android/view/ViewRootInsetsControllerHost.java +++ b/core/java/android/view/ViewRootInsetsControllerHost.java @@ -171,6 +171,13 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host { } @Override + public void updateAnimatingTypes(@WindowInsets.Type.InsetsType int animatingTypes) { + if (mViewRoot != null) { + mViewRoot.updateAnimatingTypes(animatingTypes); + } + } + + @Override public boolean hasAnimationCallbacks() { if (mViewRoot.mView == null) { return false; @@ -275,13 +282,6 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host { } @Override - public void notifyAnimationRunningStateChanged(boolean running) { - if (mViewRoot != null) { - mViewRoot.notifyInsetsAnimationRunningStateChanged(running); - } - } - - @Override public boolean isHandlingPointerEvent() { return mViewRoot != null && mViewRoot.isHandlingPointerEvent(); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 24647f459ab5..196ae5e59fa7 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -625,6 +625,12 @@ public interface WindowManager extends ViewManager { int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000 /** + * Transition flag: Indicates that aod is showing hidden by entering doze + * @hide + */ + int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000 + + /** * @hide */ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = { @@ -643,6 +649,7 @@ public interface WindowManager extends ViewManager { TRANSIT_FLAG_KEYGUARD_OCCLUDING, TRANSIT_FLAG_KEYGUARD_UNOCCLUDING, TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH, + TRANSIT_FLAG_AOD_APPEARING, }) @Retention(RetentionPolicy.SOURCE) @interface TransitionFlags {} @@ -659,7 +666,8 @@ public interface WindowManager extends ViewManager { (TRANSIT_FLAG_KEYGUARD_GOING_AWAY | TRANSIT_FLAG_KEYGUARD_APPEARING | TRANSIT_FLAG_KEYGUARD_OCCLUDING - | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING); + | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING + | TRANSIT_FLAG_AOD_APPEARING); /** * Remove content mode: Indicates remove content mode is currently not defined. diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 72a595d95ec2..0a86ff89c53c 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -597,6 +597,11 @@ public class WindowlessWindowManager implements IWindowSession { } @Override + public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) { + // NO-OP + } + + @Override public void reportSystemGestureExclusionChanged(android.view.IWindow window, List<Rect> exclusionRects) { } @@ -679,11 +684,6 @@ public class WindowlessWindowManager implements IWindowSession { @NonNull ImeTracker.Token statsToken) { } - @Override - public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) { - // NO-OP - } - void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) { IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder(); IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder(); diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 49a11cab1de9..80a9cbc2f859 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -235,6 +235,14 @@ flag { } flag { + name: "request_rectangle_with_source" + namespace: "accessibility" + description: "Request rectangle on screen with source parameter" + bug: "391877896" + is_exported: true +} + +flag { name: "restore_a11y_secure_settings_on_hsum_device" namespace: "accessibility" description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user" diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index aa0111a13b8e..60178cde249f 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -225,6 +225,7 @@ public interface ImeTracker { PHASE_SERVER_UPDATE_CLIENT_VISIBILITY, PHASE_WM_DISPLAY_IME_CONTROLLER_SET_IME_REQUESTED_VISIBLE, PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES, + PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED, }) @Retention(RetentionPolicy.SOURCE) @interface Phase {} @@ -445,6 +446,9 @@ public interface ImeTracker { /** The control target reported its requestedVisibleTypes back to WindowManagerService. */ int PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES = ImeProtoEnums.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES; + /** The requestedVisibleTypes have not been changed, so this request is not continued. */ + int PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED = + ImeProtoEnums.PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED; /** * Called when an IME request is started. diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 16f41146fd6b..c81c2bbc2f27 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -196,3 +196,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "report_animating_insets_types" + namespace: "input_method" + description: "Adding animating insets types and report IME visibility at the beginning of hiding" + bug: "393049691" +} diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 118edc29f378..fa7b74f37ed0 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -242,7 +242,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback @Override public void onNullBinding(ComponentName name) { - enqueueDeferredUnbindServiceMessage(); + unbindNow(); } @Override diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 99fe0cbdca25..5e828ba46df7 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -5211,7 +5211,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @Nullable public String getFontVariationSettings() { - return mTextPaint.getFontVariationSettings(); + if (Flags.typefaceRedesignReadonly()) { + return mTextPaint.getFontVariationOverride(); + } else { + return mTextPaint.getFontVariationSettings(); + } } /** @@ -5567,10 +5571,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Math.clamp(400 + mFontWeightAdjustment, FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); } - mTextPaint.setFontVariationSettings( + mTextPaint.setFontVariationOverride( FontVariationAxis.toFontVariationSettings(axes)); } else { - mTextPaint.setFontVariationSettings(fontVariationSettings); + mTextPaint.setFontVariationOverride(fontVariationSettings); } effective = true; } else { diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 4aeedbb72903..42bf6d150bb4 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -97,6 +97,8 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true), ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false), + ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX( + Flags::enableDragToDesktopIncomingTransitionsBugfix, false), ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 2e36e9a205bf..684f320162c0 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -811,4 +811,13 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} +flag { + name: "enable_drag_to_desktop_incoming_transitions_bugfix" + namespace: "lse_desktop_experience" + description: "Enables bugfix handling incoming transitions during the DragToDesktop transition." + bug: "397135730" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java index 69c04807c604..7ee22f30ace0 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java @@ -157,7 +157,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable @Override public void writeToParcel(Parcel dest, int flags) { - sForInternedString.parcel(this.name, dest, flags); + dest.writeString(this.name); dest.writeInt(this.getIcon()); dest.writeInt(this.getLabelRes()); dest.writeCharSequence(this.getNonLocalizedLabel()); @@ -175,7 +175,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable // We use the boot classloader for all classes that we load. final ClassLoader boot = Object.class.getClassLoader(); //noinspection ConstantConditions - this.name = sForInternedString.unparcel(in); + this.name = in.readString(); this.icon = in.readInt(); this.labelRes = in.readInt(); this.nonLocalizedLabel = in.readCharSequence(); diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 7018ebcbe9f4..5a180d7358dd 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -82,7 +82,7 @@ oneway interface IStatusBar * Notify system UI the immersive mode changed. This shall be removed when client immersive is * enabled. */ - void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode); + void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, int windowType); void dismissKeyboardShortcutsMenu(); void toggleKeyboardShortcutsMenu(int deviceId); diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 9acb2427aaab..a1961aedf6b7 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -268,6 +268,9 @@ 72dp (content margin) - 12dp (action padding) - 4dp (button inset) --> <dimen name="notification_2025_actions_margin_start">56dp</dimen> + <!-- Notification action button text size --> + <dimen name="notification_2025_action_text_size">16sp</dimen> + <!-- The margin on the end of most content views (ignores the expander) --> <dimen name="notification_content_margin_end">16dp</dimen> diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 8ef105f79988..de5f0ffbe23f 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -177,8 +177,10 @@ public class DisplayManagerGlobalTest { @RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED) public void test_refreshRateRegistration_implicitRRCallbacksEnabled() throws RemoteException { + DisplayManager.DisplayListener displayListener1 = + Mockito.mock(DisplayManager.DisplayListener.class); // Subscription without supplied events doesn't subscribe to RR events - mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + mDisplayManagerGlobal.registerDisplayListener(displayListener1, mHandler, ALL_DISPLAY_EVENTS, /* packageName= */ null, /* isEventFilterExplicit */ false); Mockito.verify(mDisplayManager) @@ -187,7 +189,9 @@ public class DisplayManagerGlobalTest { // After registering to refresh rate changes, subscription without supplied events subscribe // to RR events mDisplayManagerGlobal.registerForRefreshRateChanges(); - mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + DisplayManager.DisplayListener displayListener2 = + Mockito.mock(DisplayManager.DisplayListener.class); + mDisplayManagerGlobal.registerDisplayListener(displayListener2, mHandler, ALL_DISPLAY_EVENTS, /* packageName= */ null, /* isEventFilterExplicit */ false); Mockito.verify(mDisplayManager) @@ -203,7 +207,9 @@ public class DisplayManagerGlobalTest { } // Subscription to RR when events are supplied doesn't happen - mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + DisplayManager.DisplayListener displayListener3 = + Mockito.mock(DisplayManager.DisplayListener.class); + mDisplayManagerGlobal.registerDisplayListener(displayListener3, mHandler, ALL_DISPLAY_EVENTS, /* packageName= */ null, /* isEventFilterExplicit */ true); Mockito.verify(mDisplayManager) @@ -214,7 +220,6 @@ public class DisplayManagerGlobalTest { int subscribedListenersCount = 0; int nonSubscribedListenersCount = 0; for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) { - if (delegate.isEventFilterExplicit()) { assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask); nonSubscribedListenersCount++; diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 4516e9ce72fc..af87af0d243f 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -1195,6 +1195,23 @@ public class InsetsControllerTest { }); } + @Test + public void testAnimatingTypes() throws Exception { + prepareControls(); + + final int types = navigationBars() | statusBars(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + clearInvocations(mTestHost); + mController.hide(types); + // quickly jump to final state by cancelling it. + mController.cancelExistingAnimations(); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + verify(mTestHost, times(1)).updateAnimatingTypes(eq(types)); + verify(mTestHost, times(1)).updateAnimatingTypes(eq(0) /* animatingTypes */); + } + private void waitUntilNextFrame() throws Exception { final CountDownLatch latch = new CountDownLatch(1); Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index c40137f1bd34..f5d1e7a85e83 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -1054,7 +1054,7 @@ public class ViewRootImplTest { ViewRootImpl viewRootImpl = mView.getViewRootImpl(); sInstrumentation.runOnMainSync(() -> { mView.invalidate(); - viewRootImpl.notifyInsetsAnimationRunningStateChanged(true); + viewRootImpl.updateAnimatingTypes(Type.systemBars()); mView.invalidate(); }); sInstrumentation.waitForIdleSync(); diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt index 3aefcd5ec6c0..9087da34d259 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt @@ -552,7 +552,9 @@ class BubbleTaskViewListenerTest { private fun createAppBubble(usePendingIntent: Boolean = false): Bubble { val target = Intent(context, TestActivity::class.java) + val component = ComponentName(context, TestActivity::class.java) target.setPackage(context.packageName) + target.setComponent(component) if (usePendingIntent) { // Robolectric doesn't seem to play nice with PendingIntents, have to mock it. val pendingIntent = mock<PendingIntent>() diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index 7b5831376dc0..14c15210252a 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -19,7 +19,9 @@ package com.android.wm.shell.bubbles.bar import android.animation.AnimatorTestRule import android.content.Context import android.content.pm.LauncherApps +import android.graphics.Insets import android.graphics.PointF +import android.graphics.Rect import android.os.Handler import android.os.UserManager import android.view.IWindowManager @@ -61,6 +63,7 @@ import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.shared.TransactionPool import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit @@ -80,6 +83,10 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class BubbleBarLayerViewTest { + companion object { + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + } @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) @@ -111,6 +118,16 @@ class BubbleBarLayerViewTest { bubblePositioner = BubblePositioner(context, windowManager) bubblePositioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + bubblePositioner.update(deviceConfig) testBubblesList = mutableListOf() val bubbleData = mock<BubbleData>() @@ -313,6 +330,48 @@ class BubbleBarLayerViewTest { assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble) } + @Test + fun testUpdateExpandedView_updateLocation() { + bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT + val bubble = createBubble("first") + + getInstrumentation().runOnMainSync { + bubbleBarLayerView.showExpandedView(bubble) + } + waitForExpandedViewAnimation() + + val previousX = bubble.bubbleBarExpandedView!!.x + + bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT + getInstrumentation().runOnMainSync { + bubbleBarLayerView.updateExpandedView() + } + + assertThat(bubble.bubbleBarExpandedView!!.x).isNotEqualTo(previousX) + } + + @Test + fun testUpdatedExpandedView_updateLocation_skipWhileAnimating() { + bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT + val bubble = createBubble("first") + + getInstrumentation().runOnMainSync { + bubbleBarLayerView.showExpandedView(bubble) + } + waitForExpandedViewAnimation() + + val previousX = bubble.bubbleBarExpandedView!!.x + bubble.bubbleBarExpandedView!!.isAnimating = true + + bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT + getInstrumentation().runOnMainSync { + bubbleBarLayerView.updateExpandedView() + } + + // Expanded view is not updated while animating + assertThat(bubble.bubbleBarExpandedView!!.x).isEqualTo(previousX) + } + private fun createBubble(key: String): Bubble { val bubbleTaskView = FakeBubbleTaskViewFactory(context, mainExecutor).create() val bubbleBarExpandedView = 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 d50a14cf5dae..c2aa146d6437 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 @@ -79,7 +79,7 @@ android:layout_marginEnd="4dp"> <Button - android:layout_width="94dp" + android:layout_width="108dp" android:layout_height="60dp" android:id="@+id/maximize_menu_size_toggle_button" style="?android:attr/buttonBarButtonStyle" @@ -126,7 +126,7 @@ <Button android:id="@+id/maximize_menu_snap_left_button" style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" + android:layout_width="48dp" android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" android:layout_marginEnd="4dp" android:background="@drawable/desktop_mode_maximize_menu_button_background" @@ -137,7 +137,7 @@ <Button android:id="@+id/maximize_menu_snap_right_button" style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" + android:layout_width="48dp" android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" android:background="@drawable/desktop_mode_maximize_menu_button_background" android:importantForAccessibility="yes" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index a0c68ad44379..32660e8fca27 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -618,6 +618,15 @@ <!-- The vertical inset to apply to the app chip's ripple drawable --> <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen> + <!-- The corner radius of the windowing actions pill buttons's ripple drawable --> + <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen> + <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing + actions pill central buttons --> + <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen> + <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing + actions pill edge buttons --> + <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen> + <!-- The corner radius of the minimize button's ripple drawable --> <dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen> <!-- The vertical inset to apply to the minimize button's ripple drawable --> 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 00c446c3da60..7acad5054e98 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 @@ -374,7 +374,7 @@ public class DesktopModeStatus { * of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM. */ public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) { - if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) { + if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) { return false; } return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP, @@ -387,7 +387,7 @@ public class DesktopModeStatus { * screen. */ public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) { - if (!Flags.enableDragToMaximize()) { + if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) { return false; } return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 53551387230c..26c362611518 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -147,10 +147,9 @@ class ActivityEmbeddingAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Update the surface position and alpha. - if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() - && mAnimation.getExtensionEdges() != 0x0 + if (mAnimation.getExtensionEdges() != 0x0 && !(mChange.hasFlags(FLAG_TRANSLUCENT) - && mChange.getActivityComponent() != null)) { + && mChange.getActivityComponent() != null)) { // Extend non-translucent activities t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges()); } @@ -189,8 +188,7 @@ class ActivityEmbeddingAnimationAdapter { @CallSuper void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { onAnimationUpdate(t, mAnimation.getDuration()); - if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() - && mAnimation.getExtensionEdges() != 0x0) { + if (mAnimation.getExtensionEdges() != 0x0) { t.setEdgeExtensionEffect(mLeash, /* edge */ 0); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index c3e783ddf4f1..85b7ac27daa0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -20,11 +20,9 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; -import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation; import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; -import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; @@ -143,10 +141,6 @@ class ActivityEmbeddingAnimationRunner { // ending states. prepareForJumpCut(info, startTransaction); } else { - if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { - addEdgeExtensionIfNeeded(startTransaction, finishTransaction, - postStartTransactionCallbacks, adapters); - } addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -329,34 +323,6 @@ class ActivityEmbeddingAnimationRunner { } } - /** Adds edge extension to the surfaces that have such an animation property. */ - private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks, - @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { - for (ActivityEmbeddingAnimationAdapter adapter : adapters) { - final Animation animation = adapter.mAnimation; - if (animation.getExtensionEdges() == 0) { - continue; - } - if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT) - && adapter.mChange.getActivityComponent() != null) { - // Skip edge extension for translucent activity. - continue; - } - final TransitionInfo.Change change = adapter.mChange; - if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) { - // Need to screenshot after startTransaction is applied otherwise activity - // may not be visible or ready yet. - postStartTransactionCallbacks.add( - t -> edgeExtendWindow(change, animation, t, finishTransaction)); - } else { - // Can screenshot now (before startTransaction is applied) - edgeExtendWindow(change, animation, startTransaction, finishTransaction); - } - } - } - /** Adds background color to the transition if any animation has such a property. */ private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 313d151aeab7..d9489287ff42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -364,7 +364,7 @@ public class Bubble implements BubbleViewProvider { @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(intent, user, - /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user), + /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), mainExecutor, bgExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 29837dc04423..677c21c96f4b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -473,7 +473,7 @@ public class BubbleBarLayerView extends FrameLayout /** Updates the expanded view size and position. */ public void updateExpandedView() { - if (mExpandedView == null || mExpandedBubble == null) return; + if (mExpandedView == null || mExpandedBubble == null || mExpandedView.isAnimating()) return; boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(), isOverflowExpanded, mTempRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index df82091ef002..dd2050a5fd5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -461,6 +461,14 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + private void setAnimating(boolean imeAnimationOngoing) { + int animatingTypes = imeAnimationOngoing ? WindowInsets.Type.ime() : 0; + try { + mWmService.updateDisplayWindowAnimatingTypes(mDisplayId, animatingTypes); + } catch (RemoteException e) { + } + } + private int imeTop(float surfaceOffset, float surfacePositionY) { // surfaceOffset is already offset by the surface's top inset, so we need to subtract // the top inset so that the return value is in screen coordinates. @@ -619,6 +627,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged + imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY) + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); } + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + setAnimating(true); + } int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY), imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); @@ -666,6 +677,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } if (!android.view.inputmethod.Flags.refactorInsetsController()) { dispatchEndPositioning(mDisplayId, mCancelled, t); + } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + setAnimating(false); } if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 59acdc574434..48fadc02ff1f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -100,6 +100,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; +import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver; @@ -770,7 +771,8 @@ public abstract class WMShellModule { DesksOrganizer desksOrganizer, DesksTransitionObserver desksTransitionObserver, UserProfileContexts userProfileContexts, - DesktopModeCompatPolicy desktopModeCompatPolicy) { + DesktopModeCompatPolicy desktopModeCompatPolicy, + DragToDisplayTransitionHandler dragToDisplayTransitionHandler) { return new DesktopTasksController( context, shellInit, @@ -808,7 +810,8 @@ public abstract class WMShellModule { desksOrganizer, desksTransitionObserver, userProfileContexts, - desktopModeCompatPolicy); + desktopModeCompatPolicy, + dragToDisplayTransitionHandler); } @WMSingleton @@ -934,6 +937,12 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() { + return new DragToDisplayTransitionHandler(); + } + + @WMSingleton + @Provides static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler( Context context, Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt index c9a63ff818f5..e89aafe267ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -27,9 +27,9 @@ 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 import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider @@ -47,31 +47,9 @@ class DesktopDisplayModeController( ) { fun refreshDisplayWindowingMode() { - if (!Flags.enableDisplayWindowingModeSwitching()) return - // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. - val isExtendedDisplayEnabled = - 0 != - Settings.Global.getInt( - context.contentResolver, - DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, - 0, - ) - if (!isExtendedDisplayEnabled) { - // No action needed in mirror or projected mode. - return - } + if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return - val hasNonDefaultDisplay = - rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId -> - displayId != DEFAULT_DISPLAY - } - val targetDisplayWindowingMode = - if (hasNonDefaultDisplay) { - WINDOWING_MODE_FREEFORM - } else { - // Use the default display windowing mode when no non-default display. - windowManager.getWindowingMode(DEFAULT_DISPLAY) - } + val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay() val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode @@ -111,6 +89,25 @@ class DesktopDisplayModeController( transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } + private fun getTargetWindowingModeForDefaultDisplay(): Int { + if (isExtendedDisplayEnabled() && hasExternalDisplay()) { + return WINDOWING_MODE_FREEFORM + } + return windowManager.getWindowingMode(DEFAULT_DISPLAY) + } + + // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. + private fun isExtendedDisplayEnabled() = + 0 != + Settings.Global.getInt( + context.contentResolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, + 0, + ) + + private fun hasExternalDisplay() = + rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } + private fun logV(msg: String, vararg arguments: Any?) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } 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 04e609ec3820..03423ba3b96a 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 @@ -1007,6 +1007,21 @@ class DesktopRepository( fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) = boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds)) + /** Returns the current state of the desktop, formatted for usage by remote clients. */ + fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> = + desktopData + .desksSequence() + .groupBy { it.displayId } + .map { (displayId, desks) -> + val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId + DisplayDeskState().apply { + this.displayId = displayId + this.activeDeskId = activeDeskId ?: INVALID_DESK_ID + this.deskIds = desks.map { it.deskId }.toIntArray() + } + } + .toTypedArray() + /** TODO: b/389960283 - consider updating only the changing desks. */ private fun updatePersistentRepository(displayId: Int) { val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList() 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 180d069f359d..fca5084b65bc 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 @@ -205,6 +205,7 @@ class DesktopTasksController( private val desksTransitionObserver: DesksTransitionObserver, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, + private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -811,7 +812,7 @@ class DesktopTasksController( willExitDesktop( triggerTaskId = taskInfo.taskId, displayId = displayId, - forceToFullscreen = false, + forceExitDesktop = false, ) taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) val desktopExitRunnable = @@ -884,7 +885,7 @@ class DesktopTasksController( snapEventHandler.removeTaskIfTiled(displayId, taskId) taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) - val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false) + val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) val desktopExitRunnable = performDesktopExitCleanUp( wct = wct, @@ -977,7 +978,7 @@ class DesktopTasksController( ) { logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() - val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true) + val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true) val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop) // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. @@ -996,7 +997,14 @@ class DesktopTasksController( deactivationRunnable?.invoke(transition) // handles case where we are moving to full screen without closing all DW tasks. - if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) { + if ( + !taskRepository.isOnlyVisibleNonClosingTask(task.taskId) + // This callback is already invoked by |addMoveToFullscreenChanges| when one of these + // flags is enabled. + && + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && + !Flags.enableDesktopWindowingPip() + ) { desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) @@ -1893,16 +1901,24 @@ class DesktopTasksController( private fun willExitDesktop( triggerTaskId: Int, displayId: Int, - forceToFullscreen: Boolean, + forceExitDesktop: Boolean, ): Boolean { + if ( + forceExitDesktop && + (Flags.enableDesktopWindowingPip() || + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) + ) { + // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when + // explicitly going fullscreen, so there's no point in checking the desktop state. + return true + } if (Flags.enablePerDisplayDesktopWallpaperActivity()) { if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) { return false } } else if ( Flags.enableDesktopWindowingPip() && - taskRepository.isMinimizedPipPresentInDisplay(displayId) && - !forceToFullscreen + taskRepository.isMinimizedPipPresentInDisplay(displayId) ) { return false } else { @@ -2295,7 +2311,7 @@ class DesktopTasksController( willExitDesktop( triggerTaskId = task.taskId, displayId = task.displayId, - forceToFullscreen = true, + forceExitDesktop = true, ), ) wct.reorder(task.token, true) @@ -2328,7 +2344,7 @@ class DesktopTasksController( willExitDesktop( triggerTaskId = task.taskId, displayId = task.displayId, - forceToFullscreen = true, + forceExitDesktop = true, ), ) return wct @@ -2433,7 +2449,7 @@ class DesktopTasksController( willExitDesktop( triggerTaskId = task.taskId, displayId = task.displayId, - forceToFullscreen = true, + forceExitDesktop = true, ), ) } @@ -2471,7 +2487,7 @@ class DesktopTasksController( willExitDesktop( triggerTaskId = task.taskId, displayId = task.displayId, - forceToFullscreen = true, + forceExitDesktop = true, ), ) } @@ -3173,25 +3189,24 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.setBounds(taskInfo.token, destinationBounds) - // TODO: b/362720497 - reparent to a specific desk within the target display. - // Reparent task if it has been moved to a new display. - if (Flags.enableConnectedDisplaysWindowDrag()) { - val newDisplayId = motionEvent.getDisplayId() - if (newDisplayId != taskInfo.getDisplayId()) { - val displayAreaInfo = - rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId) - if (displayAreaInfo == null) { - logW( - "Task reparent cannot find DisplayAreaInfo for displayId=%d", - newDisplayId, - ) - } else { - wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true) - } + val newDisplayId = motionEvent.getDisplayId() + val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId) + val isCrossDisplayDrag = + Flags.enableConnectedDisplaysWindowDrag() && + newDisplayId != taskInfo.getDisplayId() && + displayAreaInfo != null + val handler = + if (isCrossDisplayDrag) { + dragToDisplayTransitionHandler + } else { + null } + if (isCrossDisplayDrag) { + // TODO: b/362720497 - reparent to a specific desk within the target display. + wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true) } - transitions.startTransition(TRANSIT_CHANGE, wct, null) + transitions.startTransition(TRANSIT_CHANGE, wct, handler) releaseVisualIndicator() } @@ -3613,27 +3628,11 @@ class DesktopTasksController( controller, { c -> run { - c.taskRepository.addDeskChangeListener( - deskChangeListener, - c.mainExecutor, - ) - c.taskRepository.addVisibleTasksListener( - visibleTasksListener, - c.mainExecutor, - ) - c.taskbarDesktopTaskListener = taskbarDesktopTaskListener - c.desktopModeEnterExitTransitionListener = - desktopModeEntryExitTransitionListener - } - }, - { c -> - run { - c.taskRepository.removeDeskChangeListener(deskChangeListener) - c.taskRepository.removeVisibleTasksListener(visibleTasksListener) - c.taskbarDesktopTaskListener = null - c.desktopModeEnterExitTransitionListener = null + syncInitialState(c) + registerListeners(c) } }, + { c -> run { unregisterListeners(c) } }, ) } @@ -3729,6 +3728,31 @@ class DesktopTasksController( c.startLaunchIntentTransition(intent, options, displayId) } } + + private fun syncInitialState(c: DesktopTasksController) { + remoteListener.call { l -> + // TODO: b/393962589 - implement desks limit. + val canCreateDesks = true + l.onListenerConnected( + c.taskRepository.getDeskDisplayStateForRemote(), + canCreateDesks, + ) + } + } + + private fun registerListeners(c: DesktopTasksController) { + c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor) + c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor) + c.taskbarDesktopTaskListener = taskbarDesktopTaskListener + c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener + } + + private fun unregisterListeners(c: DesktopTasksController) { + c.taskRepository.removeDeskChangeListener(deskChangeListener) + c.taskRepository.removeVisibleTasksListener(visibleTasksListener) + c.taskbarDesktopTaskListener = null + c.desktopModeEnterExitTransitionListener = null + } } private fun logV(msg: String, vararg arguments: Any?) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt new file mode 100644 index 000000000000..d51576a5148e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode + +import android.os.IBinder +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.transition.Transitions + +/** Handles the transition to drag a window to another display by dragging the caption. */ +class DragToDisplayTransitionHandler : Transitions.TransitionHandler { + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? { + return null + } + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + for (change in info.changes) { + val sc = change.leash + val endBounds = change.endAbsBounds + val endPosition = change.endRelOffset + startTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + finishTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + } + + startTransaction.apply() + finishCallback.onTransitionFinished(null) + return true + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index e9c6adec75d7..3652a1661f28 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -67,7 +67,6 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; import static com.android.wm.shell.transition.DefaultSurfaceAnimator.buildSurfaceAnimation; -import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange; @@ -543,21 +542,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition); if (!isTask && a.getExtensionEdges() != 0x0) { - if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { - startTransaction.setEdgeExtensionEffect( - change.getLeash(), a.getExtensionEdges()); - finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0); - } else { - if (!TransitionUtil.isOpeningType(mode)) { - // Can screenshot now (before startTransaction is applied) - edgeExtendWindow(change, a, startTransaction, finishTransaction); - } else { - // Need to screenshot after startTransaction is applied otherwise - // activity may not be visible or ready yet. - postStartTransactionCallbacks - .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); - } - } + startTransaction.setEdgeExtensionEffect( + change.getLeash(), a.getExtensionEdges()); + finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0); } final Rect clipRect = TransitionUtil.isClosingType(mode) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 7984bcedc4e5..edfb56019a60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -26,7 +26,6 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; -import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; @@ -39,20 +38,10 @@ import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration; -import android.graphics.BitmapShader; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.Shader; -import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.animation.Animation; -import android.view.animation.Transformation; -import android.window.ScreenCapture; import android.window.TransitionInfo; import com.android.internal.R; @@ -317,129 +306,6 @@ public class TransitionAnimationHelper { } /** - * Adds edge extension surface to the given {@code change} for edge extension animation. - */ - public static void edgeExtendWindow(@NonNull TransitionInfo.Change change, - @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction) { - // Do not create edge extension surface for transfer starting window change. - // The app surface could be empty thus nothing can draw on the hardware renderer, which will - // block this thread when calling Surface#unlockCanvasAndPost. - if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - return; - } - final Transformation transformationAtStart = new Transformation(); - a.getTransformationAt(0, transformationAtStart); - final Transformation transformationAtEnd = new Transformation(); - a.getTransformationAt(1, transformationAtEnd); - - // We want to create an extension surface that is the maximal size and the animation will - // take care of cropping any part that overflows. - final Insets maxExtensionInsets = Insets.min( - transformationAtStart.getInsets(), transformationAtEnd.getInsets()); - - final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), - change.getEndAbsBounds().height()); - final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), - change.getEndAbsBounds().width()); - if (maxExtensionInsets.left < 0) { - final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.left, targetSurfaceHeight); - final int xPos = maxExtensionInsets.left; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Left Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.top < 0) { - final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.top); - final int xPos = 0; - final int yPos = maxExtensionInsets.top; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Top Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.right < 0) { - final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.right, targetSurfaceHeight); - final int xPos = targetSurfaceWidth; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Right Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.bottom < 0) { - final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.bottom); - final int xPos = maxExtensionInsets.left; - final int yPos = targetSurfaceHeight; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Bottom Edge Extension", startTransaction, finishTransaction); - } - } - - /** - * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension - * animation. - */ - private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend, - @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos, - @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction) { - final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() - .setName(layerName) - .setParent(surfaceToExtend) - .setHidden(true) - .setCallsite("TransitionAnimationHelper#createExtensionSurface") - .setOpaque(true) - .setBufferSize(extensionRect.width(), extensionRect.height()) - .build(); - - final ScreenCapture.LayerCaptureArgs captureArgs = - new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend) - .setSourceCrop(edgeBounds) - .setFrameScale(1) - .setPixelFormat(PixelFormat.RGBA_8888) - .setChildrenOnly(true) - .setAllowProtected(false) - .setCaptureSecureLayers(true) - .build(); - final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer = - ScreenCapture.captureLayers(captureArgs); - - if (edgeBuffer == null) { - ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Failed to capture edge of window."); - return null; - } - - final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(), - Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - final Paint paint = new Paint(); - paint.setShader(shader); - - final Surface surface = new Surface(edgeExtensionLayer); - final Canvas c = surface.lockHardwareCanvas(); - c.drawRect(extensionRect, paint); - surface.unlockCanvasAndPost(c); - surface.release(); - - startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); - startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); - startTransaction.setVisibility(edgeExtensionLayer, true); - finishTransaction.remove(edgeExtensionLayer); - - return edgeExtensionLayer; - } - - /** * Returns whether there is an opaque fullscreen Change positioned in front of the given Change * in the given TransitionInfo. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index ff50672953c9..ad2e23cb4028 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -50,6 +50,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R +import com.android.wm.shell.bubbles.ContextUtils.isRtl import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper @@ -60,6 +61,8 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewCo import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.calculateMenuPosition +import com.android.wm.shell.windowdecor.common.DrawableInsets +import com.android.wm.shell.windowdecor.common.createRippleDrawable import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -71,6 +74,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext + /** * Handle menu opened when the appropriate button is clicked on. * @@ -467,6 +471,33 @@ class HandleMenu( val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View + private val windowingButtonRippleRadius = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius) + private val windowingButtonDrawableInsets = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), + horizontal = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base) + ) + private val windowingButtonDrawableInsetsLeft = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), + horizontalLeft = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift), + ) + private val windowingButtonDrawableInsetsRight = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), + horizontalRight = context.resources + .getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift) + ) + // App Info Pill. private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>( @@ -708,6 +739,49 @@ class HandleMenu( desktopBtn.isSelected = taskInfo.isFreeform desktopBtn.isEnabled = !taskInfo.isFreeform desktopBtn.imageTintList = style.windowingButtonColor + + val startInsets = if (context.isRtl) { + windowingButtonDrawableInsetsRight + } else { + windowingButtonDrawableInsetsLeft + } + val endInsets = if (context.isRtl) { + windowingButtonDrawableInsetsLeft + } else { + windowingButtonDrawableInsetsRight + } + + fullscreenBtn.apply { + background = createRippleDrawable( + color = style.textColor, + cornerRadius = windowingButtonRippleRadius, + drawableInsets = startInsets + ) + } + + splitscreenBtn.apply { + background = createRippleDrawable( + color = style.textColor, + cornerRadius = windowingButtonRippleRadius, + drawableInsets = windowingButtonDrawableInsets + ) + } + + floatingBtn.apply { + background = createRippleDrawable( + color = style.textColor, + cornerRadius = windowingButtonRippleRadius, + drawableInsets = windowingButtonDrawableInsets + ) + } + + desktopBtn.apply { + background = createRippleDrawable( + color = style.textColor, + cornerRadius = windowingButtonRippleRadius, + drawableInsets = endInsets + ) + } } private fun bindMoreActionsPill(style: MenuStyle) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index c6cb62d153ac..1b0e0f70ed21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -363,10 +363,11 @@ class MultiDisplayVeiledResizeTaskPositioner( dragEventListeners.remove(dragEventListener) } - override fun onTopologyChanged(topology: DisplayTopology) { + override fun onTopologyChanged(topology: DisplayTopology?) { // TODO: b/383069173 - Cancel window drag when topology changes happen during drag. displayIds.clear() + if (topology == null) return val displayBounds = topology.getAbsoluteBounds() displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) }) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index 7af6b8e26cbf..5bd42280e790 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -225,7 +225,7 @@ public class ResizeVeil @JvmOverloads constructor( val veilAnimT = surfaceControlTransactionSupplier.get() val iconAnimT = surfaceControlTransactionSupplier.get() veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply { - duration = RESIZE_ALPHA_DURATION + duration = VEIL_ENTRY_ALPHA_ANIMATION_DURATION addUpdateListener { veilAnimT.setAlpha(background, animatedValue as Float) .apply() @@ -243,7 +243,8 @@ public class ResizeVeil @JvmOverloads constructor( }) } iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply { - duration = RESIZE_ALPHA_DURATION + duration = ICON_ALPHA_ANIMATION_DURATION + startDelay = ICON_ENTRY_DELAY addUpdateListener { iconAnimT.setAlpha(icon, animatedValue as Float) .apply() @@ -387,23 +388,38 @@ public class ResizeVeil @JvmOverloads constructor( if (background == null || icon == null) return veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply { - duration = RESIZE_ALPHA_DURATION + duration = VEIL_EXIT_ALPHA_ANIMATION_DURATION + startDelay = VEIL_EXIT_DELAY addUpdateListener { surfaceControlTransactionSupplier.get() .setAlpha(background, animatedValue as Float) - .setAlpha(icon, animatedValue as Float) .apply() } addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { surfaceControlTransactionSupplier.get() .hide(background) - .hide(icon) .apply() } }) } + iconAnimator = ValueAnimator.ofFloat(1f, 0f).apply { + duration = ICON_ALPHA_ANIMATION_DURATION + addUpdateListener { + surfaceControlTransactionSupplier.get() + .setAlpha(icon, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + surfaceControlTransactionSupplier.get() + .hide(icon) + .apply() + } + }) + } veilAnimator?.start() + iconAnimator?.start() isVisible = false } @@ -451,7 +467,11 @@ public class ResizeVeil @JvmOverloads constructor( companion object { private const val TAG = "ResizeVeil" - private const val RESIZE_ALPHA_DURATION = 100L + private const val ICON_ALPHA_ANIMATION_DURATION = 50L + private const val VEIL_ENTRY_ALPHA_ANIMATION_DURATION = 50L + private const val VEIL_EXIT_ALPHA_ANIMATION_DURATION = 200L + private const val ICON_ENTRY_DELAY = 33L + private const val VEIL_EXIT_DELAY = 33L private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL /** The background is a child of the veil container layer and goes at the bottom. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt new file mode 100644 index 000000000000..e18239d3eb70 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt @@ -0,0 +1,88 @@ +/* + * 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.windowdecor.common + +import android.annotation.ColorInt +import android.graphics.Color +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import com.android.wm.shell.windowdecor.common.OPACITY_11 +import com.android.wm.shell.windowdecor.common.OPACITY_15 +import android.content.res.ColorStateList + +/** + * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds. + */ +data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) { + constructor(vertical: Int = 0, horizontal: Int = 0) : + this(horizontal, vertical, horizontal, vertical) + constructor(vertical: Int = 0, horizontalLeft: Int = 0, horizontalRight: Int = 0) : + this(horizontalLeft, vertical, horizontalRight, vertical) +} + +/** + * Replaces the alpha component of a color with the given alpha value. + */ +@ColorInt +fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { + return Color.argb( + alpha, + Color.red(color), + Color.green(color), + Color.blue(color) + ) +} + +/** + * Creates a RippleDrawable with specified color, corner radius, and insets. + */ +fun createRippleDrawable( + @ColorInt color: Int, + cornerRadius: Int, + drawableInsets: DrawableInsets, +): RippleDrawable { + return RippleDrawable( + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_hovered), + intArrayOf(android.R.attr.state_pressed), + intArrayOf(), + ), + intArrayOf( + replaceColorAlpha(color, OPACITY_11), + replaceColorAlpha(color, OPACITY_15), + Color.TRANSPARENT, + ) + ), + null /* content */, + LayerDrawable(arrayOf( + ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { cornerRadius.toFloat() }, + null /* inset */, + null /* innerRadii */ + ) + paint.color = Color.WHITE + } + )).apply { + require(numberOfLayers == 1) { "Must only contain one layer" } + setLayerInset(0 /* index */, + drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b) + } + ) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 870c894fe885..eb8b617df4ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -61,6 +61,8 @@ import com.android.wm.shell.windowdecor.common.OPACITY_15 import com.android.wm.shell.windowdecor.common.OPACITY_55 import com.android.wm.shell.windowdecor.common.OPACITY_65 import com.android.wm.shell.windowdecor.common.Theme +import com.android.wm.shell.windowdecor.common.DrawableInsets +import com.android.wm.shell.windowdecor.common.createRippleDrawable import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance @@ -635,61 +637,10 @@ class AppHeaderViewHolder( ) } - @ColorInt - private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { - return Color.argb( - alpha, - Color.red(color), - Color.green(color), - Color.blue(color) - ) - } - - private fun createRippleDrawable( - @ColorInt color: Int, - cornerRadius: Int, - drawableInsets: DrawableInsets, - ): RippleDrawable { - return RippleDrawable( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_hovered), - intArrayOf(android.R.attr.state_pressed), - intArrayOf(), - ), - intArrayOf( - replaceColorAlpha(color, OPACITY_11), - replaceColorAlpha(color, OPACITY_15), - Color.TRANSPARENT - ) - ), - null /* content */, - LayerDrawable(arrayOf( - ShapeDrawable().apply { - shape = RoundRectShape( - FloatArray(8) { cornerRadius.toFloat() }, - null /* inset */, - null /* innerRadii */ - ) - paint.color = Color.WHITE - } - )).apply { - require(numberOfLayers == 1) { "Must only contain one layer" } - setLayerInset(0 /* index */, - drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b) - } - ) - } - private enum class SizeToggleDirection { MAXIMIZE, RESTORE } - private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) { - constructor(vertical: Int = 0, horizontal: Int = 0) : - this(horizontal, vertical, horizontal, vertical) - } - private data class Header( val type: Type, val appTheme: Theme, diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt index 28008393da84..d82c06691e46 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -18,9 +18,9 @@ package com.android.wm.shell.scenarios import android.tools.NavBar import android.tools.Rotation -import com.android.internal.R import com.android.window.flags.Flags import com.android.wm.shell.Utils +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import org.junit.After import org.junit.Assume import org.junit.Before @@ -42,8 +42,8 @@ constructor( fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) // Skip the test when the drag-to-maximize is enabled on this device. - Assume.assumeFalse(Flags.enableDragToMaximize() && - instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode)) + Assume.assumeFalse( + DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context)) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) testApp.enterDesktopMode(wmHelper, device) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt index 60a0fb547909..675b63cf56bb 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt @@ -23,12 +23,12 @@ import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice -import com.android.internal.R import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags import com.android.wm.shell.Utils +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import org.junit.After import org.junit.Assume import org.junit.Before @@ -54,8 +54,8 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) { fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) // Skip the test when the drag-to-maximize is disabled on this device. - Assume.assumeTrue(Flags.enableDragToMaximize() && - instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode)) + Assume.assumeTrue( + DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context)) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt index 81c46f13b384..b9a5e4a95e36 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt @@ -25,6 +25,7 @@ import android.tools.Rotation import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import android.util.DisplayMetrics +import android.window.DesktopExperienceFlags import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation @@ -64,7 +65,7 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) { @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching()) + Assume.assumeTrue(DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 7f48499b0558..e39fa3a71b03 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -22,7 +22,6 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.component.ComponentNameMatcher -import android.tools.traces.component.EdgeExtensionComponentMatcher import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark @@ -99,7 +98,6 @@ class CopyContentInSplit(override val flicker: LegacyFlickerTest) : ComponentNameMatcher.SPLASH_SCREEN, ComponentNameMatcher.SNAPSHOT, ComponentNameMatcher.IME_SNAPSHOT, - EdgeExtensionComponentMatcher(), magnifierLayer, popupWindowLayer ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt index 0ff7230f6e0c..f0c97d359a16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -101,7 +101,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { private fun testDisplayWindowingModeSwitch( defaultWindowingMode: Int, extendedDisplayEnabled: Boolean, - expectTransition: Boolean, + expectToSwitch: Boolean, ) { defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) @@ -113,10 +113,14 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { settingsSession.use { connectExternalDisplay() - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + if (expectToSwitch) { + // Assumes [connectExternalDisplay] properly triggered the switching transition. + // Will verify the transition later along with [disconnectExternalDisplay]. + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + } disconnectExternalDisplay() - if (expectTransition) { + if (expectToSwitch) { val arg = argumentCaptor<WindowContainerTransaction>() verify(transitions, times(2)) .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) @@ -139,7 +143,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { testDisplayWindowingModeSwitch( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, extendedDisplayEnabled = false, - expectTransition = false, + expectToSwitch = false, ) } @@ -148,7 +152,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { testDisplayWindowingModeSwitch( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, extendedDisplayEnabled = true, - expectTransition = true, + expectToSwitch = true, ) } @@ -157,7 +161,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { testDisplayWindowingModeSwitch( defaultWindowingMode = WINDOWING_MODE_FREEFORM, extendedDisplayEnabled = true, - expectTransition = false, + expectToSwitch = false, ) } 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 ac1deec53bf6..04acaef344eb 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 @@ -261,6 +261,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mockDisplayContext: Context + @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -431,6 +432,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() desksTransitionsObserver, userProfileContexts, desktopModeCompatPolicy, + dragToDisplayTransitionHandler, ) @After @@ -2069,6 +2071,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDeskWithMultipleTasks_deactivatesDesk() { + val deskId = 1 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desksOrganizer).deactivateDesk(wct, deskId = deskId) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -2278,7 +2295,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @DisableFlags( + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + ) fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -2305,29 +2325,6 @@ 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() @@ -4455,7 +4452,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @DisableFlags( + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + ) fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -4480,27 +4480,6 @@ 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() @@ -5031,7 +5010,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Mockito.argThat { wct -> return@argThat wct.hierarchyOps[0].isReparent }, - eq(null), + eq(dragToDisplayTransitionHandler), ) } @@ -5225,6 +5204,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags( + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + ) fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt new file mode 100644 index 000000000000..51c302983fd0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode + +import android.graphics.Point +import android.graphics.Rect +import android.os.IBinder +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import com.android.wm.shell.transition.Transitions +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Test class for {@link DragToDisplayTransitionHandler} + * + * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest + */ +class DragToDisplayTransitionHandlerTest { + private lateinit var handler: DragToDisplayTransitionHandler + private val mockTransition: IBinder = mock() + private val mockRequestInfo: TransitionRequestInfo = mock() + private val mockTransitionInfo: TransitionInfo = mock() + private val mockStartTransaction: SurfaceControl.Transaction = mock() + private val mockFinishTransaction: SurfaceControl.Transaction = mock() + private val mockFinishCallback: Transitions.TransitionFinishCallback = mock() + + @Before + fun setUp() { + handler = DragToDisplayTransitionHandler() + whenever(mockStartTransaction.setWindowCrop(any(), any(), any())) + .thenReturn(mockStartTransaction) + whenever(mockFinishTransaction.setWindowCrop(any(), any(), any())) + .thenReturn(mockFinishTransaction) + } + + @Test + fun handleRequest_anyRequest_returnsNull() { + val result = handler.handleRequest(mockTransition, mockRequestInfo) + assert(result == null) + } + + @Test + fun startAnimation_verifyTransformationsApplied() { + val mockChange1 = mock<TransitionInfo.Change>() + val leash1 = mock<SurfaceControl>() + val endBounds1 = Rect(0, 0, 50, 50) + val endPosition1 = Point(5, 5) + + whenever(mockChange1.leash).doReturn(leash1) + whenever(mockChange1.endAbsBounds).doReturn(endBounds1) + whenever(mockChange1.endRelOffset).doReturn(endPosition1) + + val mockChange2 = mock<TransitionInfo.Change>() + val leash2 = mock<SurfaceControl>() + val endBounds2 = Rect(100, 100, 200, 150) + val endPosition2 = Point(15, 25) + + whenever(mockChange2.leash).doReturn(leash2) + whenever(mockChange2.endAbsBounds).doReturn(endBounds2) + whenever(mockChange2.endRelOffset).doReturn(endPosition2) + + whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2)) + + handler.startAnimation( + mockTransition, + mockTransitionInfo, + mockStartTransaction, + mockFinishTransaction, + mockFinishCallback, + ) + + verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height()) + verify(mockStartTransaction) + .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat()) + verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height()) + verify(mockStartTransaction) + .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat()) + verify(mockStartTransaction).apply() + verify(mockFinishCallback).onTransitionFinished(null) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 6f73db0bacc3..677330790bab 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -1316,9 +1316,11 @@ public class ShellTransitionTests extends ShellTestCase { mTransactionPool, createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); + final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class); + doReturn(mContext).when(mockRecentsTaskController).getContext(); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, - mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); + mockRecentsTaskController, mock(HomeTransitionObserver.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java index 200d4ef86146..6ae73a2e6fe5 100644 --- a/location/java/android/location/GnssMeasurement.java +++ b/location/java/android/location/GnssMeasurement.java @@ -1484,6 +1484,10 @@ public final class GnssMeasurement implements Parcelable { * in an open sky test - the important aspect of this output is that changes in this value are * indicative of changes on input signal power in the frequency band for this measurement. * + * <p> This field is part of the GnssMeasurement object so it is only reported when the GNSS + * measurement is reported. E.g., when a GNSS signal is too weak to be acquired, the AGC value + * is not reported. + * * <p> The value is only available if {@link #hasAutomaticGainControlLevelDb()} is {@code true} * * @deprecated Use {@link GnssMeasurementsEvent#getGnssAutomaticGainControls()} instead. diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java index 4fc2ee8b7fb0..8cdfd013130a 100644 --- a/location/java/android/location/GnssMeasurementsEvent.java +++ b/location/java/android/location/GnssMeasurementsEvent.java @@ -158,6 +158,14 @@ public final class GnssMeasurementsEvent implements Parcelable { /** * Gets the collection of {@link GnssAutomaticGainControl} associated with the * current event. + * + * <p>This field must be reported when the GNSS measurement engine is running, even when the + * GnssMeasurement or GnssClock fields are not reported yet. E.g., when a GNSS signal is too + * weak to be acquired, the AGC value must still be reported. + * + * <p>For devices that do not support this field, an empty collection is returned. In that case, + * please use {@link GnssMeasurement#hasAutomaticGainControlLevelDb()} + * and {@link GnssMeasuremen#getAutomaticGainControlLevelDb()}. */ @NonNull public Collection<GnssAutomaticGainControl> getGnssAutomaticGainControls() { diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java index 8b6194fa66f5..fb89973bcc11 100644 --- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java +++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java @@ -28,7 +28,6 @@ import android.telephony.emergency.EmergencyNumber; import android.util.Log; import com.android.internal.annotations.KeepForWeakReference; -import com.android.internal.telephony.flags.Flags; import java.util.concurrent.TimeUnit; @@ -146,17 +145,12 @@ public class GpsNetInitiatedHandler { < emergencyExtensionMillis); boolean isInEmergencyCallback = false; boolean isInEmergencySmsMode = false; - if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) { + PackageManager pm = mContext.getPackageManager(); + if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode(); + } + if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); - } else { - PackageManager pm = mContext.getPackageManager(); - if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { - isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode(); - } - if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { - isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); - } } return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension || isInEmergencySmsMode; diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig index 6d4f0b4f47d5..846448b6afbf 100644 --- a/media/java/android/media/flags/projection.aconfig +++ b/media/java/android/media/flags/projection.aconfig @@ -39,3 +39,12 @@ flag { } is_exported: true } + +flag { + namespace: "media_projection" + name: "app_content_sharing" + description: "Enable apps to share some sub-surface" + bug: "379989921" + is_exported: true +} + diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt index 9d037e91a86f..806580b10cc8 100644 --- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt +++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt @@ -17,20 +17,20 @@ package com.android.settingslib.widget import android.content.Context -import android.os.Build import android.text.TextUtils import android.util.AttributeSet import android.view.View -import androidx.annotation.RequiresApi import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.settingslib.widget.preference.intro.R -class IntroPreference @JvmOverloads constructor( +class IntroPreference +@JvmOverloads +constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, - defStyleRes: Int = 0 + defStyleRes: Int = 0, ) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin { private var isCollapsable: Boolean = true @@ -66,9 +66,9 @@ class IntroPreference @JvmOverloads constructor( /** * Sets whether the summary is collapsable. + * * @param collapsable True if the summary should be collapsable, false otherwise. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun setCollapsable(collapsable: Boolean) { isCollapsable = collapsable minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES @@ -77,9 +77,9 @@ class IntroPreference @JvmOverloads constructor( /** * Sets the minimum number of lines to display when collapsed. + * * @param lines The minimum number of lines. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun setMinLines(lines: Int) { minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) notifyChanged() @@ -87,9 +87,9 @@ class IntroPreference @JvmOverloads constructor( /** * Sets the action when clicking on the hyperlink in the text. + * * @param listener The click listener for hyperlink. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun setHyperlinkListener(listener: View.OnClickListener) { if (hyperlinkListener != listener) { hyperlinkListener = listener @@ -99,9 +99,9 @@ class IntroPreference @JvmOverloads constructor( /** * Sets the action when clicking on the learn more view. + * * @param listener The click listener for learn more. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun setLearnMoreAction(listener: View.OnClickListener) { if (learnMoreListener != listener) { learnMoreListener = listener @@ -111,9 +111,9 @@ class IntroPreference @JvmOverloads constructor( /** * Sets the text of learn more view. + * * @param text The text of learn more. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun setLearnMoreText(text: CharSequence) { if (!TextUtils.equals(learnMoreText, text)) { learnMoreText = text diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 1cb8005ddae0..02bef9fd2fb2 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -59,8 +59,6 @@ class PreferenceScreenBindingHelper( private val preferenceHierarchy: PreferenceHierarchy, ) : KeyedDataObservable<String>() { - private val mainExecutor = HandlerExecutor.main - private val preferenceLifecycleContext = object : PreferenceLifecycleContext(context) { override val lifecycleScope: LifecycleCoroutineScope @@ -88,11 +86,11 @@ class PreferenceScreenBindingHelper( private val preferences: ImmutableMap<String, PreferenceHierarchyNode> private val dependencies: ImmutableMultimap<String, String> private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider> - private val storages = mutableMapOf<String, KeyedObservable<String>>() + private val observables = mutableMapOf<String, KeyedObservable<String>>() private val preferenceObserver: KeyedObserver<String?> - private val storageObserver = + private val observer = KeyedObserver<String> { key, reason -> if (DataChangeReason.isDataChange(reason)) { notifyChange(key, PreferenceChangeReason.VALUE) @@ -133,15 +131,19 @@ class PreferenceScreenBindingHelper( this.dependencies = dependenciesBuilder.build() this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray() + val executor = HandlerExecutor.main preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) } - addObserver(preferenceObserver, mainExecutor) + addObserver(preferenceObserver, executor) preferenceScreen.forEachRecursively { - it.preferenceDataStore?.findKeyValueStore()?.let { keyValueStore -> - val key = it.key - storages[key] = keyValueStore - keyValueStore.addObserver(key, storageObserver, mainExecutor) - } + val key = it.key ?: return@forEachRecursively + @Suppress("UNCHECKED_CAST") + val observable = + it.preferenceDataStore?.findKeyValueStore() + ?: (preferences[key]?.metadata as? KeyedObservable<String>) + ?: return@forEachRecursively + observables[key] = observable + observable.addObserver(key, observer, executor) } } @@ -212,7 +214,7 @@ class PreferenceScreenBindingHelper( fun onDestroy() { removeObserver(preferenceObserver) - for ((key, storage) in storages) storage.removeObserver(key, storageObserver) + for ((key, observable) in observables) observable.removeObserver(key, observer) for (preference in lifecycleAwarePreferences) { preference.onDestroy(preferenceLifecycleContext) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index ebd5a1deffd2..3625c002e9d8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; +import android.os.UserManager; import android.telephony.TelephonyManager; import android.util.Log; @@ -37,6 +38,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; +import com.android.settingslib.flags.Flags; +import com.android.settingslib.utils.ThreadUtils; import java.util.Collection; import java.util.HashMap; @@ -65,6 +68,7 @@ public class BluetoothEventManager { private final android.os.Handler mReceiverHandler; private final UserHandle mUserHandle; private final Context mContext; + private boolean mIsWorkProfile = false; interface Handler { void onReceive(Context context, Intent intent, BluetoothDevice device); @@ -140,6 +144,9 @@ public class BluetoothEventManager { addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); registerAdapterIntentReceiver(); + + UserManager userManager = context.getSystemService(UserManager.class); + mIsWorkProfile = userManager != null && userManager.isManagedProfile(); } /** Register to start receiving callbacks for Bluetooth events. */ @@ -220,20 +227,32 @@ public class BluetoothEventManager { callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); } + if (mIsWorkProfile) { + Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile"); + return; + } + + LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null + : mBtManager.getProfileManager().getLeAudioBroadcastProfile(); + LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null + : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); // Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when // audio sharing is enabled. if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT && state == BluetoothAdapter.STATE_DISCONNECTED - && BluetoothUtils.isAudioSharingUIAvailable(mContext)) { - LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - if (profileManager != null - && profileManager.getLeAudioBroadcastProfile() != null - && profileManager.getLeAudioBroadcastProfile().isProfileReady() - && profileManager.getLeAudioBroadcastAssistantProfile() != null - && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) { - Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected"); - profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded(); - } + && BluetoothUtils.isAudioSharingUIAvailable(mContext) + && broadcast != null && assistant != null && broadcast.isProfileReady() + && assistant.isProfileReady()) { + Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected"); + broadcast.updateFallbackActiveDeviceIfNeeded(); + } + // Dispatch handleOnProfileStateChanged to local broadcast profile + if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice() + && broadcast != null + && state == BluetoothAdapter.STATE_CONNECTED) { + Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile"); + var unused = ThreadUtils.postOnBackgroundThread( + () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager)); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 31948e49b4ce..e78a69239334 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -719,6 +719,30 @@ public class BluetoothUtils { } } + /** Check if the {@link CachedBluetoothDevice} is a media device */ + @WorkerThread + public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) { + if (cachedDevice == null) return false; + return cachedDevice.getProfiles().stream() + .anyMatch( + profile -> + profile instanceof A2dpProfile + || profile instanceof HearingAidProfile + || profile instanceof LeAudioProfile + || profile instanceof HeadsetProfile); + } + + /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */ + @WorkerThread + public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) { + if (cachedDevice == null) return false; + return cachedDevice.getProfiles().stream() + .anyMatch( + profile -> + profile instanceof LeAudioProfile + && profile.isEnabled(cachedDevice.getDevice())); + } + /** Returns if the broadcast is on-going. */ @WorkerThread public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index f18a2da27a70..08f7806207db 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -54,6 +54,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.WorkerThread; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; @@ -64,6 +65,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -107,6 +109,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String SETTINGS_PKG = "com.android.settings"; private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; + private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID"; private static final boolean DEBUG = BluetoothUtils.D; private static final String VALID_PASSWORD_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," @@ -120,6 +123,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { // Order of this profile in device profiles list private static final int ORDINAL = 1; static final int UNKNOWN_VALUE_PLACEHOLDER = -1; + private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s private static final Uri[] SETTINGS_URIS = new Uri[] { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME), @@ -1283,4 +1287,87 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { UserManager userManager = context.getSystemService(UserManager.class); return userManager != null && userManager.isManagedProfile(); } + + /** Handle profile connected for {@link CachedBluetoothDevice}. */ + @WorkerThread + public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice, + int bluetoothProfile, @Nullable LocalBluetoothManager btManager) { + if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) { + Log.d(TAG, "Skip handleProfileConnected, flag off"); + return; + } + if (!SYSUI_PKG.equals(mContext.getPackageName())) { + Log.d(TAG, "Skip handleProfileConnected, not a valid caller"); + return; + } + if (!BluetoothUtils.isMediaDevice(cachedDevice)) { + Log.d(TAG, "Skip handleProfileConnected, not a media device"); + return; + } + Timestamp bondTimestamp = cachedDevice.getBondTimestamp(); + if (bondTimestamp != null) { + long diff = System.currentTimeMillis() - bondTimestamp.getTime(); + if (diff <= JUST_BOND_MILLIS_THRESHOLD) { + Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff); + return; + } + } + if (!isEnabled(null)) { + Log.d(TAG, "Skip handleProfileConnected, not broadcasting"); + return; + } + BluetoothDevice device = cachedDevice.getDevice(); + if (device == null) { + Log.d(TAG, "Skip handleProfileConnected, null device"); + return; + } + // TODO: sync source in a reasonable place + if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) { + Log.d(TAG, "Skip handleProfileConnected, already has source"); + return; + } + if (isAutoRejoinDevice(device)) { + Log.d(TAG, "Skip handleProfileConnected, auto rejoin device"); + return; + } + boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); + // For eligible (LE audio) remote device, we only check assistant profile connected. + if (isLeAudioSupported + && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { + Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile"); + return; + } + boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile); + // For ineligible (classic) remote device, we only check its first connected profile. + if (!isLeAudioSupported && !isFirstConnectedProfile) { + Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile"); + return; + } + + Intent intent = new Intent( + LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); + intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device); + intent.setPackage(SETTINGS_PKG); + Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress()); + + mContext.sendBroadcast(intent); + } + + private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) { + String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice, + AUTO_REJOIN_BROADCAST_TAG); + return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue, + String.valueOf(getLatestBroadcastId())); + } + + private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice, + int bluetoothProfile) { + if (cachedDevice == null) return false; + return cachedDevice.getProfiles().stream() + .noneMatch( + profile -> + profile.getProfileId() != bluetoothProfile + && profile.getConnectionStatus(cachedDevice.getDevice()) + == BluetoothProfile.STATE_CONNECTED); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java index ae17acb5104b..8bb41ccf9600 100644 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java @@ -16,8 +16,8 @@ package com.android.settingslib.qrcode; +import android.annotation.NonNull; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.SurfaceTexture; @@ -75,12 +75,29 @@ public class QrCamera extends Handler { @VisibleForTesting Camera mCamera; + Camera.CameraInfo mCameraInfo; + + /** + * The size of the preview image as requested to camera, e.g. 1920x1080. + */ private Size mPreviewSize; + + /** + * Whether the preview image would be displayed in "portrait" (width less + * than height) orientation in current display orientation. + * + * Note that we don't distinguish between a rotation of 90 degrees or 270 + * degrees here, since we center crop all the preview. + * + * TODO: Handle external camera / multiple display, this likely requires + * migrating to newer Camera2 API. + */ + private boolean mPreviewInPortrait; + private WeakReference<Context> mContext; private ScannerCallback mScannerCallback; private MultiFormatReader mReader; private DecodingTask mDecodeTask; - private int mCameraOrientation; @VisibleForTesting Camera.Parameters mParameters; @@ -152,8 +169,14 @@ public class QrCamera extends Handler { * @param previewSize Is the preview size set by camera * @param cameraOrientation Is the orientation of current Camera * @return The rectangle would like to crop from the camera preview shot. + * @deprecated This is no longer used, and the frame position is + * automatically calculated from the preview size and the + * background View size. */ - Rect getFramePosition(Size previewSize, int cameraOrientation); + @Deprecated + default @NonNull Rect getFramePosition(@NonNull Size previewSize, int cameraOrientation) { + throw new AssertionError("getFramePosition shouldn't be used"); + } /** * Sets the transform to associate with preview area. @@ -172,6 +195,41 @@ public class QrCamera extends Handler { boolean isValid(String qrCode); } + private boolean setPreviewDisplayOrientation() { + if (mContext.get() == null) { + return false; + } + + final WindowManager winManager = + (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE); + final int rotation = winManager.getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + int rotateDegrees = 0; + if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + rotateDegrees = (mCameraInfo.orientation + degrees) % 360; + rotateDegrees = (360 - rotateDegrees) % 360; // compensate the mirror + } else { + rotateDegrees = (mCameraInfo.orientation - degrees + 360) % 360; + } + mCamera.setDisplayOrientation(rotateDegrees); + mPreviewInPortrait = (rotateDegrees == 90 || rotateDegrees == 270); + return true; + } + @VisibleForTesting void setCameraParameter() { mParameters = mCamera.getParameters(); @@ -195,37 +253,39 @@ public class QrCamera extends Handler { mCamera.setParameters(mParameters); } - private boolean startPreview() { - if (mContext.get() == null) { - return false; - } + /** + * Set transform matrix to crop and center the preview picture. + */ + private void setTransformationMatrix() { + final Size previewDisplaySize = rotateIfPortrait(mPreviewSize); + final Size viewSize = mScannerCallback.getViewSize(); + final Rect cropRegion = calculateCenteredCrop(previewDisplaySize, viewSize); - final WindowManager winManager = - (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE); - final int rotation = winManager.getDefaultDisplay().getRotation(); - int degrees = 0; - switch (rotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 90; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 270; - break; - } - final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360; - mCamera.setDisplayOrientation(rotateDegrees); + // Note that strictly speaking, since the preview is mirrored in front + // camera case, we should also mirror the crop region here. But since + // we're cropping at the center, mirroring would result in the same + // crop region other than small off-by-one error from floating point + // calculation and wouldn't be noticeable. + + // Calculate transformation matrix. + float scaleX = previewDisplaySize.getWidth() / (float) cropRegion.width(); + float scaleY = previewDisplaySize.getHeight() / (float) cropRegion.height(); + float translateX = -cropRegion.left / (float) cropRegion.width() * viewSize.getWidth(); + float translateY = -cropRegion.top / (float) cropRegion.height() * viewSize.getHeight(); + + // Set the transform matrix. + final Matrix matrix = new Matrix(); + matrix.setScale(scaleX, scaleY); + matrix.postTranslate(translateX, translateY); + mScannerCallback.setTransform(matrix); + } + + private void startPreview() { mCamera.startPreview(); if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) { mCamera.autoFocus(/* Camera.AutoFocusCallback */ null); sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS); } - return true; } private class DecodingTask extends AsyncTask<Void, Void, String> { @@ -300,7 +360,7 @@ public class QrCamera extends Handler { if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { releaseCamera(); mCamera = Camera.open(i); - mCameraOrientation = cameraInfo.orientation; + mCameraInfo = cameraInfo; break; } } @@ -309,7 +369,7 @@ public class QrCamera extends Handler { Camera.getCameraInfo(0, cameraInfo); releaseCamera(); mCamera = Camera.open(0); - mCameraOrientation = cameraInfo.orientation; + mCameraInfo = cameraInfo; } } catch (RuntimeException e) { Log.e(TAG, "Fail to open camera: " + e); @@ -323,11 +383,12 @@ public class QrCamera extends Handler { throw new IOException("Cannot find available camera"); } mCamera.setPreviewTexture(surface); + if (!setPreviewDisplayOrientation()) { + throw new IOException("Lost context"); + } setCameraParameter(); setTransformationMatrix(); - if (!startPreview()) { - throw new IOException("Lost contex"); - } + startPreview(); } catch (IOException ioe) { Log.e(TAG, "Fail to startPreview camera: " + ioe); mCamera = null; @@ -345,32 +406,30 @@ public class QrCamera extends Handler { } } - /** Set transform matrix to crop and center the preview picture */ - private void setTransformationMatrix() { - final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT; - - final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight(); - final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth(); - final float ratioPreview = (float) getRatio(previewWidth, previewHeight); - - // Calculate transformation matrix. - float scaleX = 1.0f; - float scaleY = 1.0f; - if (previewWidth > previewHeight) { - scaleY = scaleX / ratioPreview; + /** + * Calculates the crop region in `previewSize` to have the same aspect + * ratio as `viewSize` and center aligned. + */ + private Rect calculateCenteredCrop(Size previewSize, Size viewSize) { + final double previewRatio = getRatio(previewSize); + final double viewRatio = getRatio(viewSize); + int width; + int height; + if (previewRatio > viewRatio) { + width = previewSize.getWidth(); + height = (int) Math.round(width * viewRatio); } else { - scaleX = scaleY / ratioPreview; + height = previewSize.getHeight(); + width = (int) Math.round(height / viewRatio); } - - // Set the transform matrix. - final Matrix matrix = new Matrix(); - matrix.setScale(scaleX, scaleY); - mScannerCallback.setTransform(matrix); + final int left = (previewSize.getWidth() - width) / 2; + final int top = (previewSize.getHeight() - height) / 2; + return new Rect(left, top, left + width, top + height); } private QrYuvLuminanceSource getFrameImage(byte[] imageData) { - final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation); + final Size viewSize = mScannerCallback.getViewSize(); + final Rect frame = calculateCenteredCrop(mPreviewSize, rotateIfPortrait(viewSize)); final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData, mPreviewSize.getWidth(), mPreviewSize.getHeight()); return (QrYuvLuminanceSource) @@ -398,17 +457,18 @@ public class QrCamera extends Handler { */ private Size getBestPreviewSize(Camera.Parameters parameters) { final double minRatioDiffPercent = 0.1; - final Size windowSize = mScannerCallback.getViewSize(); - final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight()); + final Size viewSize = rotateIfPortrait(mScannerCallback.getViewSize()); + final double viewRatio = getRatio(viewSize); double bestChoiceRatio = 0; Size bestChoice = new Size(0, 0); for (Camera.Size size : parameters.getSupportedPreviewSizes()) { - double ratio = getRatio(size.width, size.height); + final Size newSize = toAndroidSize(size); + final double ratio = getRatio(newSize); if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight() - && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent - || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) { - bestChoice = new Size(size.width, size.height); - bestChoiceRatio = getRatio(size.width, size.height); + && (Math.abs(bestChoiceRatio - viewRatio) / viewRatio > minRatioDiffPercent + || Math.abs(ratio - viewRatio) / viewRatio <= minRatioDiffPercent)) { + bestChoice = newSize; + bestChoiceRatio = ratio; } } return bestChoice; @@ -419,25 +479,26 @@ public class QrCamera extends Handler { * picture size and aspect ratio to choose the best one. */ private Size getBestPictureSize(Camera.Parameters parameters) { - final Camera.Size previewSize = parameters.getPreviewSize(); - final double previewRatio = getRatio(previewSize.width, previewSize.height); + final Size previewSize = mPreviewSize; + final double previewRatio = getRatio(previewSize); List<Size> bestChoices = new ArrayList<>(); final List<Size> similarChoices = new ArrayList<>(); // Filter by ratio - for (Camera.Size size : parameters.getSupportedPictureSizes()) { - double ratio = getRatio(size.width, size.height); + for (Camera.Size picSize : parameters.getSupportedPictureSizes()) { + final Size size = toAndroidSize(picSize); + final double ratio = getRatio(size); if (ratio == previewRatio) { - bestChoices.add(new Size(size.width, size.height)); + bestChoices.add(size); } else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) { - similarChoices.add(new Size(size.width, size.height)); + similarChoices.add(size); } } if (bestChoices.size() == 0 && similarChoices.size() == 0) { Log.d(TAG, "No proper picture size, return default picture size"); Camera.Size defaultPictureSize = parameters.getPictureSize(); - return new Size(defaultPictureSize.width, defaultPictureSize.height); + return toAndroidSize(defaultPictureSize); } if (bestChoices.size() == 0) { @@ -447,7 +508,7 @@ public class QrCamera extends Handler { // Get the best by area int bestAreaDifference = Integer.MAX_VALUE; Size bestChoice = null; - final int previewArea = previewSize.width * previewSize.height; + final int previewArea = previewSize.getWidth() * previewSize.getHeight(); for (Size size : bestChoices) { int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea); if (areaDifference < bestAreaDifference) { @@ -458,8 +519,20 @@ public class QrCamera extends Handler { return bestChoice; } - private double getRatio(double x, double y) { - return (x < y) ? x / y : y / x; + private Size rotateIfPortrait(Size size) { + if (mPreviewInPortrait) { + return new Size(size.getHeight(), size.getWidth()); + } else { + return size; + } + } + + private double getRatio(Size size) { + return size.getHeight() / (double) size.getWidth(); + } + + private Size toAndroidSize(Camera.Size size) { + return new Size(size.width, size.height); } @VisibleForTesting diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index b86f4b3715b5..eac6923473b1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.eq; 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; @@ -38,12 +37,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.settingslib.R; import com.android.settingslib.flags.Flags; import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.utils.ThreadUtils; import org.junit.Before; import org.junit.Rule; @@ -54,6 +55,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import java.util.ArrayList; @@ -61,7 +64,7 @@ import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowBluetoothAdapter.class}) +@Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class}) public class BluetoothEventManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -100,6 +103,8 @@ public class BluetoothEventManagerTest { private BluetoothUtils.ErrorListener mErrorListener; @Mock private LocalBluetoothLeBroadcast mBroadcast; + @Mock + private UserManager mUserManager; private Context mContext; private Intent mIntent; @@ -130,6 +135,7 @@ public class BluetoothEventManagerTest { mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1); mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2); mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); BluetoothUtils.setErrorListener(mErrorListener); } @@ -196,6 +202,7 @@ public class BluetoothEventManagerTest { * callback. */ @Test + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); @@ -208,10 +215,12 @@ public class BluetoothEventManagerTest { /** * dispatchProfileConnectionStateChanged should not call {@link - * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off. + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and + * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off. */ @Test - public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() { + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) + public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( @@ -219,16 +228,19 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** * dispatchProfileConnectionStateChanged should not call {@link - * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not - * support audio sharing. + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and + * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support + * audio sharing. */ @Test - public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() { + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) + public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( @@ -236,7 +248,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** @@ -245,6 +258,7 @@ public class BluetoothEventManagerTest { * not ready. */ @Test + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ false, /* workProfile= */ false); @@ -253,7 +267,7 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** @@ -262,6 +276,7 @@ public class BluetoothEventManagerTest { * other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED. */ @Test + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); @@ -270,16 +285,17 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO); - verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); } /** * dispatchProfileConnectionStateChanged should not call {@link - * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for - * work profile. + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and + * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile. */ @Test - public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() { + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) + public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ true); mBluetoothEventManager.dispatchProfileConnectionStateChanged( @@ -287,7 +303,8 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); - verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); } /** @@ -296,7 +313,8 @@ public class BluetoothEventManagerTest { * disconnected and audio sharing is enabled. */ @Test - public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() { + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) + public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() { setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ true, /* workProfile= */ false); mBluetoothEventManager.dispatchProfileConnectionStateChanged( @@ -305,6 +323,27 @@ public class BluetoothEventManagerTest { BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); verify(mBroadcast).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any()); + } + + /** + * dispatchProfileConnectionStateChanged should call {@link + * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and + * audio sharing is enabled. + */ + @Test + @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE) + public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() { + setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */ + true, /* workProfile= */ false); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded(); + verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager); } private void setUpAudioSharing(boolean enableFlag, boolean enableFeature, @@ -325,13 +364,19 @@ public class BluetoothEventManagerTest { LocalBluetoothLeBroadcastAssistant assistant = mock(LocalBluetoothLeBroadcastAssistant.class); when(assistant.isProfileReady()).thenReturn(enableProfile); - LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); - when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); - when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); - when(mBtManager.getProfileManager()).thenReturn(profileManager); - UserManager userManager = mock(UserManager.class); - when(mContext.getSystemService(UserManager.class)).thenReturn(userManager); - when(userManager.isManagedProfile()).thenReturn(workProfile); + when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mUserManager.isManagedProfile()).thenReturn(workProfile); + if (workProfile) { + mBluetoothEventManager = + new BluetoothEventManager( + mLocalAdapter, + mBtManager, + mCachedDeviceManager, + mContext, + /* handler= */ null, + /* userHandle= */ null); + } } @Test @@ -665,4 +710,12 @@ public class BluetoothEventManagerTest { verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); } + + @Implements(value = ThreadUtils.class) + public static class ShadowThreadUtils { + @Implementation + protected static void postOnBackgroundThread(Runnable runnable) { + runnable.run(); + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 0325c0ec7915..b7814127b716 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -1349,6 +1349,36 @@ public class BluetoothUtilsTest { } @Test + public void isMediaDevice_returnsFalse() { + when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant)); + assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse(); + } + + @Test + public void isMediaDevice_returnsTrue() { + when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); + assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue(); + } + + @Test + public void isLeAudioSupported_returnsFalse() { + when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false); + + assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse(); + } + + @Test + public void isLeAudioSupported_returnsTrue() { + when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true); + + assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue(); + } + + @Test public void isTemporaryBondDevice_hasMetadata_returnsTrue() { when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) .thenReturn(TEMP_BOND_METADATA.getBytes()); diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java index a9fd380c2733..76b6aa8b5026 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java @@ -28,6 +28,7 @@ import org.robolectric.annotation.Implements; public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowColorDisplayManager { private boolean mIsReduceBrightColorsActivated; + private int mColorMode; @Implementation @SystemApi @@ -43,4 +44,13 @@ public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowCol return mIsReduceBrightColorsActivated; } + @Implementation + public int getColorMode() { + return mColorMode; + } + + @Implementation + public void setColorMode(int colorMode) { + mColorMode = colorMode; + } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt index ed144bd20234..375dadeba3ad 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt @@ -78,6 +78,10 @@ class AndroidColorScheme( val underSurface: Color, val weatherTemp: Color, val widgetBackground: Color, + val surfaceEffect0: Color, + val surfaceEffect1: Color, + val surfaceEffect2: Color, + val surfaceEffect3: Color, ) { companion object { internal fun color(context: Context, @ColorRes id: Int): Color { @@ -123,6 +127,10 @@ class AndroidColorScheme( underSurface = color(context, R.color.customColorUnderSurface), weatherTemp = color(context, R.color.customColorWeatherTemp), widgetBackground = color(context, R.color.customColorWidgetBackground), + surfaceEffect0 = color(context, R.color.surface_effect_0), + surfaceEffect1 = color(context, R.color.surface_effect_1), + surfaceEffect2 = color(context, R.color.surface_effect_2), + surfaceEffect3 = color(context, R.color.surface_effect_3), ) } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt index 71ec63c1666c..84370ed4d2c7 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt @@ -31,6 +31,7 @@ import com.android.compose.theme.typography.TypeScaleTokens import com.android.compose.theme.typography.TypefaceNames import com.android.compose.theme.typography.TypefaceTokens import com.android.compose.theme.typography.TypographyTokens +import com.android.compose.theme.typography.VariableFontTypeScaleEmphasizedTokens import com.android.compose.theme.typography.platformTypography import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.compose.windowsizeclass.calculateWindowSizeClass @@ -44,9 +45,15 @@ fun PlatformTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Compos val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) } val androidColorScheme = remember(context) { AndroidColorScheme(context) } val typefaceNames = remember(context) { TypefaceNames.get(context) } + val typefaceTokens = remember(typefaceNames) { TypefaceTokens(typefaceNames) } val typography = - remember(typefaceNames) { - platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames)))) + remember(typefaceTokens) { + platformTypography( + TypographyTokens( + TypeScaleTokens(typefaceTokens), + VariableFontTypeScaleEmphasizedTokens(typefaceTokens), + ) + ) } val windowSizeClass = calculateWindowSizeClass() diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt index 1ce1ae3c8a32..652f946a8e70 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt @@ -16,6 +16,7 @@ package com.android.compose.theme.typography +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Typography @@ -25,6 +26,7 @@ import androidx.compose.material3.Typography * Do not use directly and call [MaterialTheme.typography] instead to access the different text * styles. */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) internal fun platformTypography(typographyTokens: TypographyTokens): Typography { return Typography( displayLarge = typographyTokens.displayLarge, @@ -42,5 +44,21 @@ internal fun platformTypography(typographyTokens: TypographyTokens): Typography labelLarge = typographyTokens.labelLarge, labelMedium = typographyTokens.labelMedium, labelSmall = typographyTokens.labelSmall, + // GSF emphasized tokens + displayLargeEmphasized = typographyTokens.displayLargeEmphasized, + displayMediumEmphasized = typographyTokens.displayMediumEmphasized, + displaySmallEmphasized = typographyTokens.displaySmallEmphasized, + headlineLargeEmphasized = typographyTokens.headlineLargeEmphasized, + headlineMediumEmphasized = typographyTokens.headlineMediumEmphasized, + headlineSmallEmphasized = typographyTokens.headlineSmallEmphasized, + titleLargeEmphasized = typographyTokens.titleLargeEmphasized, + titleMediumEmphasized = typographyTokens.titleMediumEmphasized, + titleSmallEmphasized = typographyTokens.titleSmallEmphasized, + bodyLargeEmphasized = typographyTokens.bodyLargeEmphasized, + bodyMediumEmphasized = typographyTokens.bodyMediumEmphasized, + bodySmallEmphasized = typographyTokens.bodySmallEmphasized, + labelLargeEmphasized = typographyTokens.labelLargeEmphasized, + labelMediumEmphasized = typographyTokens.labelMediumEmphasized, + labelSmallEmphasized = typographyTokens.labelSmallEmphasized, ) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt index 13acfd6b85f3..280b8d95c8b7 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt @@ -34,6 +34,29 @@ internal class TypefaceTokens(typefaceNames: TypefaceNames) { private val brandFont = DeviceFontFamilyName(typefaceNames.brand) private val plainFont = DeviceFontFamilyName(typefaceNames.plain) + // Google Sans Flex emphasized styles + private val displayLargeEmphasizedFont = + DeviceFontFamilyName("variable-display-large-emphasized") + private val displayMediumEmphasizedFont = + DeviceFontFamilyName("variable-display-medium-emphasized") + private val displaySmallEmphasizedFont = + DeviceFontFamilyName("variable-display-small-emphasized") + private val headlineLargeEmphasizedFont = + DeviceFontFamilyName("variable-headline-large-emphasized") + private val headlineMediumEmphasizedFont = + DeviceFontFamilyName("variable-headline-medium-emphasized") + private val headlineSmallEmphasizedFont = + DeviceFontFamilyName("variable-headline-small-emphasized") + private val titleLargeEmphasizedFont = DeviceFontFamilyName("variable-title-large-emphasized") + private val titleMediumEmphasizedFont = DeviceFontFamilyName("variable-title-medium-emphasized") + private val titleSmallEmphasizedFont = DeviceFontFamilyName("variable-title-small-emphasized") + private val bodyLargeEmphasizedFont = DeviceFontFamilyName("variable-body-large-emphasized") + private val bodyMediumEmphasizedFont = DeviceFontFamilyName("variable-body-medium-emphasized") + private val bodySmallEmphasizedFont = DeviceFontFamilyName("variable-body-small-emphasized") + private val labelLargeEmphasizedFont = DeviceFontFamilyName("variable-label-large-emphasized") + private val labelMediumEmphasizedFont = DeviceFontFamilyName("variable-label-medium-emphasized") + private val labelSmallEmphasizedFont = DeviceFontFamilyName("variable-label-small-emphasized") + val brand = FontFamily( Font(brandFont, weight = WeightMedium), @@ -44,6 +67,22 @@ internal class TypefaceTokens(typefaceNames: TypefaceNames) { Font(plainFont, weight = WeightMedium), Font(plainFont, weight = WeightRegular), ) + + val displayLargeEmphasized = FontFamily(Font(displayLargeEmphasizedFont)) + val displayMediumEmphasized = FontFamily(Font(displayMediumEmphasizedFont)) + val displaySmallEmphasized = FontFamily(Font(displaySmallEmphasizedFont)) + val headlineLargeEmphasized = FontFamily(Font(headlineLargeEmphasizedFont)) + val headlineMediumEmphasized = FontFamily(Font(headlineMediumEmphasizedFont)) + val headlineSmallEmphasized = FontFamily(Font(headlineSmallEmphasizedFont)) + val titleLargeEmphasized = FontFamily(Font(titleLargeEmphasizedFont)) + val titleMediumEmphasized = FontFamily(Font(titleMediumEmphasizedFont)) + val titleSmallEmphasized = FontFamily(Font(titleSmallEmphasizedFont)) + val bodyLargeEmphasized = FontFamily(Font(bodyLargeEmphasizedFont)) + val bodyMediumEmphasized = FontFamily(Font(bodyMediumEmphasizedFont)) + val bodySmallEmphasized = FontFamily(Font(bodySmallEmphasizedFont)) + val labelLargeEmphasized = FontFamily(Font(labelLargeEmphasizedFont)) + val labelMediumEmphasized = FontFamily(Font(labelMediumEmphasizedFont)) + val labelSmallEmphasized = FontFamily(Font(labelSmallEmphasizedFont)) } internal data class TypefaceNames diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt index 38aadb8c4d15..41156478b1c5 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt @@ -18,7 +18,10 @@ package com.android.compose.theme.typography import androidx.compose.ui.text.TextStyle -internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) { +internal class TypographyTokens( + typeScaleTokens: TypeScaleTokens, + variableTypeScaleTokens: VariableFontTypeScaleEmphasizedTokens, +) { val bodyLarge = TextStyle( fontFamily = typeScaleTokens.bodyLargeFont, @@ -139,4 +142,112 @@ internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) { lineHeight = typeScaleTokens.titleSmallLineHeight, letterSpacing = typeScaleTokens.titleSmallTracking, ) + // GSF emphasized styles + // note: we don't need to define fontWeight or axes values because they are pre-defined + // as part of the font family in fonts_customization.xml (for performance optimization) + val displayLargeEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.displayLargeFont, + fontSize = variableTypeScaleTokens.displayLargeSize, + lineHeight = variableTypeScaleTokens.displayLargeLineHeight, + letterSpacing = variableTypeScaleTokens.displayLargeTracking, + ) + val displayMediumEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.displayMediumFont, + fontSize = variableTypeScaleTokens.displayMediumSize, + lineHeight = variableTypeScaleTokens.displayMediumLineHeight, + letterSpacing = variableTypeScaleTokens.displayMediumTracking, + ) + val displaySmallEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.displaySmallFont, + fontSize = variableTypeScaleTokens.displaySmallSize, + lineHeight = variableTypeScaleTokens.displaySmallLineHeight, + letterSpacing = variableTypeScaleTokens.displaySmallTracking, + ) + val headlineLargeEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.headlineLargeFont, + fontSize = variableTypeScaleTokens.headlineLargeSize, + lineHeight = variableTypeScaleTokens.headlineLargeLineHeight, + letterSpacing = variableTypeScaleTokens.headlineLargeTracking, + ) + val headlineMediumEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.headlineMediumFont, + fontSize = variableTypeScaleTokens.headlineMediumSize, + lineHeight = variableTypeScaleTokens.headlineMediumLineHeight, + letterSpacing = variableTypeScaleTokens.headlineMediumTracking, + ) + val headlineSmallEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.headlineSmallFont, + fontSize = variableTypeScaleTokens.headlineSmallSize, + lineHeight = variableTypeScaleTokens.headlineSmallLineHeight, + letterSpacing = variableTypeScaleTokens.headlineSmallTracking, + ) + val titleLargeEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.titleLargeFont, + fontSize = variableTypeScaleTokens.titleLargeSize, + lineHeight = variableTypeScaleTokens.titleLargeLineHeight, + letterSpacing = variableTypeScaleTokens.titleLargeTracking, + ) + val titleMediumEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.titleMediumFont, + fontSize = variableTypeScaleTokens.titleMediumSize, + lineHeight = variableTypeScaleTokens.titleMediumLineHeight, + letterSpacing = variableTypeScaleTokens.titleMediumTracking, + ) + val titleSmallEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.titleSmallFont, + fontSize = variableTypeScaleTokens.titleSmallSize, + lineHeight = variableTypeScaleTokens.titleSmallLineHeight, + letterSpacing = variableTypeScaleTokens.titleSmallTracking, + ) + val bodyLargeEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.bodyLargeFont, + fontSize = variableTypeScaleTokens.bodyLargeSize, + lineHeight = variableTypeScaleTokens.bodyLargeLineHeight, + letterSpacing = variableTypeScaleTokens.bodyLargeTracking, + ) + val bodyMediumEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.bodyMediumFont, + fontSize = variableTypeScaleTokens.bodyMediumSize, + lineHeight = variableTypeScaleTokens.bodyMediumLineHeight, + letterSpacing = variableTypeScaleTokens.bodyMediumTracking, + ) + val bodySmallEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.bodySmallFont, + fontSize = variableTypeScaleTokens.bodySmallSize, + lineHeight = variableTypeScaleTokens.bodySmallLineHeight, + letterSpacing = variableTypeScaleTokens.bodySmallTracking, + ) + val labelLargeEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.labelLargeFont, + fontSize = variableTypeScaleTokens.labelLargeSize, + lineHeight = variableTypeScaleTokens.labelLargeLineHeight, + letterSpacing = variableTypeScaleTokens.labelLargeTracking, + ) + val labelMediumEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.labelMediumFont, + fontSize = variableTypeScaleTokens.labelMediumSize, + lineHeight = variableTypeScaleTokens.labelMediumLineHeight, + letterSpacing = variableTypeScaleTokens.labelMediumTracking, + ) + val labelSmallEmphasized = + TextStyle( + fontFamily = variableTypeScaleTokens.labelSmallFont, + fontSize = variableTypeScaleTokens.labelSmallSize, + lineHeight = variableTypeScaleTokens.labelSmallLineHeight, + letterSpacing = variableTypeScaleTokens.labelSmallTracking, + ) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt new file mode 100644 index 000000000000..52b93904a4a2 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt @@ -0,0 +1,82 @@ +/* + * 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.compose.theme.typography + +import androidx.compose.ui.unit.sp + +internal class VariableFontTypeScaleEmphasizedTokens(typefaceTokens: TypefaceTokens) { + val bodyLargeFont = typefaceTokens.bodyLargeEmphasized + val bodyLargeLineHeight = 24.0.sp + val bodyLargeSize = 16.sp + val bodyLargeTracking = 0.0.sp + val bodyMediumFont = typefaceTokens.bodyMediumEmphasized + val bodyMediumLineHeight = 20.0.sp + val bodyMediumSize = 14.sp + val bodyMediumTracking = 0.0.sp + val bodySmallFont = typefaceTokens.bodySmallEmphasized + val bodySmallLineHeight = 16.0.sp + val bodySmallSize = 12.sp + val bodySmallTracking = 0.0.sp + val displayLargeFont = typefaceTokens.displayLargeEmphasized + val displayLargeLineHeight = 64.0.sp + val displayLargeSize = 57.sp + val displayLargeTracking = 0.0.sp + val displayMediumFont = typefaceTokens.displayMediumEmphasized + val displayMediumLineHeight = 52.0.sp + val displayMediumSize = 45.sp + val displayMediumTracking = 0.0.sp + val displaySmallFont = typefaceTokens.displaySmallEmphasized + val displaySmallLineHeight = 44.0.sp + val displaySmallSize = 36.sp + val displaySmallTracking = 0.0.sp + val headlineLargeFont = typefaceTokens.headlineLargeEmphasized + val headlineLargeLineHeight = 40.0.sp + val headlineLargeSize = 32.sp + val headlineLargeTracking = 0.0.sp + val headlineMediumFont = typefaceTokens.headlineMediumEmphasized + val headlineMediumLineHeight = 36.0.sp + val headlineMediumSize = 28.sp + val headlineMediumTracking = 0.0.sp + val headlineSmallFont = typefaceTokens.headlineSmallEmphasized + val headlineSmallLineHeight = 32.0.sp + val headlineSmallSize = 24.sp + val headlineSmallTracking = 0.0.sp + val labelLargeFont = typefaceTokens.labelLargeEmphasized + val labelLargeLineHeight = 20.0.sp + val labelLargeSize = 14.sp + val labelLargeTracking = 0.0.sp + val labelMediumFont = typefaceTokens.labelMediumEmphasized + val labelMediumLineHeight = 16.0.sp + val labelMediumSize = 12.sp + val labelMediumTracking = 0.0.sp + val labelSmallFont = typefaceTokens.labelSmallEmphasized + val labelSmallLineHeight = 16.0.sp + val labelSmallSize = 11.sp + val labelSmallTracking = 0.0.sp + val titleLargeFont = typefaceTokens.titleLargeEmphasized + val titleLargeLineHeight = 28.0.sp + val titleLargeSize = 22.sp + val titleLargeTracking = 0.0.sp + val titleMediumFont = typefaceTokens.titleMediumEmphasized + val titleMediumLineHeight = 24.0.sp + val titleMediumSize = 16.sp + val titleMediumTracking = 0.0.sp + val titleSmallFont = typefaceTokens.titleSmallEmphasized + val titleSmallLineHeight = 20.0.sp + val titleSmallSize = 14.sp + val titleSmallTracking = 0.0.sp +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt index 48dee240a1df..f1b273ae5741 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction @@ -102,6 +103,8 @@ private fun ContentScope.BouncerOverlay( viewModel, dialogFactory, Modifier.element(Bouncer.Elements.Content) + // TODO(b/393516240): Use the same sysuiResTag() as views instead. + .testTag(Bouncer.Elements.Content.testTag) .overscroll(verticalOverscrollEffect) .sysuiResTag(Bouncer.TestTags.Root) .fillMaxSize(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 5e61af634bbc..aa07370aa9cf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.composable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult @@ -55,7 +56,11 @@ constructor( @Composable override fun ContentScope.Content(modifier: Modifier) { - LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier) + LockscreenScene( + lockscreenContent = lockscreenContent, + // TODO(b/393516240): Use the same sysuiResTag() as views instead. + modifier = modifier.testTag(key.rootElementKey.testTag), + ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index b11c83c778f4..4b3ebc2bd53d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -35,9 +35,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon as MaterialIcon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -73,11 +73,15 @@ import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState +import com.android.systemui.volume.ui.slider.AccessibilityParams +import com.android.systemui.volume.ui.slider.Haptics +import com.android.systemui.volume.ui.slider.Slider import kotlin.math.round import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +@OptIn(ExperimentalMaterial3Api::class) @Composable fun VolumeSlider( state: SliderState, @@ -102,17 +106,6 @@ fun VolumeSlider( return } - val value by valueState(state) - val interactionSource = remember { MutableInteractionSource() } - val hapticsViewModel: SliderHapticsViewModel? = - setUpHapticsViewModel( - value, - state.valueRange, - state.hapticFilter, - interactionSource, - hapticsViewModelFactory, - ) - Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) { Row( horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -134,60 +127,30 @@ fun VolumeSlider( ) button?.invoke() } + Slider( - value = value, + value = state.value, valueRange = state.valueRange, - onValueChange = { newValue -> - hapticsViewModel?.addVelocityDataPoint(newValue) - onValueChange(newValue) - }, - onValueChangeFinished = { - hapticsViewModel?.onValueChangeEnded() - onValueChangeFinished?.invoke() - }, - enabled = state.isEnabled, + onValueChanged = onValueChange, + onValueChangeFinished = { onValueChangeFinished?.invoke() }, + isEnabled = state.isEnabled, + stepDistance = state.a11yStep, + accessibilityParams = + AccessibilityParams( + label = state.label, + disabledMessage = state.disabledMessage, + currentStateDescription = state.a11yStateDescription, + ), + haptics = + hapticsViewModelFactory?.let { + Haptics.Enabled( + hapticsViewModelFactory = it, + hapticFilter = state.hapticFilter, + orientation = Orientation.Horizontal, + ) + } ?: Haptics.Disabled, modifier = - Modifier.height(40.dp) - .padding(top = 4.dp, bottom = 12.dp) - .sysuiResTag(state.label) - .clearAndSetSemantics { - if (state.isEnabled) { - contentDescription = state.label - state.a11yClickDescription?.let { - customActions = - listOf( - CustomAccessibilityAction(it) { - onIconTapped() - true - } - ) - } - - state.a11yStateDescription?.let { stateDescription = it } - progressBarRangeInfo = - ProgressBarRangeInfo(state.value, state.valueRange) - } else { - disabled() - contentDescription = - state.disabledMessage?.let { "${state.label}, $it" } ?: state.label - } - setProgress { targetValue -> - val targetDirection = - when { - targetValue > value -> 1 - targetValue < value -> -1 - else -> 0 - } - - val newValue = - (value + targetDirection * state.a11yStep).coerceIn( - state.valueRange.start, - state.valueRange.endInclusive, - ) - onValueChange(newValue) - true - } - }, + Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label), ) state.disabledMessage?.let { disabledMessage -> AnimatedVisibility(visible = !state.isEnabled) { @@ -348,7 +311,7 @@ private fun SliderIcon( } @Composable -fun setUpHapticsViewModel( +private fun setUpHapticsViewModel( value: Float, valueRange: ClosedFloatingPointRange<Float>, hapticFilter: SliderHapticFeedbackFilter, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 907b5bc2143a..05958a212f47 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -169,7 +169,7 @@ internal fun Modifier.element( Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates) } .then(ElementModifier(layoutImpl, currentTransitionStates, content, key)) - .testTag(key.testTag) + .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) } } /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 53d0ee1d2045..404f1b217026 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -66,6 +66,8 @@ fun SceneTransitionLayout( swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f, + // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting(). + implicitTestTags: Boolean = false, builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit, ) { SceneTransitionLayoutForTesting( @@ -74,6 +76,7 @@ fun SceneTransitionLayout( swipeSourceDetector, swipeDetector, transitionInterceptionThreshold, + implicitTestTags = implicitTestTags, onLayoutImpl = null, builder = builder, ) @@ -727,10 +730,8 @@ class FixedDistance(private val distance: Dp) : UserActionDistance { } /** - * An internal version of [SceneTransitionLayout] to be used for tests. - * - * Important: You should use this only in tests and if you need to access the underlying - * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout]. + * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the + * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements. */ @Composable internal fun SceneTransitionLayoutForTesting( @@ -743,6 +744,7 @@ internal fun SceneTransitionLayoutForTesting( sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() }, ancestors: List<Ancestor> = remember { emptyList() }, lookaheadScope: LookaheadScope? = null, + implicitTestTags: Boolean = true, builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit, ) { val density = LocalDensity.current @@ -767,6 +769,7 @@ internal fun SceneTransitionLayoutForTesting( directionChangeSlop = directionChangeSlop, defaultEffectFactory = defaultEffectFactory, decayAnimationSpec = decayAnimationSpec, + implicitTestTags = implicitTestTags, ) .also { onLayoutImpl?.invoke(it) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 53996d25afdb..e3c4eb0f8bea 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -122,6 +122,9 @@ internal class SceneTransitionLayoutImpl( * This is used to enable transformations and shared elements across NestedSTLs. */ internal val ancestors: List<Ancestor> = emptyList(), + + /** Whether elements and scene should be tagged using `Modifier.testTag`. */ + internal val implicitTestTags: Boolean = false, lookaheadScope: LookaheadScope? = null, defaultEffectFactory: OverscrollFactory, ) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 9ca45fe92ad5..149a9e7c4705 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -173,7 +173,7 @@ internal sealed class Content( .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) { Modifier.container(containerState) } - .testTag(key.testTag) + .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) } ) { CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) { scope.content() @@ -301,6 +301,7 @@ internal class ContentScopeImpl( sharedElementMap = layoutImpl.elements, ancestors = ancestors, lookaheadScope = layoutImpl.lookaheadScope, + implicitTestTags = layoutImpl.implicitTestTags, ) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 338fb9b674a1..86cbfe4f1a8b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -227,7 +227,7 @@ class ElementTest { to = SceneB, transitionLayout = { state -> coroutineScope = rememberCoroutineScope() - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneA) { Box(Modifier.size(layoutSize)) { // Transformed element @@ -633,7 +633,7 @@ class ElementTest { val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) } scene(SceneB) {} } @@ -674,7 +674,7 @@ class ElementTest { CompositionLocalProvider( LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory() ) { - SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) { scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Spacer(Modifier.fillMaxSize()) } @@ -734,7 +734,7 @@ class ElementTest { CompositionLocalProvider( LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory() ) { - SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) { scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Spacer( Modifier.overscroll(verticalOverscrollEffect) @@ -834,7 +834,7 @@ class ElementTest { CompositionLocalProvider( LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory() ) { - SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) { scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Spacer(Modifier.fillMaxSize()) } @@ -893,7 +893,7 @@ class ElementTest { CompositionLocalProvider( LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory() ) { - SceneTransitionLayout( + SceneTransitionLayoutForTesting( state = state, modifier = Modifier.size(layoutWidth, layoutHeight), ) { @@ -970,7 +970,7 @@ class ElementTest { rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop - SceneTransitionLayout( + SceneTransitionLayoutForTesting( state = state, modifier = Modifier.size(layoutWidth, layoutHeight), ) { @@ -1057,7 +1057,7 @@ class ElementTest { rule.setContent { coroutineScope = rememberCoroutineScope() - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneA) { Box(Modifier.size(layoutSize)) { Box( @@ -1374,7 +1374,7 @@ class ElementTest { val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state, Modifier.size(layoutSize)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } } @@ -1742,7 +1742,7 @@ class ElementTest { val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Foo(offset = 0.dp) } scene(SceneB) { Foo(offset = 20.dp) } scene(SceneC) { Foo(offset = 40.dp) } @@ -1828,7 +1828,7 @@ class ElementTest { val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } // Define A after B so that Foo is placed in A during A <=> B. @@ -1887,7 +1887,7 @@ class ElementTest { val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneA) { Foo() } scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt index 04c762f43907..98ecb644878b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt @@ -90,7 +90,7 @@ class OverlayTest { lateinit var coroutineScope: CoroutineScope rule.setContent { coroutineScope = rememberCoroutineScope() - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } overlay(OverlayA) { Foo() } } @@ -132,7 +132,7 @@ class OverlayTest { lateinit var coroutineScope: CoroutineScope rule.setContent { coroutineScope = rememberCoroutineScope() - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } overlay(OverlayA) { Foo() } overlay(OverlayB) { Foo() } @@ -230,7 +230,7 @@ class OverlayTest { lateinit var coroutineScope: CoroutineScope rule.setContent { coroutineScope = rememberCoroutineScope() - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } } overlay(OverlayA) { MovableBar() } overlay(OverlayB) { MovableBar() } @@ -302,7 +302,7 @@ class OverlayTest { } var alignment by mutableStateOf(Alignment.Center) rule.setContent { - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } overlay(OverlayA, alignment = alignment) { Foo() } } @@ -761,7 +761,7 @@ class OverlayTest { val movableElementChildTag = "movableElementChildTag" val scope = rule.setContentAndCreateMainScope { - SceneTransitionLayout(state) { + SceneTransitionLayoutForTesting(state) { scene(SceneA) { MovableElement(key, Modifier) { content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt index 2bf235846b32..366b11d9fabd 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -250,7 +250,7 @@ class PredictiveBackHandlerTest { } rule.setContent { - SceneTransitionLayout(state, Modifier.size(200.dp)) { + SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) { scene(SceneA) { Box(Modifier.fillMaxSize()) } overlay(OverlayA) { Box(Modifier.fillMaxSize()) } overlay(OverlayB) { Box(Modifier.fillMaxSize()) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index d7f7a514682c..fa7661b6d102 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -97,7 +97,7 @@ class SceneTransitionLayoutTest { MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions) } - SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) { + SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) { scene(SceneA, userActions = mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) { SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd)) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 751b31481e3a..11abbbec79bf 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -763,7 +763,7 @@ class SwipeToSceneTest { var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop - SceneTransitionLayout(state, Modifier.size(layoutSize)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) { scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) { Box(Modifier.fillMaxSize()) } @@ -837,7 +837,7 @@ class SwipeToSceneTest { rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { - SceneTransitionLayout(state, Modifier.size(layoutSize)) { + SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) { scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) { Box(Modifier.fillMaxSize()) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt index bb511bc27317..8b568928bde0 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt @@ -40,7 +40,7 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests import com.android.compose.animation.scene.Scale import com.android.compose.animation.scene.SceneKey -import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.SceneTransitionLayoutForTesting import com.android.compose.animation.scene.SceneTransitions import com.android.compose.animation.scene.TestScenes import com.android.compose.animation.scene.testNestedTransition @@ -114,7 +114,7 @@ class NestedElementTransformationTest { @Composable (states: List<MutableSceneTransitionLayoutState>) -> Unit = { states -> - SceneTransitionLayout(states[0]) { + SceneTransitionLayoutForTesting(states[0]) { scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) }) scene( TestScenes.SceneB, diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt index 6d47babd716a..e56d1bed4c25 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt @@ -30,5 +30,7 @@ fun TestContentScope( content: @Composable ContentScope.() -> Unit, ) { val state = rememberMutableSceneTransitionLayoutState(currentScene) - SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) } + SceneTransitionLayout(state, modifier, implicitTestTags = true) { + scene(currentScene, content = content) + } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index f94a7ed77341..a362a370328a 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -137,7 +137,7 @@ fun ComposeContentTestRule.testTransition( }, changeState = changeState, transitionLayout = { state -> - SceneTransitionLayout(state, layoutModifier) { + SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) { scene(fromScene, content = fromSceneContent) scene(toScene, content = toSceneContent) } @@ -163,7 +163,7 @@ fun ComposeContentTestRule.testShowOverlayTransition( ) }, transitionLayout = { state -> - SceneTransitionLayout(state) { + SceneTransitionLayout(state, implicitTestTags = true) { scene(fromScene) { fromSceneContent() } overlay(overlay) { overlayContent() } } @@ -191,7 +191,7 @@ fun ComposeContentTestRule.testHideOverlayTransition( ) }, transitionLayout = { state -> - SceneTransitionLayout(state) { + SceneTransitionLayout(state, implicitTestTags = true) { scene(toScene) { toSceneContent() } overlay(overlay) { overlayContent() } } @@ -223,7 +223,7 @@ fun ComposeContentTestRule.testReplaceOverlayTransition( ) }, transitionLayout = { state -> - SceneTransitionLayout(state) { + SceneTransitionLayout(state, implicitTestTags = true) { scene(currentScene) { currentSceneContent() } overlay(from, alignment = fromAlignment) { fromContent() } overlay(to, alignment = toAlignment) { toContent() } @@ -273,7 +273,7 @@ fun MotionTestRule<ComposeToolkit>.recordTransition( } } - SceneTransitionLayout(state, layoutModifier) { + SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) { scene(fromScene, content = fromSceneContent) scene(toScene, content = toSceneContent) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index aad1276d76e5..654478af3fb0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -28,6 +28,7 @@ import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProvider import com.android.systemui.plugins.clocks.ClockSettings +import com.android.systemui.shared.clocks.FlexClockController.Companion.AXIS_PRESETS import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes private val TAG = DefaultClockProvider::class.simpleName @@ -98,16 +99,16 @@ class DefaultClockProvider( throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } - val fontAxes = - if (!isClockReactiveVariantsEnabled) listOf() - else getDefaultAxes(settings).merge(settings.axes) return ClockPickerConfig( settings.clockId ?: DEFAULT_CLOCK_ID, resources.getString(R.string.clock_default_name), resources.getString(R.string.clock_default_description), resources.getDrawable(R.drawable.clock_default_thumbnail, null), isReactiveToTone = true, - axes = fontAxes, + axes = + if (!isClockReactiveVariantsEnabled) emptyList() + else getDefaultAxes(settings).merge(settings.axes), + axisPresets = if (!isClockReactiveVariantsEnabled) emptyList() else AXIS_PRESETS, ) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index ac1c5a8dfaf3..1a1033ba42e0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -132,7 +132,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController listOf( GSFAxes.WEIGHT.toClockAxis( type = AxisType.Float, - currentValue = 400f, + currentValue = 475f, name = "Weight", description = "Glyph Weight", ), @@ -161,5 +161,59 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController GSFAxes.ROUND.toClockAxisSetting(100f), GSFAxes.SLANT.toClockAxisSetting(0f), ) + + val AXIS_PRESETS = + listOf( + FONT_AXES.map { it.toSetting() }, + LEGACY_FLEX_SETTINGS, + listOf( // Porcelain + GSFAxes.WEIGHT.toClockAxisSetting(500f), + GSFAxes.WIDTH.toClockAxisSetting(100f), + GSFAxes.ROUND.toClockAxisSetting(0f), + GSFAxes.SLANT.toClockAxisSetting(0f), + ), + listOf( // Midnight + GSFAxes.WEIGHT.toClockAxisSetting(300f), + GSFAxes.WIDTH.toClockAxisSetting(100f), + GSFAxes.ROUND.toClockAxisSetting(100f), + GSFAxes.SLANT.toClockAxisSetting(-10f), + ), + listOf( // Sterling + GSFAxes.WEIGHT.toClockAxisSetting(1000f), + GSFAxes.WIDTH.toClockAxisSetting(100f), + GSFAxes.ROUND.toClockAxisSetting(0f), + GSFAxes.SLANT.toClockAxisSetting(0f), + ), + listOf( // Smoky Green + GSFAxes.WEIGHT.toClockAxisSetting(150f), + GSFAxes.WIDTH.toClockAxisSetting(50f), + GSFAxes.ROUND.toClockAxisSetting(0f), + GSFAxes.SLANT.toClockAxisSetting(0f), + ), + listOf( // Iris + GSFAxes.WEIGHT.toClockAxisSetting(500f), + GSFAxes.WIDTH.toClockAxisSetting(100f), + GSFAxes.ROUND.toClockAxisSetting(100f), + GSFAxes.SLANT.toClockAxisSetting(0f), + ), + listOf( // Margarita + GSFAxes.WEIGHT.toClockAxisSetting(300f), + GSFAxes.WIDTH.toClockAxisSetting(30f), + GSFAxes.ROUND.toClockAxisSetting(100f), + GSFAxes.SLANT.toClockAxisSetting(-10f), + ), + listOf( // Raspberry + GSFAxes.WEIGHT.toClockAxisSetting(700f), + GSFAxes.WIDTH.toClockAxisSetting(140f), + GSFAxes.ROUND.toClockAxisSetting(100f), + GSFAxes.SLANT.toClockAxisSetting(-7f), + ), + listOf( // Ultra Blue + GSFAxes.WEIGHT.toClockAxisSetting(850f), + GSFAxes.WIDTH.toClockAxisSetting(130f), + GSFAxes.ROUND.toClockAxisSetting(0f), + GSFAxes.SLANT.toClockAxisSetting(0f), + ), + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt index 781e416e6374..ede29d8f8f75 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt @@ -26,6 +26,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos @@ -173,6 +176,58 @@ class PackageInstallerMonitorTest : SysuiTestCase() { } @Test + fun onCreateUpdatedSession_ignoreNullPackageNameSessions() = + kosmos.runTest { + val nullPackageSession = + SessionInfo().apply { + sessionId = 1 + appPackageName = null + appIcon = icon1 + } + + val wellFormedSession = + SessionInfo().apply { + sessionId = 2 + appPackageName = "pkg_name" + appIcon = icon2 + } + + defaultSessions = listOf(wellFormedSession) + + whenever(packageInstaller.allSessions).thenReturn(defaultSessions) + whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession) + whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession) + + val packageInstallerMonitor = + PackageInstallerMonitor( + handler, + backgroundScope, + logcatLogBuffer("PackageInstallerRepositoryImplTest"), + packageInstaller, + ) + + val sessions by collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser) + + // Verify flow updated with the new session + assertThat(sessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions) + + val callback = + withArgCaptor<PackageInstaller.SessionCallback> { + verify(packageInstaller).registerSessionCallback(capture(), eq(handler)) + } + + // New session added + callback.onCreated(nullPackageSession.sessionId) + + // Verify flow updated with the new session + assertThat(sessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions) + } + + @Test fun installSessions_newSessionsAreAdded() = testScope.runTest { val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt index e53155de653d..ed73d89db2c7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt @@ -21,6 +21,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.domain.interactor.communalInteractor @@ -28,12 +30,12 @@ import com.android.systemui.communal.domain.interactor.communalSettingsInteracto import com.android.systemui.communal.domain.interactor.setCommunalEnabled import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -42,46 +44,64 @@ import org.junit.runner.RunWith @EnableFlags(FLAG_COMMUNAL_HUB) @RunWith(AndroidJUnit4::class) class CommunalOngoingContentStartableTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope + private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val mediaRepository = kosmos.fakeCommunalMediaRepository - private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository + private var showUmoOnHub = true - private lateinit var underTest: CommunalOngoingContentStartable + private val Kosmos.underTest by + Kosmos.Fixture { + CommunalOngoingContentStartable( + bgScope = applicationCoroutineScope, + communalInteractor = communalInteractor, + communalMediaRepository = communalMediaRepository, + communalSettingsInteractor = communalSettingsInteractor, + communalSmartspaceRepository = communalSmartspaceRepository, + showUmoOnHub = showUmoOnHub, + ) + } @Before fun setUp() { kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) - underTest = - CommunalOngoingContentStartable( - bgScope = kosmos.applicationCoroutineScope, - communalInteractor = kosmos.communalInteractor, - communalMediaRepository = mediaRepository, - communalSettingsInteractor = kosmos.communalSettingsInteractor, - communalSmartspaceRepository = smartspaceRepository, - ) } @Test - fun testListenForOngoingContentWhenCommunalIsEnabled() = - testScope.runTest { + fun testListenForOngoingContent() = + kosmos.runTest { + underTest.start() + + assertThat(fakeCommunalMediaRepository.isListening()).isFalse() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() + + kosmos.setCommunalEnabled(true) + + assertThat(fakeCommunalMediaRepository.isListening()).isTrue() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() + + kosmos.setCommunalEnabled(false) + + assertThat(fakeCommunalMediaRepository.isListening()).isFalse() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() + } + + @Test + fun testListenForOngoingContent_showUmoFalse() = + kosmos.runTest { + showUmoOnHub = false underTest.start() - runCurrent() - assertThat(mediaRepository.isListening()).isFalse() - assertThat(smartspaceRepository.isListening()).isFalse() + assertThat(fakeCommunalMediaRepository.isListening()).isFalse() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() kosmos.setCommunalEnabled(true) - runCurrent() - assertThat(mediaRepository.isListening()).isTrue() - assertThat(smartspaceRepository.isListening()).isTrue() + // Media listening does not start when UMO is disabled. + assertThat(fakeCommunalMediaRepository.isListening()).isFalse() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() kosmos.setCommunalEnabled(false) - runCurrent() - assertThat(mediaRepository.isListening()).isFalse() - assertThat(smartspaceRepository.isListening()).isFalse() + assertThat(fakeCommunalMediaRepository.isListening()).isFalse() + assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt index 943ada9346e7..4e14fec8408f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt @@ -18,9 +18,6 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator -import android.icu.text.MeasureFormat -import android.icu.util.Measure -import android.icu.util.MeasureUnit import android.testing.TestableLooper import android.view.View import android.widget.SeekBar @@ -33,7 +30,6 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat -import java.util.Locale import org.junit.Before import org.junit.Rule import org.junit.Test @@ -65,11 +61,11 @@ class SeekBarObserverTest : SysuiTestCase() { fun setUp() { context.orCreateTestableResources.addOverride( R.dimen.qs_media_enabled_seekbar_height, - enabledHeight, + enabledHeight ) context.orCreateTestableResources.addOverride( R.dimen.qs_media_disabled_seekbar_height, - disabledHeight, + disabledHeight ) seekBarView = SeekBar(context) @@ -114,31 +110,14 @@ class SeekBarObserverTest : SysuiTestCase() { @Test fun seekBarProgress() { - val elapsedTime = 3000 - val duration = (1.5 * 60 * 60 * 1000).toInt() // WHEN part of the track has been played - val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true) + val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true) observer.onChanged(data) // THEN seek bar shows the progress - assertThat(seekBarView.progress).isEqualTo(elapsedTime) - assertThat(seekBarView.max).isEqualTo(duration) - - val expectedProgress = - MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) - .formatMeasures(Measure(3, MeasureUnit.SECOND)) - val expectedDuration = - MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) - .formatMeasures( - Measure(1, MeasureUnit.HOUR), - Measure(30, MeasureUnit.MINUTE), - Measure(0, MeasureUnit.SECOND), - ) - val desc = - context.getString( - R.string.controls_media_seekbar_description, - expectedProgress, - expectedDuration, - ) + assertThat(seekBarView.progress).isEqualTo(3000) + assertThat(seekBarView.max).isEqualTo(120000) + + val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00") assertThat(seekBarView.contentDescription).isEqualTo(desc) } 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 917f3564a1bd..80ce43d61003 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 @@ -65,8 +65,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java index 1899b7d1f1bb..0e5e3330ccd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java @@ -423,5 +423,15 @@ public class TileQueryHelperTest extends SysuiTestCase { @Override public void destroy() {} + + @Override + public boolean isDestroyed() { + return false; + } + + @Override + public int getCurrentTileUser() { + return 0; + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt index 5a58597bc097..67fb1003a6ce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt @@ -56,166 +56,178 @@ class TilesAvailabilityInteractorTest(flags: FlagsParameterization) : SysuiTestC private val createdTiles = mutableListOf<FakeQSTile>() - private val kosmos = testKosmos().apply { - tileAvailabilityInteractorsMap = buildMap { - put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor) - put(WORK_MODE_TILE_SPEC, FakeTileAvailabilityInteractor( - mapOf( - fakeUserRepository.getSelectedUserInfo().id to flowOf(true), - ).withDefault { flowOf(false) } - )) - put(HOTSPOT_TILE_SPEC, FakeTileAvailabilityInteractor( - emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) } - )) - } + private val kosmos = + testKosmos().apply { + tileAvailabilityInteractorsMap = buildMap { + put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor) + put( + WORK_MODE_TILE_SPEC, + FakeTileAvailabilityInteractor( + mapOf(fakeUserRepository.getSelectedUserInfo().id to flowOf(true)) + .withDefault { flowOf(false) } + ), + ) + put( + HOTSPOT_TILE_SPEC, + FakeTileAvailabilityInteractor( + emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) } + ), + ) + } - qsTileFactory = constantFactory( - tilesForCreator( + qsTileFactory = + constantFactory( + tilesForCreator( userRepository.getSelectedUserInfo().id, mapOf( - AIRPLANE_MODE_TILE_SPEC to false, - WORK_MODE_TILE_SPEC to false, - HOTSPOT_TILE_SPEC to true, - INTERNET_TILE_SPEC to true, - FLASHLIGHT_TILE_SPEC to false, - ) + AIRPLANE_MODE_TILE_SPEC to false, + WORK_MODE_TILE_SPEC to false, + HOTSPOT_TILE_SPEC to true, + INTERNET_TILE_SPEC to true, + FLASHLIGHT_TILE_SPEC to false, + ), + ) ) - ) - } + } private val underTest by lazy { kosmos.tilesAvailabilityInteractor } @Test @DisableFlags(FLAG_QS_NEW_TILES) - fun flagOff_usesAvailabilityFromFactoryTiles() = with(kosmos) { - testScope.runTest { - val unavailableTiles = underTest.getUnavailableTiles( - setOf( - AIRPLANE_MODE_TILE_SPEC, - WORK_MODE_TILE_SPEC, - HOTSPOT_TILE_SPEC, - INTERNET_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).map(TileSpec::create) - ) - assertThat(unavailableTiles).isEqualTo(setOf( - AIRPLANE_MODE_TILE_SPEC, - WORK_MODE_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).mapTo(mutableSetOf(), TileSpec::create)) + fun flagOff_usesAvailabilityFromFactoryTiles() = + with(kosmos) { + testScope.runTest { + val unavailableTiles = + underTest.getUnavailableTiles( + setOf( + AIRPLANE_MODE_TILE_SPEC, + WORK_MODE_TILE_SPEC, + HOTSPOT_TILE_SPEC, + INTERNET_TILE_SPEC, + FLASHLIGHT_TILE_SPEC, + ) + .map(TileSpec::create) + ) + assertThat(unavailableTiles) + .isEqualTo( + setOf(AIRPLANE_MODE_TILE_SPEC, WORK_MODE_TILE_SPEC, FLASHLIGHT_TILE_SPEC) + .mapTo(mutableSetOf(), TileSpec::create) + ) + } } - } @Test - fun tileCannotBeCreated_isUnavailable() = with(kosmos) { - testScope.runTest { - val badSpec = TileSpec.create("unknown") - val unavailableTiles = underTest.getUnavailableTiles( - setOf( - badSpec - ) - ) - assertThat(unavailableTiles).contains(badSpec) + fun tileCannotBeCreated_isUnavailable() = + with(kosmos) { + testScope.runTest { + val badSpec = TileSpec.create("unknown") + val unavailableTiles = underTest.getUnavailableTiles(setOf(badSpec)) + assertThat(unavailableTiles).contains(badSpec) + } } - } @Test @EnableFlags(FLAG_QS_NEW_TILES) - fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() = with(kosmos) { - testScope.runTest { - val unavailableTiles = underTest.getUnavailableTiles( - setOf( - AIRPLANE_MODE_TILE_SPEC, - WORK_MODE_TILE_SPEC, - HOTSPOT_TILE_SPEC, - INTERNET_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).map(TileSpec::create) - ) - assertThat(unavailableTiles).isEqualTo(setOf( - HOTSPOT_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).mapTo(mutableSetOf(), TileSpec::create)) + fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() = + with(kosmos) { + testScope.runTest { + val unavailableTiles = + underTest.getUnavailableTiles( + setOf( + AIRPLANE_MODE_TILE_SPEC, + WORK_MODE_TILE_SPEC, + HOTSPOT_TILE_SPEC, + INTERNET_TILE_SPEC, + FLASHLIGHT_TILE_SPEC, + ) + .map(TileSpec::create) + ) + assertThat(unavailableTiles) + .isEqualTo( + setOf(HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC) + .mapTo(mutableSetOf(), TileSpec::create) + ) + } } - } @Test @EnableFlags(FLAG_QS_NEW_TILES) - fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() = with(kosmos) { - testScope.runTest { - fakeUserRepository.asMainUser() - val unavailableTiles = underTest.getUnavailableTiles( - setOf( - AIRPLANE_MODE_TILE_SPEC, - WORK_MODE_TILE_SPEC, - HOTSPOT_TILE_SPEC, - INTERNET_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).map(TileSpec::create) - ) - assertThat(unavailableTiles).isEqualTo(setOf( - WORK_MODE_TILE_SPEC, - HOTSPOT_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ).mapTo(mutableSetOf(), TileSpec::create)) + fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() = + with(kosmos) { + testScope.runTest { + fakeUserRepository.asMainUser() + val unavailableTiles = + underTest.getUnavailableTiles( + setOf( + AIRPLANE_MODE_TILE_SPEC, + WORK_MODE_TILE_SPEC, + HOTSPOT_TILE_SPEC, + INTERNET_TILE_SPEC, + FLASHLIGHT_TILE_SPEC, + ) + .map(TileSpec::create) + ) + assertThat(unavailableTiles) + .isEqualTo( + setOf(WORK_MODE_TILE_SPEC, HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC) + .mapTo(mutableSetOf(), TileSpec::create) + ) + } } - } @Test @EnableFlags(FLAG_QS_NEW_TILES) - fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() = with(kosmos) { - testScope.runTest { - underTest.getUnavailableTiles( + fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() = + with(kosmos) { + testScope.runTest { + underTest.getUnavailableTiles( setOf( AIRPLANE_MODE_TILE_SPEC, WORK_MODE_TILE_SPEC, HOTSPOT_TILE_SPEC, INTERNET_TILE_SPEC, FLASHLIGHT_TILE_SPEC, - ).map(TileSpec::create) - ) - assertThat(createdTiles.map { it.tileSpec }) + ) + .map(TileSpec::create) + ) + assertThat(createdTiles.map { it.tileSpec }) .containsExactly(INTERNET_TILE_SPEC, FLASHLIGHT_TILE_SPEC) - assertThat(createdTiles.all { it.destroyed }).isTrue() + assertThat(createdTiles.all { it.isDestroyed }).isTrue() + } } - } @Test @DisableFlags(FLAG_QS_NEW_TILES) - fun flagOn_TilesAreCreatedAndThenDestroyed() = with(kosmos) { - testScope.runTest { - val allTiles = setOf( - AIRPLANE_MODE_TILE_SPEC, - WORK_MODE_TILE_SPEC, - HOTSPOT_TILE_SPEC, - INTERNET_TILE_SPEC, - FLASHLIGHT_TILE_SPEC, - ) - underTest.getUnavailableTiles(allTiles.map(TileSpec::create)) - assertThat(createdTiles.map { it.tileSpec }) - .containsExactlyElementsIn(allTiles) - assertThat(createdTiles.all { it.destroyed }).isTrue() + fun flagOn_TilesAreCreatedAndThenDestroyed() = + with(kosmos) { + testScope.runTest { + val allTiles = + setOf( + AIRPLANE_MODE_TILE_SPEC, + WORK_MODE_TILE_SPEC, + HOTSPOT_TILE_SPEC, + INTERNET_TILE_SPEC, + FLASHLIGHT_TILE_SPEC, + ) + underTest.getUnavailableTiles(allTiles.map(TileSpec::create)) + assertThat(createdTiles.map { it.tileSpec }).containsExactlyElementsIn(allTiles) + assertThat(createdTiles.all { it.isDestroyed }).isTrue() + } } - } - private fun constantFactory(creatorTiles: Set<FakeQSTile>): QSFactory { return FakeQSFactory { spec -> - creatorTiles.firstOrNull { it.tileSpec == spec }?.also { - createdTiles.add(it) - } + creatorTiles.firstOrNull { it.tileSpec == spec }?.also { createdTiles.add(it) } } } companion object { private fun tilesForCreator( - user: Int, - specAvailabilities: Map<String, Boolean> + user: Int, + specAvailabilities: Map<String, Boolean>, ): Set<FakeQSTile> { return specAvailabilities.mapTo(mutableSetOf()) { - FakeQSTile(user, it.value).apply { - tileSpec = it.key - } + FakeQSTile(user, it.value).apply { tileSpec = it.key } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index 9b50f1bd735d..c3089761effc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -17,10 +17,10 @@ package com.android.systemui.qs.pipeline.domain.interactor import android.content.ComponentName -import android.content.Context import android.content.Intent import android.content.pm.UserInfo import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import android.service.quicksettings.Tile import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,653 +28,702 @@ import com.android.systemui.Flags.FLAG_QS_NEW_TILES import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTile.BooleanState import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.FakeQSFactory import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.external.CustomTile -import com.android.systemui.qs.external.CustomTileStatePersister import com.android.systemui.qs.external.TileLifecycleManager import com.android.systemui.qs.external.TileServiceKey -import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository -import com.android.systemui.qs.pipeline.data.repository.FakeCustomTileAddedRepository -import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository -import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository -import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository -import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.external.customTileStatePersister +import com.android.systemui.qs.external.tileLifecycleManagerFactory +import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository +import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository import com.android.systemui.qs.pipeline.domain.model.TileModel -import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger -import com.android.systemui.qs.tiles.di.NewQSTileFactory +import com.android.systemui.qs.pipeline.shared.logging.qsLogger +import com.android.systemui.qs.qsTileFactory +import com.android.systemui.qs.tiles.di.newQSTileFactory import com.android.systemui.qs.toProto -import com.android.systemui.retail.data.repository.FakeRetailModeRepository -import com.android.systemui.settings.UserTracker -import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.protobuf.nano.MessageNano -import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_QS_NEW_TILES) class CurrentTilesInteractorImplTest : SysuiTestCase() { - private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository() - private val userRepository = FakeUserRepository() - private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository() - private val tileFactory = FakeQSFactory(::tileCreator) - private val customTileAddedRepository: CustomTileAddedRepository = - FakeCustomTileAddedRepository() - private val pipelineFlags = QSPipelineFlagsRepository() - private val tileLifecycleManagerFactory = TLMFactory() - private val minimumTilesRepository = MinimumTilesFixedRepository() - private val retailModeRepository = FakeRetailModeRepository() - - @Mock private lateinit var customTileStatePersister: CustomTileStatePersister - - @Mock private lateinit var userTracker: UserTracker - - @Mock private lateinit var logger: QSPipelineLogger - - @Mock private lateinit var newQSTileFactory: NewQSTileFactory - - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = + testKosmos().apply { + qsTileFactory = FakeQSFactory { tileCreator(it) } + fakeUserTracker.set(listOf(USER_INFO_0), 0) + fakeUserRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) + tileLifecycleManagerFactory = TLMFactory() + newQSTileFactory = mock() + qsLogger = mock() + } private val unavailableTiles = mutableSetOf("e") - private lateinit var underTest: CurrentTilesInteractorImpl - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - mSetFlagsRule.enableFlags(FLAG_QS_NEW_TILES) - - userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) - - setUserTracker(0) - - underTest = - CurrentTilesInteractorImpl( - tileSpecRepository = tileSpecRepository, - installedTilesComponentRepository = installedTilesPackageRepository, - userRepository = userRepository, - minimumTilesRepository = minimumTilesRepository, - retailModeRepository = retailModeRepository, - customTileStatePersister = customTileStatePersister, - tileFactory = tileFactory, - newQSTileFactory = { newQSTileFactory }, - customTileAddedRepository = customTileAddedRepository, - tileLifecycleManagerFactory = tileLifecycleManagerFactory, - userTracker = userTracker, - mainDispatcher = testDispatcher, - backgroundDispatcher = testDispatcher, - scope = testScope.backgroundScope, - logger = logger, - featureFlags = pipelineFlags, - ) - } + private val underTest = kosmos.currentTilesInteractor @Test fun initialState() = - testScope.runTest(USER_INFO_0) { - assertThat(underTest.currentTiles.value).isEmpty() - assertThat(underTest.currentQSTiles).isEmpty() - assertThat(underTest.currentTilesSpecs).isEmpty() - assertThat(underTest.userId.value).isEqualTo(0) - assertThat(underTest.userContext.value.userId).isEqualTo(0) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + assertThat(underTest.currentTiles.value).isEmpty() + assertThat(underTest.currentQSTiles).isEmpty() + assertThat(underTest.currentTilesSpecs).isEmpty() + assertThat(underTest.userId.value).isEqualTo(0) + assertThat(underTest.userContext.value.userId).isEqualTo(0) + } } @Test fun correctTiles() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - - val specs = - listOf( - TileSpec.create("a"), - TileSpec.create("e"), - CUSTOM_TILE_SPEC, - TileSpec.create("d"), - TileSpec.create("non_existent"), - ) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - - // check each tile - - // Tile a - val tile0 = tiles!![0] - assertThat(tile0.spec).isEqualTo(specs[0]) - assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec) - assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java) - assertThat(tile0.tile.isAvailable).isTrue() - - // Tile e is not available and is not in the list - - // Custom Tile - val tile1 = tiles!![1] - assertThat(tile1.spec).isEqualTo(specs[2]) - assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec) - assertThat(tile1.tile).isInstanceOf(CustomTile::class.java) - assertThat(tile1.tile.isAvailable).isTrue() - - // Tile d - val tile2 = tiles!![2] - assertThat(tile2.spec).isEqualTo(specs[3]) - assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec) - assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java) - assertThat(tile2.tile.isAvailable).isTrue() - - // Tile non-existent shouldn't be created. Therefore, only 3 tiles total - assertThat(tiles?.size).isEqualTo(3) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = + listOf( + TileSpec.create("a"), + TileSpec.create("e"), + CUSTOM_TILE_SPEC, + TileSpec.create("d"), + TileSpec.create("non_existent"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + // check each tile + + // Tile a + val tile0 = tiles!![0] + assertThat(tile0.spec).isEqualTo(specs[0]) + assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec) + assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile0.tile.isAvailable).isTrue() + + // Tile e is not available and is not in the list + + // Custom Tile + val tile1 = tiles!![1] + assertThat(tile1.spec).isEqualTo(specs[2]) + assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec) + assertThat(tile1.tile).isInstanceOf(CustomTile::class.java) + assertThat(tile1.tile.isAvailable).isTrue() + + // Tile d + val tile2 = tiles!![2] + assertThat(tile2.spec).isEqualTo(specs[3]) + assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec) + assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile2.tile.isAvailable).isTrue() + + // Tile non-existent shouldn't be created. Therefore, only 3 tiles total + assertThat(tiles?.size).isEqualTo(3) + } } @Test fun logTileCreated() = - testScope.runTest(USER_INFO_0) { - val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() - specs.forEach { verify(logger).logTileCreated(it) } + specs.forEach { verify(qsLogger).logTileCreated(it) } + } } @Test fun logTileNotFoundInFactory() = - testScope.runTest(USER_INFO_0) { - val specs = listOf(TileSpec.create("non_existing")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() - - verify(logger, never()).logTileCreated(any()) - verify(logger).logTileNotFoundInFactory(specs[0]) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val specs = listOf(TileSpec.create("non_existing")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(qsLogger, never()).logTileCreated(any()) + verify(qsLogger).logTileNotFoundInFactory(specs[0]) + } } @Test fun tileNotAvailableDestroyed_logged() = - testScope.runTest(USER_INFO_0) { - val specs = listOf(TileSpec.create("e")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() - - verify(logger, never()).logTileCreated(any()) - verify(logger) - .logTileDestroyed( - specs[0], - QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE, - ) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val specs = listOf(TileSpec.create("e")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(qsLogger, never()).logTileCreated(any()) + verify(qsLogger) + .logTileDestroyed( + specs[0], + QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE, + ) + } } @Test fun someTilesNotValid_repositorySetToDefinitiveList() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val specs = listOf(TileSpec.create("a"), TileSpec.create("e")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val specs = listOf(TileSpec.create("a"), TileSpec.create("e")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + } } @Test fun deduplicatedTiles() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), TileSpec.create("a")) + val specs = listOf(TileSpec.create("a"), TileSpec.create("a")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles?.size).isEqualTo(1) - assertThat(tiles!![0].spec).isEqualTo(specs[0]) + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(specs[0]) + } } @Test fun tilesChange_platformTileNotRecreated() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a")) + val specs = listOf(TileSpec.create("a")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - val originalTileA = tiles!![0].tile + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile - tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) - assertThat(tiles?.size).isEqualTo(2) - assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + } } @Test fun tileRemovedIsDestroyed() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), TileSpec.create("c")) + val specs = listOf(TileSpec.create("a"), TileSpec.create("c")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - val originalTileC = tiles!![1].tile + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileC = tiles!![1].tile - tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c"))) + tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c"))) - assertThat(tiles?.size).isEqualTo(1) - assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a")) + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a")) - assertThat((originalTileC as FakeQSTile).destroyed).isTrue() - verify(logger) - .logTileDestroyed( - TileSpec.create("c"), - QSPipelineLogger.TileDestroyedReason.TILE_REMOVED, - ) + assertThat(originalTileC.isDestroyed).isTrue() + verify(qsLogger) + .logTileDestroyed( + TileSpec.create("c"), + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED, + ) + } } @Test fun tileBecomesNotAvailable_destroyed() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - - val specs = listOf(TileSpec.create("a")) - - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - val originalTileA = tiles!![0].tile - - // Tile becomes unavailable - (originalTileA as FakeQSTile).available = false - unavailableTiles.add("a") - // and there is some change in the specs - tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) - runCurrent() - - assertThat(originalTileA.destroyed).isTrue() - verify(logger) - .logTileDestroyed( - TileSpec.create("a"), - QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE, - ) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile + + // Tile becomes unavailable + (originalTileA as FakeQSTile).available = false + unavailableTiles.add("a") + // and there is some change in the specs + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + runCurrent() + + assertThat(originalTileA.isDestroyed).isTrue() + verify(qsLogger) + .logTileDestroyed( + TileSpec.create("a"), + QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE, + ) - assertThat(tiles?.size).isEqualTo(1) - assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b")) - assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA) + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b")) + assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA) - assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec)) + assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec)) + } } @Test fun userChange_tilesChange() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs0 = listOf(TileSpec.create("a")) - val specs1 = listOf(TileSpec.create("b")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs0) - tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) - switchUser(USER_INFO_1) + switchUser(USER_INFO_1) - assertThat(tiles!![0].spec).isEqualTo(specs1[0]) - assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec) + assertThat(tiles!![0].spec).isEqualTo(specs1[0]) + assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec) + } } @Test fun tileNotPresentInSecondaryUser_destroyedInUserChange() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs0 = listOf(TileSpec.create("a")) - val specs1 = listOf(TileSpec.create("b")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs0) - tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) - val originalTileA = tiles!![0].tile + val originalTileA = tiles!![0].tile - switchUser(USER_INFO_1) - runCurrent() + switchUser(USER_INFO_1) + runCurrent() - assertThat((originalTileA as FakeQSTile).destroyed).isTrue() - verify(logger) - .logTileDestroyed( - specs0[0], - QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER, - ) + assertThat(originalTileA.isDestroyed).isTrue() + verify(qsLogger) + .logTileDestroyed( + specs0[0], + QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER, + ) + } } @Test - fun userChange_customTileDestroyed_lifecycleNotTerminated() { - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + fun userChange_customTileDestroyed_lifecycleNotTerminated() = + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(CUSTOM_TILE_SPEC) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - tileSpecRepository.setTiles(USER_INFO_1.id, specs) + val specs = listOf(CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) - val originalCustomTile = tiles!![0].tile + val originalCustomTile = tiles!![0].tile - switchUser(USER_INFO_1) - runCurrent() + switchUser(USER_INFO_1) + runCurrent() - verify(originalCustomTile).destroy() - assertThat(tileLifecycleManagerFactory.created).isEmpty() + verify(originalCustomTile).destroy() + assertThat((tileLifecycleManagerFactory as TLMFactory).created).isEmpty() + } } - } @Test fun userChange_sameTileUserChanged() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - tileSpecRepository.setTiles(USER_INFO_1.id, specs) + val specs = listOf(TileSpec.create("a")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) - val originalTileA = tiles!![0].tile as FakeQSTile - assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id) + val originalTileA = tiles!![0].tile as FakeQSTile + assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id) - switchUser(USER_INFO_1) - runCurrent() + switchUser(USER_INFO_1) + runCurrent() - assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) - assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id) - verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id) + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id) + verify(qsLogger).logTileUserChanged(specs[0], USER_INFO_1.id) + } } @Test fun addTile() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val spec = TileSpec.create("a") - val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c")) - tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val spec = TileSpec.create("a") + val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) - underTest.addTile(spec, position = 1) + underTest.addTile(spec, position = 1) - val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c")) - assertThat(tiles).isEqualTo(expectedSpecs) + val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c")) + assertThat(tiles).isEqualTo(expectedSpecs) + } } @Test fun addTile_currentUser() = - testScope.runTest(USER_INFO_1) { - val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) - val spec = TileSpec.create("a") - val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c")) - tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) - tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) - - switchUser(USER_INFO_1) - underTest.addTile(spec, position = 1) - - assertThat(tiles0).isEqualTo(currentSpecs) - - val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c")) - assertThat(tiles1).isEqualTo(expectedSpecs) + with(kosmos) { + testScope.runTest(USER_INFO_1) { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val spec = TileSpec.create("a") + val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + underTest.addTile(spec, position = 1) + + assertThat(tiles0).isEqualTo(currentSpecs) + + val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c")) + assertThat(tiles1).isEqualTo(expectedSpecs) + } } @Test fun removeTile_platform() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() + val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() - underTest.removeTiles(specs.subList(0, 1)) + underTest.removeTiles(specs.subList(0, 1)) - assertThat(tiles).isEqualTo(specs.subList(1, 2)) + assertThat(tiles).isEqualTo(specs.subList(1, 2)) + } } @Test - fun removeTile_customTile_lifecycleEnded() { - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - - val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() - assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) - .isTrue() - - underTest.removeTiles(listOf(CUSTOM_TILE_SPEC)) - - assertThat(tiles).isEqualTo(specs.subList(0, 1)) - - val tileLifecycleManager = - tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT] - assertThat(tileLifecycleManager).isNotNull() - - with(inOrder(tileLifecycleManager!!)) { - verify(tileLifecycleManager).onStopListening() - verify(tileLifecycleManager).onTileRemoved() - verify(tileLifecycleManager).flushMessagesAndUnbind() + fun removeTile_customTile_lifecycleEnded() = + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isTrue() + + underTest.removeTiles(listOf(CUSTOM_TILE_SPEC)) + + assertThat(tiles).isEqualTo(specs.subList(0, 1)) + + val tileLifecycleManager = + (tileLifecycleManagerFactory as TLMFactory) + .created[USER_INFO_0.id to TEST_COMPONENT] + assertThat(tileLifecycleManager).isNotNull() + + with(inOrder(tileLifecycleManager!!)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + assertThat( + customTileStatePersister.readState( + TileServiceKey(TEST_COMPONENT, USER_INFO_0.id) + ) + ) + .isNull() } - assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) - .isFalse() - verify(customTileStatePersister) - .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) } - } @Test fun removeTiles_currentUser() = - testScope.runTest { - val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) - val currentSpecs = - listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c")) - tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) - tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) - - switchUser(USER_INFO_1) - runCurrent() - - underTest.removeTiles(currentSpecs.subList(0, 2)) - - assertThat(tiles0).isEqualTo(currentSpecs) - assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3)) + with(kosmos) { + testScope.runTest { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val currentSpecs = + listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + runCurrent() + + underTest.removeTiles(currentSpecs.subList(0, 2)) + + assertThat(tiles0).isEqualTo(currentSpecs) + assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3)) + } } @Test fun setTiles() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) - val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b")) - tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) - runCurrent() + val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() - val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a")) - underTest.setTiles(newSpecs) - runCurrent() + val newSpecs = + listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a")) + underTest.setTiles(newSpecs) + runCurrent() - assertThat(tiles).isEqualTo(newSpecs) + assertThat(tiles).isEqualTo(newSpecs) + } } @Test fun setTiles_customTiles_lifecycleEndedIfGone() = - testScope.runTest(USER_INFO_0) { - val otherCustomTileSpec = TileSpec.create("custom(b/c)") + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val otherCustomTileSpec = TileSpec.create("custom(b/c)") - val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec) - tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) - runCurrent() + val currentSpecs = + listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() - val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a")) + val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a")) - underTest.setTiles(newSpecs) - runCurrent() + underTest.setTiles(newSpecs) + runCurrent() - val tileLifecycleManager = - tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!! + val tileLifecycleManager = + (tileLifecycleManagerFactory as TLMFactory) + .created[USER_INFO_0.id to TEST_COMPONENT]!! - with(inOrder(tileLifecycleManager)) { - verify(tileLifecycleManager).onStopListening() - verify(tileLifecycleManager).onTileRemoved() - verify(tileLifecycleManager).flushMessagesAndUnbind() + with(inOrder(tileLifecycleManager)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + assertThat( + customTileStatePersister.readState( + TileServiceKey(TEST_COMPONENT, USER_INFO_0.id) + ) + ) + .isNull() } - assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) - .isFalse() - verify(customTileStatePersister) - .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) } @Test fun protoDump() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) - - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - - val stateA = tiles!![0].tile.state - stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA") - val stateCustom = QSTile.BooleanState() - stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB") - stateCustom.spec = CUSTOM_TILE_SPEC.spec - whenever(tiles!![1].tile.state).thenReturn(stateCustom) - - val proto = SystemUIProtoDump() - underTest.dumpProto(proto, emptyArray()) - - assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue() - assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto())) - .isTrue() + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + val stateA = tiles!![0].tile.state + stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA") + val stateCustom = QSTile.BooleanState() + stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB") + stateCustom.spec = CUSTOM_TILE_SPEC.spec + whenever(tiles!![1].tile.state).thenReturn(stateCustom) + + val proto = SystemUIProtoDump() + underTest.dumpProto(proto, emptyArray()) + + assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue() + assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto())) + .isTrue() + } } @Test fun retainedTiles_callbackNotRemoved() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a"))) - - val tileA = tiles!![0].tile - val callback = mock<QSTile.Callback>() - tileA.addCallback(callback) - - tileSpecRepository.setTiles( - USER_INFO_0.id, - listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC), - ) - val newTileA = tiles!![0].tile - assertThat(tileA).isSameInstanceAs(newTileA) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a"))) + + val tileA = tiles!![0].tile + val callback = mock<QSTile.Callback>() + tileA.addCallback(callback) + + tileSpecRepository.setTiles( + USER_INFO_0.id, + listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC), + ) + val newTileA = tiles!![0].tile + assertThat(tileA).isSameInstanceAs(newTileA) - assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback) + assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback) + } } @Test fun packageNotInstalled_customTileNotVisible() = - testScope.runTest(USER_INFO_0) { - installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) - val tiles by collectLastValue(underTest.currentTiles) + val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles!!.size).isEqualTo(1) - assertThat(tiles!![0].spec).isEqualTo(specs[0]) + assertThat(tiles!!.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(specs[0]) + } } @Test fun packageInstalledLater_customTileAdded() = - testScope.runTest(USER_INFO_0) { - installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) - val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles!!.size).isEqualTo(2) + assertThat(tiles!!.size).isEqualTo(2) - installedTilesPackageRepository.setInstalledPackagesForUser( - USER_INFO_0.id, - setOf(TEST_COMPONENT), - ) + fakeInstalledTilesRepository.setInstalledPackagesForUser( + USER_INFO_0.id, + setOf(TEST_COMPONENT), + ) - assertThat(tiles!!.size).isEqualTo(3) - assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC) + assertThat(tiles!!.size).isEqualTo(3) + assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC) + } } @Test fun tileAddedOnEmptyList_blocked() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) - val newTile = TileSpec.create("c") + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) + val newTile = TileSpec.create("c") - underTest.addTile(newTile) + underTest.addTile(newTile) - assertThat(tiles!!.isEmpty()).isTrue() + assertThat(tiles!!.isEmpty()).isTrue() - tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles!!.size).isEqualTo(3) + assertThat(tiles!!.size).isEqualTo(3) + } } @Test fun changeInPackagesTiles_doesntTriggerUserChange_logged() = - testScope.runTest(USER_INFO_0) { - val specs = listOf(TileSpec.create("a")) - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - runCurrent() - // Settled on the same list of tiles. - assertThat(underTest.currentTilesSpecs).isEqualTo(specs) - - installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) - runCurrent() - - verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0) + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val specs = listOf(TileSpec.create("a")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + // Settled on the same list of tiles. + assertThat(underTest.currentTilesSpecs).isEqualTo(specs) + + fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet()) + runCurrent() + + verify(qsLogger, never()).logTileUserChanged(TileSpec.create("a"), 0) + } } @Test fun getTileDetails() = - testScope.runTest(USER_INFO_0) { - val tiles by collectLastValue(underTest.currentTiles) - val tileA = TileSpec.create("a") - val tileB = TileSpec.create("b") - val tileNoDetails = TileSpec.create("NoDetails") + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val tileA = TileSpec.create("a") + val tileB = TileSpec.create("b") + val tileNoDetails = TileSpec.create("NoDetails") + + val specs = listOf(tileA, tileB, tileNoDetails) + + assertThat(tiles!!.isEmpty()).isTrue() + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + assertThat(tiles!!.size).isEqualTo(3) + + // The third tile doesn't have a details view. + assertThat(tiles!![2].spec).isEqualTo(tileNoDetails) + (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false - val specs = listOf(tileA, tileB, tileNoDetails) + var currentModel: TileDetailsViewModel? = null + val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model } + tiles!![0].tile.getDetailsViewModel(setCurrentModel) + assertThat(currentModel?.getTitle()).isEqualTo("a") - assertThat(tiles!!.isEmpty()).isTrue() + currentModel = null + tiles!![1].tile.getDetailsViewModel(setCurrentModel) + assertThat(currentModel?.getTitle()).isEqualTo("b") - tileSpecRepository.setTiles(USER_INFO_0.id, specs) - assertThat(tiles!!.size).isEqualTo(3) + currentModel = null + tiles!![2].tile.getDetailsViewModel(setCurrentModel) + assertThat(currentModel).isNull() + } + } + + @Test + fun destroyedTilesNotReused() = + with(kosmos) { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) + val newTile = TileSpec.create("c") + + underTest.setTiles(specs) - // The third tile doesn't have a details view. - assertThat(tiles!![2].spec).isEqualTo(tileNoDetails) - (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false + val tileABefore = tiles!!.first { it.spec == specs[0] }.tile - var currentModel: TileDetailsViewModel? = null - val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model } - tiles!![0].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("a") + // We destroy it manually, in prod, this could happen if the tile processing action + // is interrupted in the middle. + tileABefore.destroy() - currentModel = null - tiles!![1].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("b") + underTest.addTile(newTile) - currentModel = null - tiles!![2].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel).isNull() + val tileAAfter = tiles!!.first { it.spec == specs[0] }.tile + assertThat(tileAAfter).isNotSameInstanceAs(tileABefore) + } } private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) { @@ -686,20 +735,21 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { } } - private fun tileCreator(spec: String): QSTile? { - val currentUser = userTracker.userId + private fun Kosmos.tileCreator(spec: String): QSTile? { + val currentUser = userRepository.getSelectedUserInfo().id return when (spec) { CUSTOM_TILE_SPEC.spec -> mock<CustomTile> { var tileSpecReference: String? = null - whenever(user).thenReturn(currentUser) - whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName) - whenever(isAvailable).thenReturn(true) - whenever(setTileSpec(anyString())).thenAnswer { - tileSpecReference = it.arguments[0] as? String - Unit - } - whenever(tileSpec).thenAnswer { tileSpecReference } + on { user } doReturn currentUser + on { component } doReturn CUSTOM_TILE_SPEC.componentName + on { isAvailable } doReturn true + on { setTileSpec(anyString()) } + .thenAnswer { + tileSpecReference = it.arguments[0] as? String + Unit + } + on { tileSpec }.thenAnswer { tileSpecReference } // Also, add it to the set of added tiles (as this happens as part of the tile // creation). customTileAddedRepository.setTileAdded( @@ -714,22 +764,16 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { } private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) { - return runTest { + return kosmos.runTest { switchUser(user) body() } } - private suspend fun switchUser(user: UserInfo) { - setUserTracker(user.id) - installedTilesPackageRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT)) - userRepository.setSelectedUserInfo(user) - } - - private fun setUserTracker(user: Int) { - val mockContext = mockUserContext(user) - whenever(userTracker.userContext).thenReturn(mockContext) - whenever(userTracker.userId).thenReturn(user) + private suspend fun Kosmos.switchUser(user: UserInfo) { + fakeUserTracker.set(listOf(user), 0) + fakeInstalledTilesRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT)) + fakeUserRepository.setSelectedUserInfo(user) } private class TLMFactory : TileLifecycleManager.Factory { @@ -745,13 +789,6 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { } } - private fun mockUserContext(user: Int): Context { - return mock { - whenever(this.userId).thenReturn(user) - whenever(this.user).thenReturn(UserHandle.of(user)) - } - } - companion object { private val USER_INFO_0 = UserInfo().apply { id = 0 } private val USER_INFO_1 = UserInfo().apply { id = 1 } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index 296478be77e0..1d8c6ccc75d1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -508,6 +508,12 @@ public class QSTileImplTest extends SysuiTestCase { assertThat(mTile.mRefreshes).isEqualTo(1); } + @Test + public void testIsDestroyedImmediately() { + mTile.destroy(); + assertThat(mTile.isDestroyed()).isTrue(); + } + private void assertEvent(UiEventLogger.UiEventEnum eventType, UiEventLoggerFake.FakeUiEvent fakeEvent) { assertEquals(eventType.getId(), fakeEvent.eventId); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt index fba615121a39..da3cebd24e6b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.FakeTileDetailsViewModel import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor @@ -97,6 +98,7 @@ class QSTileViewModelImplTest : SysuiTestCase() { testCoroutineDispatcher, testCoroutineDispatcher, testScope.backgroundScope, + FakeTileDetailsViewModel("QSTileViewModelImplTest"), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt index 3db5efcb6eb8..261e3de939f2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt @@ -26,8 +26,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx -import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager -import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.statusbar.connectivity.AccessPointController @@ -39,11 +37,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.eq -import org.mockito.kotlin.any import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @SmallTest @EnabledOnRavenwood @@ -56,31 +51,17 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { private lateinit var internetDialogManager: InternetDialogManager private lateinit var controller: AccessPointController - private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory - private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory - private lateinit var internetDetailsViewModel: InternetDetailsViewModel @Before fun setup() { internetDialogManager = mock<InternetDialogManager>() controller = mock<AccessPointController>() - internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>() - internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>() - internetDetailsViewModel = - InternetDetailsViewModel( - onLongClick = {}, - accessPointController = mock<AccessPointController>(), - contentManagerFactory = internetDetailsContentManagerFactory, - ) - whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel) - underTest = InternetTileUserActionInteractor( kosmos.testScope.coroutineContext, internetDialogManager, controller, inputHandler, - internetDetailsViewModelFactory, ) } @@ -127,12 +108,4 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS) } } - - @Test - fun detailsViewModel() = - kosmos.testScope.runTest { - assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet") - assertThat(underTest.detailsViewModel.getSubTitle()) - .isEqualTo("Tab a network to connect") - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt index 0598a8b9d058..4e9b63517d6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.FakeTileDetailsViewModel import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor @@ -171,21 +172,6 @@ class QSTileViewModelTest : SysuiTestCase() { .isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER)) } - @Test - fun tileDetails() = - testScope.runTest { - assertThat(tileUserActionInteractor.detailsViewModel).isNotNull() - assertThat(tileUserActionInteractor.detailsViewModel?.getTitle()) - .isEqualTo("FakeQSTileUserActionInteractor") - assertThat(underTest.detailsViewModel).isNotNull() - assertThat(underTest.detailsViewModel?.getTitle()) - .isEqualTo("FakeQSTileUserActionInteractor") - - tileUserActionInteractor.detailsViewModel = null - assertThat(tileUserActionInteractor.detailsViewModel).isNull() - assertThat(underTest.detailsViewModel).isNull() - } - private fun createViewModel( scope: TestScope, config: QSTileConfig = tileConfig, @@ -209,6 +195,7 @@ class QSTileViewModelTest : SysuiTestCase() { testCoroutineDispatcher, testCoroutineDispatcher, scope.backgroundScope, + FakeTileDetailsViewModel("QSTileViewModelTest"), ) private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt index ece21e1bef66..166e9500cff9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.FakeTileDetailsViewModel import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor @@ -253,5 +254,6 @@ class QSTileViewModelUserInputTest : SysuiTestCase() { testCoroutineDispatcher, testCoroutineDispatcher, scope.backgroundScope, + FakeTileDetailsViewModel("QSTileViewModelUserInputTest"), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt index c69ebab7a170..baf0aeb701d3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -61,8 +61,7 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { usingMediaInComposeFragment = false // This is not for the compose fragment } private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 559e363d8937..d3f592357b14 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -73,9 +73,8 @@ class SceneInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val fakeSceneDataSource = kosmos.fakeSceneDataSource - - private val underTest = kosmos.sceneInteractor + private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } + private val underTest by lazy { kosmos.sceneInteractor } @Before fun setUp() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index 4a011c0844e5..ccc876c20623 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -50,11 +50,10 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val configurationRepository = kosmos.fakeConfigurationRepository - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val sceneInteractor = kosmos.sceneInteractor + private val configurationRepository by lazy { kosmos.fakeConfigurationRepository } + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val shadeTestUtil by lazy { kosmos.shadeTestUtil } - private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 37b4688f753d..a832f486ef32 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -15,7 +15,9 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -51,12 +53,11 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @EnableSceneContainer class ShadeHeaderViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope - private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor - private val sceneInteractor = kosmos.sceneInteractor - private val deviceEntryInteractor = kosmos.deviceEntryInteractor - + private val mobileIconsInteractor by lazy { kosmos.fakeMobileIconsInteractor } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } private val underTest by lazy { kosmos.shadeHeaderViewModel } @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index 3d8da6140ff7..70df82d95008 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -22,6 +22,7 @@ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYSUI_CALLBACKS; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -556,9 +557,9 @@ public class CommandQueueTest extends SysuiTestCase { @Test public void testImmersiveModeChanged() { final int displayAreaId = 10; - mCommandQueue.immersiveModeChanged(displayAreaId, true); + mCommandQueue.immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION); waitForIdleSync(); - verify(mCallbacks).immersiveModeChanged(displayAreaId, true); + verify(mCallbacks).immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION); } @Test diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt index 6e4dc1485c7b..0cbc30d399d0 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt @@ -34,6 +34,9 @@ constructor( /** Font axes that can be modified on this clock */ val axes: List<ClockFontAxis> = listOf(), + + /** List of font presets for this clock. Can be assigned directly. */ + val axisPresets: List<List<ClockFontAxisSetting>> = listOf(), ) /** Represents an Axis that can be modified */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java index 56176cfc0f91..d197cdb792e4 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -42,7 +42,7 @@ import java.util.function.Supplier; @DependsOn(target = Icon.class) @DependsOn(target = State.class) public interface QSTile { - int VERSION = 4; + int VERSION = 5; String getTileSpec(); @@ -78,6 +78,7 @@ public interface QSTile { void longClick(@Nullable Expandable expandable); void userSwitch(int currentUser); + int getCurrentTileUser(); /** * @deprecated not needed as {@link com.android.internal.logging.UiEvent} will use @@ -150,6 +151,8 @@ public interface QSTile { return null; } + boolean isDestroyed(); + @ProvidesInterface(version = Callback.VERSION) interface Callback { static final int VERSION = 2; diff --git a/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml new file mode 100644 index 000000000000..1de8c2b6c35d --- /dev/null +++ b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + <solid + android:color="@color/notification_guts_priority_button_bg_fill" /> + + <stroke + android:width="1.5dp" + android:color="@color/notification_guts_priority_button_bg_stroke" /> + + <corners android:radius="16dp" /> +</shape> diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml new file mode 100644 index 000000000000..7b6916652924 --- /dev/null +++ b/packages/SystemUI/res/layout/notification_2025_info.xml @@ -0,0 +1,365 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<!-- extends LinearLayout --> +<com.android.systemui.statusbar.notification.row.NotificationInfo + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/notification_guts" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true" + android:clipChildren="false" + android:clipToPadding="true" + android:orientation="vertical" + android:paddingStart="@*android:dimen/notification_2025_margin"> + + <!-- Package Info --> + <LinearLayout + android:id="@+id/header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="true"> + <ImageView + android:id="@+id/pkg_icon" + android:layout_width="@*android:dimen/notification_2025_icon_circle_size" + android:layout_height="@*android:dimen/notification_2025_icon_circle_size" + android:layout_marginTop="@*android:dimen/notification_2025_margin" + android:layout_marginEnd="@*android:dimen/notification_2025_margin" /> + <LinearLayout + android:id="@+id/names" + android:layout_weight="1" + android:layout_width="0dp" + android:orientation="vertical" + android:layout_height="wrap_content" + android:layout_marginTop="@*android:dimen/notification_2025_margin" + android:minHeight="@*android:dimen/notification_2025_icon_circle_size"> + <TextView + android:id="@+id/channel_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + style="@style/TextAppearance.NotificationImportanceChannel"/> + <TextView + android:id="@+id/group_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + android:ellipsize="end" + style="@style/TextAppearance.NotificationImportanceChannelGroup"/> + <TextView + android:id="@+id/pkg_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/TextAppearance.NotificationImportanceApp" + android:ellipsize="end" + android:textDirection="locale" + android:maxLines="1"/> + <TextView + android:id="@+id/delegate_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/TextAppearance.NotificationImportanceHeader" + android:ellipsize="end" + android:textDirection="locale" + android:text="@string/notification_delegate_header" + android:maxLines="1" /> + + </LinearLayout> + + <!-- feedback for notificationassistantservice --> + <ImageButton + android:id="@+id/feedback" + android:layout_width="@dimen/notification_2025_guts_button_size" + android:layout_height="@dimen/notification_2025_guts_button_size" + android:visibility="gone" + android:background="@drawable/ripple_drawable" + android:contentDescription="@string/notification_guts_bundle_feedback" + android:src="@*android:drawable/ic_feedback" + android:paddingTop="@*android:dimen/notification_2025_margin" + android:tint="@androidprv:color/materialColorPrimary"/> + + <!-- Optional link to app. Only appears if the channel is not disabled and the app + asked for it --> + <ImageButton + android:id="@+id/app_settings" + android:layout_width="@dimen/notification_2025_guts_button_size" + android:layout_height="@dimen/notification_2025_guts_button_size" + android:visibility="gone" + android:background="@drawable/ripple_drawable" + android:contentDescription="@string/notification_app_settings" + android:src="@drawable/ic_info" + android:paddingTop="@*android:dimen/notification_2025_margin" + android:tint="@androidprv:color/materialColorPrimary"/> + + <!-- System notification settings --> + <ImageButton + android:id="@+id/info" + android:layout_width="@dimen/notification_2025_guts_button_size" + android:layout_height="@dimen/notification_2025_guts_button_size" + android:contentDescription="@string/notification_more_settings" + android:background="@drawable/ripple_drawable" + android:src="@drawable/ic_settings" + android:padding="@*android:dimen/notification_2025_margin" + android:tint="@androidprv:color/materialColorPrimary" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/inline_controls" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="@*android:dimen/notification_2025_margin" + android:layout_marginTop="@*android:dimen/notification_2025_margin" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="vertical"> + + <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_text" + android:text="@string/notification_unblockable_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_call_text" + android:text="@string/notification_unblockable_call_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_multichannel_text" + android:text="@string/notification_multichannel_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <LinearLayout + android:id="@+id/interruptiveness_settings" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical"> + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/automatic" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical" + android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal" + android:gravity="center_vertical" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_2025_guts_priority_button_bg" + android:orientation="horizontal" + android:visibility="gone"> + <ImageView + android:id="@+id/automatic_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="@*android:dimen/notification_2025_margin" + android:src="@drawable/ic_notifications_automatic" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:clickable="false" + android:focusable="false"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + > + <TextView + android:id="@+id/automatic_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_automatic_title"/> + <TextView + android:id="@+id/automatic_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_description_top_margin" + android:visibility="gone" + android:text="@string/notification_channel_summary_automatic" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </LinearLayout> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/alert" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical" + android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal" + android:gravity="center_vertical" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_2025_guts_priority_button_bg" + android:orientation="horizontal"> + <ImageView + android:id="@+id/alert_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="@*android:dimen/notification_2025_margin" + android:src="@drawable/ic_notifications_alert" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:clickable="false" + android:focusable="false"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + > + <TextView + android:id="@+id/alert_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_alert_title"/> + <TextView + android:id="@+id/alert_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone" + android:text="@string/notification_channel_summary_default" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </LinearLayout> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/silence" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_separation" + android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical" + android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal" + android:gravity="center_vertical" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_2025_guts_priority_button_bg" + android:orientation="horizontal"> + <ImageView + android:id="@+id/silence_icon" + android:src="@drawable/ic_notifications_silence" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="@*android:dimen/notification_2025_margin" + android:clickable="false" + android:focusable="false"/> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + > + <TextView + android:id="@+id/silence_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:layout_toEndOf="@id/silence_icon" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_silence_title"/> + <TextView + android:id="@+id/silence_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone" + android:text="@string/notification_channel_summary_low" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </LinearLayout> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + </LinearLayout> + + <LinearLayout + android:id="@+id/bottom_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@*android:dimen/notification_2025_margin" + android:minHeight="@dimen/notification_2025_guts_button_size" + android:gravity="center_vertical" + > + <TextView + android:id="@+id/turn_off_notifications" + android:text="@string/inline_turn_off_notifications" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="32dp" + android:paddingTop="8dp" + android:paddingBottom="@*android:dimen/notification_2025_margin" + android:gravity="center" + android:minWidth="@dimen/notification_2025_min_tap_target_size" + android:minHeight="@dimen/notification_2025_min_tap_target_size" + android:maxWidth="200dp" + style="@style/TextAppearance.NotificationInfo.Button" + android:textSize="@*android:dimen/notification_2025_action_text_size"/> + <TextView + android:id="@+id/done" + android:text="@string/inline_ok_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingBottom="@*android:dimen/notification_2025_margin" + android:gravity="center" + android:minWidth="@dimen/notification_2025_min_tap_target_size" + android:minHeight="@dimen/notification_2025_min_tap_target_size" + android:maxWidth="125dp" + style="@style/TextAppearance.NotificationInfo.Button" + android:textSize="@*android:dimen/notification_2025_action_text_size"/> + </LinearLayout> + </LinearLayout> +</com.android.systemui.statusbar.notification.row.NotificationInfo> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index b273886e286e..4995858f95a4 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1125,4 +1125,7 @@ <!-- Configuration to swipe to open glanceable hub --> <bool name="config_swipeToOpenGlanceableHub">false</bool> + + <!-- Whether or not to show the UMO on the glanceable hub when media is playing. --> + <bool name="config_showUmoOnHub">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 640e1fa79530..7c370d3bc064 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -390,6 +390,12 @@ <!-- Extra space for guts bundle feedback button --> <dimen name="notification_guts_bundle_feedback_size">48dp</dimen> + <!-- Size of icon buttons in notification info. --> + <!-- 24dp for the icon itself + 16dp * 2 for top and bottom padding --> + <dimen name="notification_2025_guts_button_size">56dp</dimen> + + <dimen name="notification_2025_min_tap_target_size">48dp</dimen> + <dimen name="notification_importance_toggle_size">48dp</dimen> <dimen name="notification_importance_button_separation">8dp</dimen> <dimen name="notification_importance_drawable_padding">8dp</dimen> @@ -402,6 +408,10 @@ <dimen name="notification_importance_button_description_top_margin">12dp</dimen> <dimen name="rect_button_radius">8dp</dimen> + <!-- Padding for importance selection buttons in notification info, 2025 redesign version --> + <dimen name="notification_2025_importance_button_padding_vertical">12dp</dimen> + <dimen name="notification_2025_importance_button_padding_horizontal">16dp</dimen> + <!-- The minimum height for the snackbar shown after the snooze option has been chosen. --> <dimen name="snooze_snackbar_min_height">56dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 084495f4b196..6ff1240c5e60 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3176,8 +3176,8 @@ <string name="controls_media_settings_button">Settings</string> <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]--> <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string> - <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] --> - <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string> + <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] --> + <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string> <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] --> <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string> @@ -4178,4 +4178,7 @@ <string name="qs_edit_mode_reset_dialog_content"> All Quick Settings tiles will reset to the device’s original settings </string> + + <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] --> + <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string> </resources> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java index 4a4cb7a232c5..8f8bcf273af1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -239,7 +239,6 @@ public class KeyguardPatternView extends KeyguardInputView R.dimen.keyguard_pattern_activated_dot_size)); mLockPatternView.setPathWidth( getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width)); - mLockPatternView.setKeepDotActivated(true); } mEcaView = findViewById(R.id.keyguard_selector_fade_container); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 7fb66640b29f..f6df42575bbd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -36,6 +36,7 @@ import com.android.internal.widget.LockPatternView.Cell; import com.android.internal.widget.LockscreenCredential; import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.systemui.Flags; import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; @@ -237,8 +238,12 @@ public class KeyguardPatternViewController super.onViewAttached(); mLockPatternView.setOnPatternListener(new UnlockPatternListener()); mLockPatternView.setSaveEnabled(false); - mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( - mSelectedUserInteractor.getSelectedUserId())); + boolean visiblePatternEnabled = mLockPatternUtils.isVisiblePatternEnabled( + mSelectedUserInteractor.getSelectedUserId()); + mLockPatternView.setInStealthMode(!visiblePatternEnabled); + if (Flags.bouncerUiRevamp2()) { + mLockPatternView.setKeepDotActivated(visiblePatternEnabled); + } mLockPatternView.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mFalsingCollector.avoidGesture(); diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt index 208adc22a3e0..5f7dca8d649a 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt @@ -64,15 +64,14 @@ constructor( synchronized(sessions) { sessions.putAll( packageInstaller.allSessions - .filter { !TextUtils.isEmpty(it.appPackageName) } - .map { session -> session.toModel() } + .mapNotNull { session -> session.toModel() } .associateBy { it.sessionId } ) updateInstallerSessionsFlow() } packageInstaller.registerSessionCallback( this@PackageInstallerMonitor, - bgHandler + bgHandler, ) } else { synchronized(sessions) { @@ -130,7 +129,7 @@ constructor( if (session == null) { sessions.remove(sessionId) } else { - sessions[sessionId] = session.toModel() + session.toModel()?.apply { sessions[sessionId] = this } } updateInstallerSessionsFlow() } @@ -144,7 +143,11 @@ constructor( companion object { const val TAG = "PackageInstallerMonitor" - private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession { + private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession? { + if (TextUtils.isEmpty(this.appPackageName)) { + return null + } + return PackageInstallSession( sessionId = this.sessionId, packageName = this.appPackageName, diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt index 5e8c21f9abf5..4451f07318ef 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt @@ -16,23 +16,27 @@ package com.android.systemui.common.shared.colors -import android.content.res.Resources +import android.content.Context object SurfaceEffectColors { @JvmStatic - fun surfaceEffect0(r: Resources): Int { - return r.getColor(com.android.internal.R.color.surface_effect_0) + fun surfaceEffect0(context: Context): Int { + return context.resources.getColor( + com.android.internal.R.color.surface_effect_0, context.theme) } @JvmStatic - fun surfaceEffect1(r: Resources): Int { - return r.getColor(com.android.internal.R.color.surface_effect_1) + fun surfaceEffect1(context: Context): Int { + return context.resources.getColor( + com.android.internal.R.color.surface_effect_1, context.theme) } @JvmStatic - fun surfaceEffect2(r: Resources): Int { - return r.getColor(com.android.internal.R.color.surface_effect_2) + fun surfaceEffect2(context: Context): Int { + return context.resources.getColor( + com.android.internal.R.color.surface_effect_2, context.theme) } @JvmStatic - fun surfaceEffect3(r: Resources): Int { - return r.getColor(com.android.internal.R.color.surface_effect_3) + fun surfaceEffect3(context: Context): Int { + return context.resources.getColor( + com.android.internal.R.color.surface_effect_3, context.theme) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt index 48a6d9de380c..7765d0017c4e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt @@ -16,7 +16,9 @@ package com.android.systemui.communal +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable +import com.android.systemui.communal.dagger.CommunalModule.Companion.SHOW_UMO import com.android.systemui.communal.data.repository.CommunalMediaRepository import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -24,8 +26,8 @@ import com.android.systemui.communal.domain.interactor.CommunalSettingsInteracto import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch @SysUISingleton class CommunalOngoingContentStartable @@ -36,6 +38,7 @@ constructor( private val communalMediaRepository: CommunalMediaRepository, private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSmartspaceRepository: CommunalSmartspaceRepository, + @Named(SHOW_UMO) private val showUmoOnHub: Boolean, ) : CoreStartable { override fun start() { @@ -46,10 +49,14 @@ constructor( bgScope.launch { communalInteractor.isCommunalEnabled.collect { enabled -> if (enabled) { - communalMediaRepository.startListening() + if (showUmoOnHub) { + communalMediaRepository.startListening() + } communalSmartspaceRepository.startListening() } else { - communalMediaRepository.stopListening() + if (showUmoOnHub) { + communalMediaRepository.stopListening() + } communalSmartspaceRepository.stopListening() } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index ff741625a3cc..bb3be531aa8a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -105,6 +105,7 @@ interface CommunalModule { const val LOGGABLE_PREFIXES = "loggable_prefixes" const val LAUNCHER_PACKAGE = "launcher_package" const val SWIPE_TO_HUB = "swipe_to_hub" + const val SHOW_UMO = "show_umo" @Provides @Communal @@ -150,5 +151,11 @@ interface CommunalModule { fun provideSwipeToHub(@Main resources: Resources): Boolean { return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub) } + + @Provides + @Named(SHOW_UMO) + fun provideShowUmo(@Main resources: Resources): Boolean { + return resources.getBoolean(R.bool.config_showUmoOnHub) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 26501596aa1a..15a4722d3911 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -29,8 +29,6 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun @@ -52,8 +50,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha NotificationMinimalism.token dependsOn NotificationThrottleHun.token ModesEmptyShadeFix.token dependsOn modesUi - PromotedNotificationUiAod.token dependsOn PromotedNotificationUi.token - // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index 74d471c7c1d6..e119ec94f8c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -101,7 +101,7 @@ constructor( if (Flags.notificationShadeBlur()) { transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) } else { - emptyFlow() + transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) }, flowWhenShadeIsNotExpanded = transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 3c126aa23fef..f14a5a282e88 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -51,7 +51,7 @@ constructor( if (Flags.notificationShadeBlur()) { transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) } else { - emptyFlow() + transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) }, flowWhenShadeIsNotExpanded = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 5a111aa519fc..4a39421a3737 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -32,7 +32,6 @@ import com.android.systemui.scene.shared.model.Overlays import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow /** * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to @@ -81,7 +80,7 @@ constructor( if (Flags.notificationShadeBlur()) { transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) } else { - emptyFlow() + transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) }, flowWhenShadeIsNotExpanded = transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index 0f0e7b6faa66..31b20a7ea828 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -27,7 +27,6 @@ import com.android.systemui.keyguard.ui.transitions.BlurConfig import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow @SysUISingleton class PrimaryBouncerToOccludedTransitionViewModel @@ -51,7 +50,7 @@ constructor( if (Flags.notificationShadeBlur()) { transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) } else { - emptyFlow() + transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) }, flowWhenShadeIsNotExpanded = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx), diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt index c9716be52408..34f7c4dcaec0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt @@ -18,9 +18,6 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator -import android.icu.text.MeasureFormat -import android.icu.util.Measure -import android.icu.util.MeasureUnit import android.text.format.DateUtils import androidx.annotation.UiThread import androidx.lifecycle.Observer @@ -31,11 +28,8 @@ import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R -import java.util.Locale private const val TAG = "SeekBarObserver" -private const val MIN_IN_SEC = 60 -private const val HOUR_IN_SEC = MIN_IN_SEC * 60 /** * Observer for changes from SeekBarViewModel. @@ -133,9 +127,10 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } holder.seekBar.setMax(data.duration) - val totalTimeDescription = formatTimeContentDescription(data.duration) + val totalTimeString = + DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { - holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration) + holder.scrubbingTotalTimeView.text = totalTimeString } data.elapsedTime?.let { @@ -153,62 +148,20 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } } - val elapsedTimeDescription = formatTimeContentDescription(it) + val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { - holder.scrubbingElapsedTimeView.text = formatTimeLabel(it) + holder.scrubbingElapsedTimeView.text = elapsedTimeString } holder.seekBar.contentDescription = holder.seekBar.context.getString( R.string.controls_media_seekbar_description, - elapsedTimeDescription, - totalTimeDescription, + elapsedTimeString, + totalTimeString ) } } - /** Returns a time string suitable for display, e.g. "12:34" */ - private fun formatTimeLabel(milliseconds: Int): CharSequence { - return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS) - } - - /** - * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds" - * - * Follows same logic as Chronometer#formatDuration - */ - private fun formatTimeContentDescription(milliseconds: Int): CharSequence { - var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS - - val hours = - if (seconds >= HOUR_IN_SEC) { - seconds / HOUR_IN_SEC - } else { - 0 - } - seconds -= hours * HOUR_IN_SEC - - val minutes = - if (seconds >= MIN_IN_SEC) { - seconds / MIN_IN_SEC - } else { - 0 - } - seconds -= minutes * MIN_IN_SEC - - val measures = arrayListOf<Measure>() - if (hours > 0) { - measures.add(Measure(hours, MeasureUnit.HOUR)) - } - if (minutes > 0) { - measures.add(Measure(minutes, MeasureUnit.MINUTE)) - } - measures.add(Measure(seconds, MeasureUnit.SECOND)) - - return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) - .formatMeasures(*measures.toTypedArray()) - } - @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { val animator = @@ -216,7 +169,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : holder.seekBar, "progress", holder.seekBar.progress, - targetTime + RESET_ANIMATION_DURATION_MS, + targetTime + RESET_ANIMATION_DURATION_MS ) animator.setAutoCancel(true) animator.duration = RESET_ANIMATION_DURATION_MS.toLong() diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 609541ba1ab6..c70a854a2ca0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -20,6 +20,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.UserHandle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable import com.android.systemui.dagger.SysUISingleton @@ -62,7 +63,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext /** @@ -245,7 +245,6 @@ constructor( processExistingTile( tileSpec, specsToTiles.getValue(tileSpec), - userChanged, newUser, ) ?: createTile(tileSpec) } else { @@ -378,7 +377,6 @@ constructor( private fun processExistingTile( tileSpec: TileSpec, tileOrNotInstalled: TileOrNotInstalled, - userChanged: Boolean, user: Int, ): QSTile? { return when (tileOrNotInstalled) { @@ -386,6 +384,10 @@ constructor( is TileOrNotInstalled.Tile -> { val qsTile = tileOrNotInstalled.tile when { + qsTile.isDestroyed -> { + logger.logTileDestroyedIgnored(tileSpec) + null + } !qsTile.isAvailable -> { logger.logTileDestroyed( tileSpec, @@ -399,10 +401,11 @@ constructor( qsTile !is CustomTile -> { // The tile is not a custom tile. Make sure they are reset to the correct // user - if (userChanged) { + if (qsTile.currentTileUser != user) { qsTile.userSwitch(user) logger.logTileUserChanged(tileSpec, user) } + qsTile } qsTile.user == user -> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt index e237ca9f462b..21a8ec604f08 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -60,7 +60,7 @@ constructor( bool1 = usesDefault int1 = user }, - { "Parsed tiles (default=$bool1, user=$int1): $str1" } + { "Parsed tiles (default=$bool1, user=$int1): $str1" }, ) } @@ -77,7 +77,7 @@ constructor( str2 = reconciledTiles.toString() int1 = user }, - { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" } + { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" }, ) } @@ -94,7 +94,7 @@ constructor( str2 = newList.toString() int1 = userId }, - { "Processing $str1 for user $int1\nNew list: $str2" } + { "Processing $str1 for user $int1\nNew list: $str2" }, ) } @@ -107,7 +107,16 @@ constructor( str1 = spec.toString() str2 = reason.readable }, - { "Tile $str1 destroyed. Reason: $str2" } + { "Tile $str1 destroyed. Reason: $str2" }, + ) + } + + fun logTileDestroyedIgnored(spec: TileSpec) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { str1 = spec.toString() }, + { "Tile $str1 ignored as it was already destroyed." }, ) } @@ -117,7 +126,7 @@ constructor( TILE_LIST_TAG, LogLevel.DEBUG, { str1 = spec.toString() }, - { "Tile $str1 created" } + { "Tile $str1 created" }, ) } @@ -127,7 +136,7 @@ constructor( TILE_LIST_TAG, LogLevel.VERBOSE, { str1 = spec.toString() }, - { "Tile $str1 not found in factory" } + { "Tile $str1 not found in factory" }, ) } @@ -140,7 +149,7 @@ constructor( str1 = spec.toString() int1 = user }, - { "User changed to $int1 for tile $str1" } + { "User changed to $int1 for tile $str1" }, ) } @@ -156,7 +165,7 @@ constructor( str1 = tiles.toString() int1 = user }, - { "Tiles kept for not installed packages for user $int1: $str1" } + { "Tiles kept for not installed packages for user $int1: $str1" }, ) } @@ -168,7 +177,7 @@ constructor( str1 = tiles.toString() int1 = userId }, - { "Auto add tiles parsed for user $int1: $str1" } + { "Auto add tiles parsed for user $int1: $str1" }, ) } @@ -180,7 +189,7 @@ constructor( str1 = tiles.toString() int1 = userId }, - { "Auto-add tiles reconciled for user $int1: $str1" } + { "Auto-add tiles reconciled for user $int1: $str1" }, ) } @@ -193,7 +202,7 @@ constructor( int2 = position str1 = spec.toString() }, - { "Tile $str1 auto added for user $int1 at position $int2" } + { "Tile $str1 auto added for user $int1 at position $int2" }, ) } @@ -205,7 +214,7 @@ constructor( int1 = userId str1 = spec.toString() }, - { "Tile $str1 auto removed for user $int1" } + { "Tile $str1 auto removed for user $int1" }, ) } @@ -217,7 +226,7 @@ constructor( int1 = userId str1 = spec.toString() }, - { "Tile $str1 unmarked as auto-added for user $int1" } + { "Tile $str1 unmarked as auto-added for user $int1" }, ) } @@ -226,7 +235,7 @@ constructor( RESTORE_TAG, LogLevel.DEBUG, { int1 = userId }, - { "Restored from single intent after user setup complete for user $int1" } + { "Restored from single intent after user setup complete for user $int1" }, ) } @@ -243,7 +252,7 @@ constructor( "Restored settings data for user $int1\n" + "\tRestored tiles: $str1\n" + "\tRestored auto added tiles: $str2" - } + }, ) } @@ -258,7 +267,7 @@ constructor( str1 = restoreProcessorClassName str2 = step.name }, - { "Restore $str2 processed by $str1" } + { "Restore $str2 processed by $str1" }, ) } @@ -273,6 +282,6 @@ constructor( enum class RestorePreprocessorStep { PREPROCESSING, - POSTPROCESSING + POSTPROCESSING, } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index 1d1e9911884c..c6fc868b3dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -74,6 +74,8 @@ import com.android.systemui.qs.logging.QSLogger; import java.io.PrintWriter; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * Base quick-settings tile, extend this to create a new tile. @@ -127,6 +129,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy private int mIsFullQs; private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); + private final AtomicBoolean mIsDestroyed = new AtomicBoolean(false); + private final AtomicInteger mCurrentTileUser = new AtomicInteger(); /** * Provides a new {@link TState} of the appropriate type to use between this tile and the @@ -203,6 +207,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy mMetricsLogger = metricsLogger; mStatusBarStateController = statusBarStateController; mActivityStarter = activityStarter; + mCurrentTileUser.set(host.getUserId()); resetStates(); mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED)); @@ -352,11 +357,19 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy } public void userSwitch(int newUserId) { + mCurrentTileUser.set(newUserId); mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); postStale(); } + @Override + public int getCurrentTileUser() { + return mCurrentTileUser.get(); + } + public void destroy() { + // We mark it as soon as we start the destroy process, as nothing can interrupt it. + mIsDestroyed.set(true); mHandler.sendEmptyMessage(H.DESTROY); } @@ -365,7 +378,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy * * Should be called upon creation of the tile, before performing other operations */ - public void initialize() { + public final void initialize() { mHandler.sendEmptyMessage(H.INITIALIZE); } @@ -525,6 +538,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy }); } + @Override + public final boolean isDestroyed() { + return mIsDestroyed.get(); + } + protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, userRestriction, mHost.getUserId()); @@ -799,7 +817,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy */ @Override public void dump(PrintWriter pw, String[] args) { - pw.println(this.getClass().getSimpleName() + ":"); + pw.print(this.getClass().getSimpleName() + ":"); pw.print(" "); pw.println(getState().toString()); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt index f80b8fb8cb1f..e48e943dd3f4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt @@ -99,7 +99,7 @@ constructor( } override fun getDetailsViewModel(): TileDetailsViewModel { - return internetDetailsViewModelFactory.create { longClick(null) } + return internetDetailsViewModelFactory.create() } override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt index e8c4274474e0..8ad4e16291c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt @@ -28,17 +28,4 @@ interface QSTileUserActionInteractor<DATA_TYPE> { * It's safe to run long running computations inside this function. */ @WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>) - - /** - * Provides the [TileDetailsViewModel] for constructing the corresponding details view. - * - * This property is defined here to reuse the business logic. For example, reusing the user - * long-click as the go-to-settings callback in the details view. - * Subclasses can override this property to provide a specific [TileDetailsViewModel] - * implementation. - * - * @return The [TileDetailsViewModel] instance, or null if not implemented. - */ - val detailsViewModel: TileDetailsViewModel? - get() = null } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt index 8c75cf001441..7f475f31b940 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.base.viewmodel import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor @@ -70,9 +71,7 @@ sealed interface QSTileViewModelFactory<T> { * Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent]. * Reference of that [QSTileComponent] is then stored along the view model. */ - fun create( - tileSpec: TileSpec, - ): QSTileViewModel { + fun create(tileSpec: TileSpec): QSTileViewModel { val config = qsTileConfigProvider.getConfig(tileSpec.spec) val component = customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build() @@ -90,6 +89,7 @@ sealed interface QSTileViewModelFactory<T> { backgroundDispatcher, uiBackgroundDispatcher, component.coroutineScope(), + /* tileDetailsViewModel= */ null, ) } } @@ -127,6 +127,7 @@ sealed interface QSTileViewModelFactory<T> { userActionInteractor: QSTileUserActionInteractor<T>, tileDataInteractor: QSTileDataInteractor<T>, mapper: QSTileDataToStateMapper<T>, + tileDetailsViewModel: TileDetailsViewModel? = null, ): QSTileViewModelImpl<T> = QSTileViewModelImpl( qsTileConfigProvider.getConfig(tileSpec.spec), @@ -142,6 +143,7 @@ sealed interface QSTileViewModelFactory<T> { backgroundDispatcher, uiBackgroundDispatcher, coroutineScopeFactory.create(), + tileDetailsViewModel, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt index 30bf5b309a2e..3866c17b655f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt @@ -83,6 +83,7 @@ class QSTileViewModelImpl<DATA_TYPE>( private val backgroundDispatcher: CoroutineDispatcher, uiBackgroundDispatcher: CoroutineDispatcher, private val tileScope: CoroutineScope, + override val tileDetailsViewModel: TileDetailsViewModel? = null, ) : QSTileViewModel, Dumpable { private val users: MutableStateFlow<UserHandle> = @@ -96,6 +97,9 @@ class QSTileViewModelImpl<DATA_TYPE>( private val tileData: SharedFlow<DATA_TYPE?> = createTileDataFlow() + override val currentTileUser: Int + get() = users.value.identifier + override val state: StateFlow<QSTileState?> = tileData .map { data -> @@ -114,9 +118,6 @@ class QSTileViewModelImpl<DATA_TYPE>( .flowOn(backgroundDispatcher) .stateIn(tileScope, SharingStarted.WhileSubscribed(), true) - override val detailsViewModel: TileDetailsViewModel? - get() = userActionInteractor().detailsViewModel - override fun forceUpdate() { tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt index 0ed56f62ee6c..6709fd2bb508 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt @@ -16,9 +16,11 @@ package com.android.systemui.qs.tiles.dialog +import android.content.Intent +import android.provider.Settings import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.statusbar.connectivity.AccessPointController -import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,10 +29,13 @@ class InternetDetailsViewModel constructor( private val accessPointController: AccessPointController, val contentManagerFactory: InternetDetailsContentManager.Factory, - @Assisted private val onLongClick: () -> Unit, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, ) : TileDetailsViewModel() { override fun clickOnSettingsButton() { - onLongClick() + qsTileIntentUserActionHandler.handle( + /* expandable= */ null, + Intent(Settings.ACTION_WIFI_SETTINGS), + ) } override fun getTitle(): String { @@ -58,7 +63,7 @@ constructor( } @AssistedFactory - interface Factory { - fun create(onLongClick: () -> Unit): InternetDetailsViewModel + fun interface Factory { + fun create(): InternetDetailsViewModel } } 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 0adc41313bae..8d4a24e0c2cf 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 @@ -400,6 +400,9 @@ public class InternetDialogDelegateLegacy implements mInternetDialogTitle.setText(internetContent.mInternetDialogTitleString); mInternetDialogSubTitle.setText(internetContent.mInternetDialogSubTitle); + if (!internetContent.mIsWifiEnabled) { + setProgressBarVisible(false); + } mAirplaneModeButton.setVisibility( internetContent.mIsAirplaneModeEnabled ? View.VISIBLE : View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt index 0ed46e73958d..5f692f2f5a73 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.di +import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt index 8e48fe492e13..0431e36fef6a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt @@ -18,13 +18,10 @@ package com.android.systemui.qs.tiles.impl.internet.domain.interactor import android.content.Intent import android.provider.Settings -import com.android.systemui.animation.Expandable import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor -import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction @@ -41,7 +38,6 @@ constructor( private val internetDialogManager: InternetDialogManager, private val accessPointController: AccessPointController, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, - private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory, ) : QSTileUserActionInteractor<InternetTileModel> { override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit = @@ -58,16 +54,12 @@ constructor( } } is QSTileUserAction.LongClick -> { - handleLongClick(action.expandable) + qsTileIntentUserActionHandler.handle( + action.expandable, + Intent(Settings.ACTION_WIFI_SETTINGS), + ) } else -> {} } } - - override val detailsViewModel: TileDetailsViewModel = - internetDetailsViewModelFactory.create { handleLongClick(null) } - - private fun handleLongClick(expandable: Expandable?) { - qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS)) - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt index e8b9926e5cea..7a533883444e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt @@ -39,9 +39,12 @@ interface QSTileViewModel { val isAvailable: StateFlow<Boolean> /** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */ - val detailsViewModel: TileDetailsViewModel? + val tileDetailsViewModel: TileDetailsViewModel? get() = null + /** Returns the current user for this tile */ + val currentTileUser: Int + /** * Notifies about the user change. Implementations should avoid using 3rd party userId sources * and use this value instead. This is to maintain consistent and concurrency-free behaviour @@ -65,8 +68,6 @@ interface QSTileViewModel { fun destroy() } -/** - * Returns the immediate state of the tile or null if the state haven't been collected yet. - */ +/** Returns the immediate state of the tile or null if the state haven't been collected yet. */ val QSTileViewModel.currentState: QSTileState? get() = state.value diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 30d1f05771d7..e607eae8f38d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -19,7 +19,6 @@ package com.android.systemui.qs.tiles.viewmodel import android.content.Context import android.os.UserHandle import android.util.Log -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.InstanceId import com.android.systemui.Dumpable import com.android.systemui.animation.Expandable @@ -156,8 +155,12 @@ constructor( qsTileViewModel.onUserChanged(UserHandle.of(currentUser)) } + override fun getCurrentTileUser(): Int { + return qsTileViewModel.currentTileUser + } + override fun getDetailsViewModel(): TileDetailsViewModel? { - return qsTileViewModel.detailsViewModel + return qsTileViewModel.tileDetailsViewModel } @Deprecated( @@ -213,6 +216,10 @@ constructor( qsTileViewModel.destroy() } + override fun isDestroyed(): Boolean { + return !(tileAdapterJob?.isActive ?: false) + } + override fun getState(): QSTile.AdapterState = qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } ?: QSTile.AdapterState() diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt index 00b7e61eb1c6..bdd5c73779cf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt @@ -37,4 +37,7 @@ object StubQSTileViewModel : QSTileViewModel { override fun onActionPerformed(userAction: QSTileUserAction) = error("Don't call stubs") override fun destroy() = error("Don't call stubs") + + override val currentTileUser: Int + get() = error("Don't call stubs") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 7dc2ae71b63e..e44701dba87c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -580,7 +580,8 @@ public class CommandQueue extends IStatusBar.Stub implements /** * @see IStatusBar#immersiveModeChanged */ - default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {} + default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, + int windowType) {} /** * @see IStatusBar#moveFocusedTaskToDesktop(int) @@ -876,11 +877,13 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, + int windowType) { synchronized (mLock) { final SomeArgs args = SomeArgs.obtain(); args.argi1 = rootDisplayAreaId; args.argi2 = isImmersiveMode ? 1 : 0; + args.argi3 = windowType; mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget(); } } @@ -2030,8 +2033,10 @@ public class CommandQueue extends IStatusBar.Stub implements args = (SomeArgs) msg.obj; int rootDisplayAreaId = args.argi1; boolean isImmersiveMode = args.argi2 != 0; + int windowType = args.argi3; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode); + mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, + windowType); } break; case MSG_ENTER_DESKTOP: { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java index fed3f6e81130..97e62d79b374 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java @@ -23,6 +23,8 @@ import static android.app.StatusBarManager.DISABLE_HOME; import static android.app.StatusBarManager.DISABLE_RECENT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; +import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID; @@ -208,7 +210,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca } @Override - public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, + int windowType) { mHandler.removeMessages(H.SHOW); if (isImmersiveMode) { if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed); @@ -221,7 +224,9 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca && mCanSystemBarsBeShownByUser && !mNavBarEmpty && !UserManager.isDeviceInDemoMode(mDisplayContext) - && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) { + && (mLockTaskState != LOCK_TASK_MODE_LOCKED) + && windowType != TYPE_PRESENTATION + && windowType != TYPE_PRIVATE_PRESENTATION) { final Message msg = mHandler.obtainMessage( H.SHOW); msg.arg1 = rootDisplayAreaId; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt index 1009028345de..48f0245fd5db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt @@ -34,6 +34,7 @@ import com.android.systemui.qs.tiles.InternetTileNewImpl import com.android.systemui.qs.tiles.NfcTile import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory +import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor @@ -162,13 +163,15 @@ interface ConnectivityModule { factory: QSTileViewModelFactory.Static<AirplaneModeTileModel>, mapper: AirplaneModeMapper, stateInteractor: AirplaneModeTileDataInteractor, - userActionInteractor: AirplaneModeTileUserActionInteractor + userActionInteractor: AirplaneModeTileUserActionInteractor, + internetDetailsViewModelFactory: InternetDetailsViewModel.Factory ): QSTileViewModel = factory.create( TileSpec.create(AIRPLANE_MODE_TILE_SPEC), userActionInteractor, stateInteractor, mapper, + internetDetailsViewModelFactory.create(), ) @Provides @@ -226,13 +229,15 @@ interface ConnectivityModule { factory: QSTileViewModelFactory.Static<InternetTileModel>, mapper: InternetTileMapper, stateInteractor: InternetTileDataInteractor, - userActionInteractor: InternetTileUserActionInteractor + userActionInteractor: InternetTileUserActionInteractor, + internetDetailsViewModelFactory: InternetDetailsViewModel.Factory ): QSTileViewModel = factory.create( TileSpec.create(INTERNET_TILE_SPEC), userActionInteractor, stateInteractor, mapper, + internetDetailsViewModelFactory.create(), ) @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index 25deec375c03..d09546fe80ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -391,7 +391,7 @@ public class FooterView extends StackScrollerDecorView { if (!notificationFooterBackgroundTintOptimization()) { if (notificationShadeBlur()) { Color backgroundColor = Color.valueOf( - SurfaceEffectColors.surfaceEffect1(getResources())); + SurfaceEffectColors.surfaceEffect1(getContext())); scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF); // Apply alpha on background drawables. int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 4ed9dcee072e..a081ad5bb82c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -129,7 +129,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private void updateColors() { if (notificationRowTransparency()) { - mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); } else { mNormalColor = mContext.getColor( com.android.internal.R.color.materialColorSurfaceContainerHigh); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt index 6bfc9f07ffc4..4bd6053ea23c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt @@ -21,7 +21,6 @@ import android.app.INotificationManager import android.app.NotificationChannel import android.app.NotificationChannel.DEFAULT_CHANNEL_ID import android.app.NotificationChannelGroup -import android.app.NotificationManager.IMPORTANCE_NONE import android.app.NotificationManager.Importance import android.content.Context import android.graphics.Color @@ -40,7 +39,7 @@ import android.widget.TextView import com.android.internal.annotations.VisibleForTesting import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import javax.inject.Inject private const val TAG = "ChannelDialogController" @@ -59,9 +58,9 @@ private const val TAG = "ChannelDialogController" */ @SysUISingleton class ChannelEditorDialogController @Inject constructor( - @ShadeDisplayAware private val context: Context, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, private val noMan: INotificationManager, - private val dialogBuilder: ChannelEditorDialog.Builder + private val dialogBuilder: ChannelEditorDialog.Builder, ) { private var prepared = false @@ -272,7 +271,7 @@ class ChannelEditorDialogController @Inject constructor( } private fun initDialog() { - dialogBuilder.setContext(context) + dialogBuilder.setContext(shadeDialogContextInteractor.context) dialog = dialogBuilder.build() dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index 33c36d8c4c76..c0bc13270664 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -88,7 +88,7 @@ public class NotificationBackgroundView extends View implements Dumpable, mDarkColoredStatefulColors = getResources().getColorStateList( R.color.notification_state_color_dark); if (notificationRowTransparency()) { - mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); } else { mNormalColor = mContext.getColor( com.android.internal.R.color.materialColorSurfaceContainerHigh); @@ -321,7 +321,7 @@ public class NotificationBackgroundView extends View implements Dumpable, new PorterDuffColorFilter( isColorized() ? ColorUtils.setAlphaComponent(mTintColor, (int) (255 * 0.9f)) - : SurfaceEffectColors.surfaceEffect1(getResources()), + : SurfaceEffectColors.surfaceEffect1(getContext()), PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index ab382df13d10..e89a76fd5a69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; +import static android.app.Flags.notificationsRedesignTemplates; import static android.view.HapticFeedbackConstants.CLOCK_TICK; import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; @@ -706,8 +706,11 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl static NotificationMenuItem createInfoItem(Context context) { Resources res = context.getResources(); String infoDescription = res.getString(R.string.notification_menu_gear_description); + int layoutId = notificationsRedesignTemplates() + ? R.layout.notification_2025_info + : R.layout.notification_info; NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( - R.layout.notification_info, null, false); + layoutId, null, false); return new NotificationMenuItem(context, infoDescription, infoContent, R.drawable.ic_settings); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index da988589184f..9bd5a5bd903f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -291,7 +291,7 @@ constructor( * currently being swiped. From the center outwards, the multipliers apply to the neighbors * of the swiped view. */ - private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f) + private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f) const val MAGNETIC_REDUCTION = 0.65f @@ -299,7 +299,7 @@ constructor( private const val DETACH_STIFFNESS = 800f private const val DETACH_DAMPING_RATIO = 0.95f private const val SNAP_BACK_STIFFNESS = 550f - private const val SNAP_BACK_DAMPING_RATIO = 0.52f + private const val SNAP_BACK_DAMPING_RATIO = 0.6f // Maximum value of corner roundness that gets applied during the pre-detach dragging private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index a0e3fbd2f3d1..8b0f7c4d2451 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -29,18 +29,13 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.SliderState -import androidx.compose.material3.VerticalSlider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput @@ -49,16 +44,17 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme import com.android.compose.ui.graphics.painter.DrawablePainter +import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel -import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider +import com.android.systemui.volume.ui.slider.AccessibilityParams +import com.android.systemui.volume.ui.slider.Haptics +import com.android.systemui.volume.ui.slider.Slider import javax.inject.Inject -import kotlin.math.round import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive @@ -90,7 +86,7 @@ constructor( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun VolumeDialogSlider( viewModel: VolumeDialogSliderViewModel, @@ -108,59 +104,8 @@ private fun VolumeDialogSlider( ) val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null) val sliderStateModel = collectedSliderStateModel ?: return - - val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt() - val interactionSource = remember { MutableInteractionSource() } - val hapticsViewModel: SliderHapticsViewModel? = - hapticsViewModelFactory?.let { - rememberViewModel(traceName = "SliderHapticsViewModel") { - it.create( - interactionSource, - sliderStateModel.valueRange, - Orientation.Vertical, - VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig( - sliderStateModel.valueRange - ), - VolumeHapticsConfigsProvider.seekableSliderTrackerConfig, - ) - } - } - val sliderState = - remember(steps, sliderStateModel.valueRange) { - SliderState( - value = sliderStateModel.value, - valueRange = sliderStateModel.valueRange, - steps = steps, - ) - .also { sliderState -> - sliderState.onValueChangeFinished = { - viewModel.onSliderChangeFinished(sliderState.value) - hapticsViewModel?.onValueChangeEnded() - } - sliderState.onValueChange = { newValue -> - sliderState.value = newValue - hapticsViewModel?.addVelocityDataPoint(newValue) - overscrollViewModel.setSlider( - value = sliderState.value, - min = sliderState.valueRange.start, - max = sliderState.valueRange.endInclusive, - ) - viewModel.setStreamVolume(newValue, true) - } - } - } - - var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) } - LaunchedEffect(sliderStateModel.value) { - val value = sliderStateModel.value - sliderState.value = value - if (value != lastDiscreteStep) { - lastDiscreteStep = value - hapticsViewModel?.onValueChange(value) - } - } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { when (it) { @@ -171,24 +116,33 @@ private fun VolumeDialogSlider( } } - VerticalSlider( - state = sliderState, - enabled = !sliderStateModel.isDisabled, - reverseDirection = true, + Slider( + value = sliderStateModel.value, + valueRange = sliderStateModel.valueRange, + onValueChanged = { value -> + overscrollViewModel.setSlider( + value = value, + min = sliderStateModel.valueRange.start, + max = sliderStateModel.valueRange.endInclusive, + ) + viewModel.setStreamVolume(value, true) + }, + onValueChangeFinished = { viewModel.onSliderChangeFinished(it) }, + isEnabled = !sliderStateModel.isDisabled, + isReverseDirection = true, + isVertical = true, colors = colors, interactionSource = interactionSource, - modifier = - modifier.pointerInput(Unit) { - coroutineScope { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - while (currentContext.isActive) { - viewModel.onTouchEvent(awaitPointerEvent()) - } - } - } - }, - track = { + haptics = + hapticsViewModelFactory?.let { + Haptics.Enabled( + hapticsViewModelFactory = it, + hapticFilter = SliderHapticFeedbackFilter(), + orientation = Orientation.Vertical, + ) + } ?: Haptics.Disabled, + stepDistance = 1f, + track = { sliderState -> VolumeDialogSliderTrack( sliderState, colors = colors, @@ -201,6 +155,19 @@ private fun VolumeDialogSlider( }, ) }, + accessibilityParams = + AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""), + modifier = + modifier.pointerInput(Unit) { + coroutineScope { + val currentContext = currentCoroutineContext() + awaitPointerEventScope { + while (currentContext.isActive) { + viewModel.onTouchEvent(awaitPointerEvent()) + } + } + } + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt index 3efb2b464a1d..3d98ebacc7ca 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt @@ -116,8 +116,8 @@ constructor( override val isEnabled: Boolean get() = true - override val a11yStep: Int - get() = 1 + override val a11yStep: Float + get() = 1f override val disabledMessage: String? get() = null diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index f9d776bc3aaf..9d32285fecb3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -165,7 +165,7 @@ constructor( label = label, disabledMessage = disabledMessage, isEnabled = isEnabled, - a11yStep = volumeRange.step, + a11yStep = volumeRange.step.toFloat(), a11yClickDescription = if (isAffectedByMute) { context.getString( @@ -307,7 +307,7 @@ constructor( override val label: String, override val disabledMessage: String?, override val isEnabled: Boolean, - override val a11yStep: Int, + override val a11yStep: Float, override val a11yClickDescription: String?, override val a11yStateDescription: String?, override val isMutable: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index d74a433ad86c..a6c809186ca5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -86,7 +86,7 @@ constructor( icon = Icon.Resource(R.drawable.ic_cast, null), label = context.getString(R.string.media_device_cast), isEnabled = true, - a11yStep = 1, + a11yStep = 1f, ) } @@ -96,7 +96,7 @@ constructor( override val icon: Icon, override val label: String, override val isEnabled: Boolean, - override val a11yStep: Int, + override val a11yStep: Float, ) : SliderState { override val hapticFilter: SliderHapticFeedbackFilter get() = SliderHapticFeedbackFilter() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt index f1353713799d..4bc237bd36f5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt @@ -36,7 +36,7 @@ sealed interface SliderState { * A11y slider controls works by adjusting one step up or down. The default slider step isn't * enough to trigger rounding to the correct value. */ - val a11yStep: Int + val a11yStep: Float val a11yClickDescription: String? val a11yStateDescription: String? val disabledMessage: String? @@ -49,7 +49,7 @@ sealed interface SliderState { override val icon: Icon? = null override val label: String = "" override val disabledMessage: String? = null - override val a11yStep: Int = 0 + override val a11yStep: Float = 0f override val a11yClickDescription: String? = null override val a11yStateDescription: String? = null override val isEnabled: Boolean = true diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt new file mode 100644 index 000000000000..d3562e2a4235 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt @@ -0,0 +1,265 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) + +package com.android.systemui.volume.ui.slider + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SliderState +import androidx.compose.material3.VerticalSlider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.setProgress +import androidx.compose.ui.semantics.stateDescription +import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.res.R +import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider +import kotlin.math.round +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +private val defaultSpring = + SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh) +private val defaultTrack: @Composable (SliderState) -> Unit = + @Composable { SliderDefaults.Track(it) } + +@Composable +fun Slider( + value: Float, + valueRange: ClosedFloatingPointRange<Float>, + onValueChanged: (Float) -> Unit, + onValueChangeFinished: ((Float) -> Unit)?, + stepDistance: Float, + isEnabled: Boolean, + accessibilityParams: AccessibilityParams, + modifier: Modifier = Modifier, + colors: SliderColors = SliderDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + haptics: Haptics = Haptics.Disabled, + isVertical: Boolean = false, + isReverseDirection: Boolean = false, + track: (@Composable (SliderState) -> Unit)? = null, +) { + require(stepDistance > 0) { "stepDistance must be positive" } + val coroutineScope = rememberCoroutineScope() + val snappedValue = snapValue(value, valueRange, stepDistance) + val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource) + + val animatable = remember { Animatable(snappedValue) } + var animationJob: Job? by remember { mutableStateOf(null) } + val sliderState = + remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) } + val valueChange: (Float) -> Unit = { newValue -> + hapticsViewModel?.onValueChange(newValue) + val snappedNewValue = snapValue(newValue, valueRange, stepDistance) + if (animatable.targetValue != snappedNewValue) { + onValueChanged(snappedNewValue) + animationJob?.cancel() + animationJob = + coroutineScope.launch { + animatable.animateTo( + targetValue = snappedNewValue, + animationSpec = defaultSpring, + ) + } + } + } + val semantics = + accessibilityParams.createSemantics( + animatable.targetValue, + valueRange, + valueChange, + isEnabled, + stepDistance, + ) + + LaunchedEffect(snappedValue) { + if (!animatable.isRunning && animatable.targetValue != snappedValue) { + animationJob?.cancel() + animationJob = + coroutineScope.launch { + animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring) + } + } + } + + sliderState.onValueChangeFinished = { + hapticsViewModel?.onValueChangeEnded() + onValueChangeFinished?.invoke(animatable.targetValue) + } + sliderState.onValueChange = valueChange + sliderState.value = animatable.value + + if (isVertical) { + VerticalSlider( + state = sliderState, + enabled = isEnabled, + reverseDirection = isReverseDirection, + interactionSource = interactionSource, + colors = colors, + track = track ?: defaultTrack, + modifier = modifier.clearAndSetSemantics(semantics), + ) + } else { + Slider( + state = sliderState, + enabled = isEnabled, + interactionSource = interactionSource, + colors = colors, + track = track ?: defaultTrack, + modifier = modifier.clearAndSetSemantics(semantics), + ) + } +} + +private fun snapValue( + value: Float, + valueRange: ClosedFloatingPointRange<Float>, + stepDistance: Float, +): Float { + if (stepDistance == 0f) { + return value + } + val coercedValue = value.coerceIn(valueRange) + return Math.round(coercedValue / stepDistance) * stepDistance +} + +@Composable +private fun AccessibilityParams.createSemantics( + value: Float, + valueRange: ClosedFloatingPointRange<Float>, + onValueChanged: (Float) -> Unit, + isEnabled: Boolean, + stepDistance: Float, +): SemanticsPropertyReceiver.() -> Unit { + val semanticsContentDescription = + disabledMessage + ?.takeIf { !isEnabled } + ?.let { message -> + stringResource(R.string.volume_slider_disabled_message_template, label, message) + } ?: label + return { + contentDescription = semanticsContentDescription + if (isEnabled) { + currentStateDescription?.let { stateDescription = it } + progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange) + } else { + disabled() + } + setProgress { targetValue -> + val targetDirection = + when { + targetValue > value -> 1 + targetValue < value -> -1 + else -> 0 + } + + val newValue = + (value + targetDirection * stepDistance).coerceIn( + valueRange.start, + valueRange.endInclusive, + ) + onValueChanged(newValue) + true + } + } +} + +@Composable +private fun Haptics.createViewModel( + value: Float, + valueRange: ClosedFloatingPointRange<Float>, + interactionSource: MutableInteractionSource, +): SliderHapticsViewModel? { + return when (this) { + is Haptics.Disabled -> null + is Haptics.Enabled -> { + hapticsViewModelFactory.let { + rememberViewModel(traceName = "SliderHapticsViewModel") { + it.create( + interactionSource, + valueRange, + orientation, + VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig( + valueRange, + hapticFilter, + ), + VolumeHapticsConfigsProvider.seekableSliderTrackerConfig, + ) + } + .also { hapticsViewModel -> + var lastDiscreteStep by remember { mutableFloatStateOf(value) } + LaunchedEffect(value) { + snapshotFlow { value } + .map { round(it) } + .filter { it != lastDiscreteStep } + .distinctUntilChanged() + .collect { discreteStep -> + lastDiscreteStep = discreteStep + hapticsViewModel.onValueChange(discreteStep) + } + } + } + } + } + } +} + +data class AccessibilityParams( + val label: String, + val currentStateDescription: String?, + val disabledMessage: String?, +) + +sealed interface Haptics { + data object Disabled : Haptics + + data class Enabled( + val hapticsViewModelFactory: SliderHapticsViewModel.Factory, + val hapticFilter: SliderHapticFeedbackFilter, + val orientation: Orientation, + ) : Haptics +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java index 3d0a8f6cd236..ebbe023d0d24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java @@ -878,4 +878,18 @@ public class InternetDialogDelegateLegacyTest extends SysuiTestCase { mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE); mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE); } + + @Test + public void updateDialog_wifiIsDisabled_turnOffProgressBar() { + when(mInternetDetailsContentController.isWifiEnabled()).thenReturn(false); + mInternetDialogDelegateLegacy.mIsProgressBarVisible = true; + + mInternetDialogDelegateLegacy.updateDialog(false); + + mBgExecutor.runAllReady(); + mInternetDialogDelegateLegacy.mDataInternetContent.observe( + mInternetDialogDelegateLegacy.mLifecycleOwner, i -> { + assertThat(mInternetDialogDelegateLegacy.mIsProgressBarVisible).isFalse(); + }); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt index c5b19ab5862c..0b2fea53d811 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt @@ -31,13 +31,13 @@ import android.testing.TestableLooper import android.view.View import com.android.systemui.SysuiTestCase +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor import org.junit.Assert.assertEquals import org.junit.Before import org.junit.runner.RunWith import org.junit.Test import org.mockito.Answers -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito @@ -66,11 +66,14 @@ class ChannelEditorDialogControllerTest : SysuiTestCase() { @Mock private lateinit var dialog: ChannelEditorDialog + private val shadeDialogContextInteractor = FakeShadeDialogContextInteractor(mContext) + @Before fun setup() { MockitoAnnotations.initMocks(this) `when`(dialogBuilder.build()).thenReturn(dialog) - controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder) + controller = + ChannelEditorDialogController(shadeDialogContextInteractor, mockNoMan, dialogBuilder) channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT) channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index e4806e62a9e5..e4806e62a9e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 9e914ad0a660..9e914ad0a660 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 804e7d635107..804e7d635107 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt index 4714969af508..c7ea6db926d1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt @@ -22,7 +22,7 @@ import com.android.systemui.plugins.qs.QSTile class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile { private var tileSpec: String? = null - var destroyed = false + private var destroyed = false var hasDetailsViewModel: Boolean = true private var state = QSTile.State() val callbacks = mutableListOf<QSTile.Callback>() @@ -64,6 +64,10 @@ class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile { user = currentUser } + override fun getCurrentTileUser(): Int { + return user + } + override fun getMetricsCategory(): Int { return 0 } @@ -76,6 +80,10 @@ class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile { destroyed = true } + override fun isDestroyed(): Boolean { + return destroyed + } + override fun getTileLabel(): CharSequence { return "" } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt index 4978558ff8a2..f038fdd3a1cd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.fakeSystemClock -val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by +var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by Kosmos.Fixture { TileLifecycleManager.Factory { intent, userHandle -> TileLifecycleManager( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt index 7d52f5d8aa34..c1531835b136 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt @@ -17,7 +17,14 @@ package com.android.systemui.qs.pipeline.shared.logging import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.log.logcatLogBuffer /** mock */ -var Kosmos.qsLogger: QSPipelineLogger by Kosmos.Fixture { mock<QSPipelineLogger>() } +var Kosmos.qsLogger: QSPipelineLogger by + Kosmos.Fixture { + QSPipelineLogger( + logcatLogBuffer(QSPipelineLogger.TILE_LIST_TAG), + logcatLogBuffer(QSPipelineLogger.AUTO_ADD_TAG), + logcatLogBuffer(QSPipelineLogger.RESTORE_TAG), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt index bc1c60c33d71..c0584903db2d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt @@ -33,7 +33,4 @@ class FakeQSTileUserActionInteractor<T> : QSTileUserActionInteractor<T> { override suspend fun handleInput(input: QSTileInput<T>) { mutex.withLock { mutableInputs.add(input) } } - - override var detailsViewModel: TileDetailsViewModel? = - FakeTileDetailsViewModel("FakeQSTileUserActionInteractor") } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt index 6787b8ebb37f..c223be44a70c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.di import android.os.UserHandle import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.instanceIdSequenceFake +import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.TileCategory import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory @@ -56,7 +57,11 @@ val Kosmos.customTileViewModelFactory: QSTileViewModelFactory.Component by override val config: QSTileConfig = config override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true) - override fun onUserChanged(user: UserHandle) {} + override var currentTileUser = currentTilesInteractor.userId.value + + override fun onUserChanged(user: UserHandle) { + currentTileUser = user.identifier + } override fun forceUpdate() {} @@ -68,7 +73,7 @@ val Kosmos.customTileViewModelFactory: QSTileViewModelFactory.Component by } } -val Kosmos.newQSTileFactory by +var Kosmos.newQSTileFactory by Kosmos.Fixture { NewQSTileFactory( qSTileConfigProvider, diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 3ebef02284d6..b6167665c985 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -23,13 +23,10 @@ import static org.junit.Assume.assumeTrue; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.Bundle; import android.platform.test.annotations.RavenwoodTestRunnerInitializing; import android.platform.test.annotations.internal.InnerRunner; import android.util.Log; -import androidx.test.platform.app.InstrumentationRegistry; - import com.android.ravenwood.common.RavenwoodCommonUtils; import org.junit.rules.TestRule; @@ -285,11 +282,6 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase private boolean onBefore(Description description, Scope scope, Order order) { Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); - if (scope == Scope.Instance && order == Order.Outer) { - // Start of a test method. - mState.enterTestMethod(description); - } - final var classDescription = getDescription(); // Class-level annotations are checked by the runner already, so we only check @@ -299,6 +291,12 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase return false; } } + + if (scope == Scope.Instance && order == Order.Outer) { + // Start of a test method. + mState.enterTestMethod(description); + } + return true; } @@ -314,8 +312,7 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase if (scope == Scope.Instance && order == Order.Outer) { // End of a test method. - mState.exitTestMethod(); - + mState.exitTestMethod(description); } // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error. diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java index 70bc52bdaa12..705186edba00 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java @@ -81,12 +81,15 @@ public final class RavenwoodRunnerState { RavenwoodRuntimeEnvironmentController.exitTestClass(); } + /** Called when a test method is about to start */ public void enterTestMethod(Description description) { mMethodDescription = description; - RavenwoodRuntimeEnvironmentController.initForMethod(); + RavenwoodRuntimeEnvironmentController.enterTestMethod(description); } - public void exitTestMethod() { + /** Called when a test method finishes */ + public void exitTestMethod(Description description) { + RavenwoodRuntimeEnvironmentController.exitTestMethod(description); mMethodDescription = null; } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java index f205d238c693..d935626c34df 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -51,6 +51,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; +import android.os.Message; import android.os.Process_ravenwood; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -74,6 +75,7 @@ import com.android.ravenwood.common.SneakyThrow; import com.android.server.LocalServices; import com.android.server.compat.PlatformCompat; +import org.junit.AssumptionViolatedException; import org.junit.internal.management.ManagementFactory; import org.junit.runner.Description; @@ -81,6 +83,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -93,6 +96,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Responsible for initializing and the environment. @@ -107,32 +111,60 @@ public class RavenwoodRuntimeEnvironmentController { @SuppressWarnings("UnusedVariable") private static final PrintStream sStdErr = System.err; - private static final String MAIN_THREAD_NAME = "RavenwoodMain"; + private static final String MAIN_THREAD_NAME = "Ravenwood:Main"; + private static final String TESTS_THREAD_NAME = "Ravenwood:Test"; + private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer"; private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime"; private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS"; private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS; + static volatile Thread sTestThread; + static volatile Thread sMainThread; + /** * When enabled, attempt to dump all thread stacks just before we hit the * overall Tradefed timeout, to aid in debugging deadlocks. + * + * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it. + * It'll merely print stacktraces. */ private static final boolean ENABLE_TIMEOUT_STACKS = - "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS")); + !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS")); + + private static final boolean TOLERATE_LOOPER_ASSERTS = + !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS")); + + static final int DEFAULT_TIMEOUT_SECONDS = 10; + private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000; + + static int getTimeoutSeconds() { + var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS"); + if (e == null || e.isEmpty()) { + return DEFAULT_TIMEOUT_SECONDS; + } + return Integer.parseInt(e); + } - private static final int TIMEOUT_MILLIS = 9_000; private static final ScheduledExecutorService sTimeoutExecutor = - Executors.newScheduledThreadPool(1); + Executors.newScheduledThreadPool(1, (Runnable r) -> { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("Ravenwood:TimeoutMonitor"); + t.setDaemon(true); + return t; + }); - private static ScheduledFuture<?> sPendingTimeout; + private static volatile ScheduledFuture<?> sPendingTimeout; /** * When enabled, attempt to detect uncaught exceptions from background threads. */ private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = - "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION")); + !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION")); + + private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true; /** * When set, an unhandled exception was discovered (typically on a background thread), and we @@ -141,12 +173,6 @@ public class RavenwoodRuntimeEnvironmentController { private static final AtomicReference<Throwable> sPendingUncaughtException = new AtomicReference<>(); - private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler = - (thread, throwable) -> { - // Remember the first exception we discover - sPendingUncaughtException.compareAndSet(null, throwable); - }; - // TODO: expose packCallingIdentity function in libbinder and use it directly // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp private static long packBinderIdentityToken( @@ -187,6 +213,8 @@ public class RavenwoodRuntimeEnvironmentController { * Initialize the global environment. */ public static void globalInitOnce() { + sTestThread = Thread.currentThread(); + Thread.currentThread().setName(TESTS_THREAD_NAME); synchronized (sInitializationLock) { if (!sInitialized) { // globalInitOnce() is called from class initializer, which cause @@ -194,6 +222,7 @@ public class RavenwoodRuntimeEnvironmentController { sInitialized = true; // This is the first call. + final long start = System.currentTimeMillis(); try { globalInitInner(); } catch (Throwable th) { @@ -202,6 +231,9 @@ public class RavenwoodRuntimeEnvironmentController { sExceptionFromGlobalInit = th; SneakyThrow.sneakyThrow(th); } + final long end = System.currentTimeMillis(); + // TODO Show user/system time too + Log.e(TAG, "globalInit() took " + (end - start) + "ms"); } else { // Subsequent calls. If the first call threw, just throw the same error, to prevent // the test from running. @@ -220,7 +252,8 @@ public class RavenwoodRuntimeEnvironmentController { RavenwoodCommonUtils.log(TAG, "globalInitInner()"); if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + Thread.setDefaultUncaughtExceptionHandler( + RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions); } // Some process-wide initialization: @@ -304,6 +337,7 @@ public class RavenwoodRuntimeEnvironmentController { ActivityManager.init$ravenwood(SYSTEM.getIdentifier()); final var main = new HandlerThread(MAIN_THREAD_NAME); + sMainThread = main; main.start(); Looper.setMainLooperForTest(main.getLooper()); @@ -350,9 +384,20 @@ public class RavenwoodRuntimeEnvironmentController { var systemServerContext = new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader); - sInstrumentation = new Instrumentation(); - sInstrumentation.basicInit(instContext, targetContext, null); - InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY); + var instArgs = Bundle.EMPTY; + RavenwoodUtils.runOnMainThreadSync(() -> { + try { + // TODO We should get the instrumentation class name from the build file or + // somewhere. + var InstClass = Class.forName("android.app.Instrumentation"); + sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance(); + sInstrumentation.basicInit(instContext, targetContext, null); + sInstrumentation.onCreate(instArgs); + } catch (Exception e) { + SneakyThrow.sneakyThrow(e); + } + }); + InstrumentationRegistry.registerInstance(sInstrumentation, instArgs); RavenwoodSystemServer.init(systemServerContext); @@ -399,22 +444,46 @@ public class RavenwoodRuntimeEnvironmentController { SystemProperties.clearChangeCallbacksForTest(); - if (ENABLE_TIMEOUT_STACKS) { - sPendingTimeout = sTimeoutExecutor.schedule( - RavenwoodRuntimeEnvironmentController::dumpStacks, - TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } - if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - maybeThrowPendingUncaughtException(false); - } + maybeThrowPendingUncaughtException(); } /** - * Partially reset and initialize before each test method invocation + * Called when a test method is about to be started. */ - public static void initForMethod() { + public static void enterTestMethod(Description description) { // TODO(b/375272444): this is a hacky workaround to ensure binder identity Binder.restoreCallingIdentity(sCallingIdentity); + + scheduleTimeout(); + } + + /** + * Called when a test method finished. + */ + public static void exitTestMethod(Description description) { + cancelTimeout(); + maybeThrowPendingUncaughtException(); + } + + private static void scheduleTimeout() { + if (!ENABLE_TIMEOUT_STACKS) { + return; + } + cancelTimeout(); + + sPendingTimeout = sTimeoutExecutor.schedule( + RavenwoodRuntimeEnvironmentController::onTestTimedOut, + TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } + + private static void cancelTimeout() { + if (!ENABLE_TIMEOUT_STACKS) { + return; + } + var pt = sPendingTimeout; + if (pt != null) { + pt.cancel(false); + } } private static void initializeCompatIds() { @@ -473,15 +542,36 @@ public class RavenwoodRuntimeEnvironmentController { } /** + * Return if an exception is benign and okay to continue running the main looper even + * if we detect it. + */ + private static boolean isThrowableBenign(Throwable th) { + return th instanceof AssertionError || th instanceof AssumptionViolatedException; + } + + static void dispatchMessage(Message msg) { + try { + msg.getTarget().dispatchMessage(msg); + } catch (Throwable th) { + var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(), + Thread.currentThread()); + sStdErr.println(desc); + if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) { + sStdErr.printf("*** Continuing the test because it's %s ***\n", + th.getClass().getSimpleName()); + var e = new Exception(desc, th); + sPendingUncaughtException.compareAndSet(null, e); + return; + } + throw th; + } + } + + /** * A callback when a test class finishes its execution, mostly only for debugging. */ public static void exitTestClass() { - if (ENABLE_TIMEOUT_STACKS) { - sPendingTimeout.cancel(false); - } - if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - maybeThrowPendingUncaughtException(true); - } + maybeThrowPendingUncaughtException(); } public static void logTestRunner(String label, Description description) { @@ -491,35 +581,70 @@ public class RavenwoodRuntimeEnvironmentController { + "(" + description.getTestClass().getName() + ")"); } - private static void dumpStacks() { - final PrintStream out = System.err; - out.println("-----BEGIN ALL THREAD STACKS-----"); - final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); - for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) { - out.println(); - Thread t = stack.getKey(); - out.println(t.toString() + " ID=" + t.getId()); - for (StackTraceElement e : stack.getValue()) { - out.println("\tat " + e); - } + private static void maybeThrowPendingUncaughtException() { + final Throwable pending = sPendingUncaughtException.getAndSet(null); + if (pending != null) { + throw new IllegalStateException("Found an uncaught exception", pending); } - out.println("-----END ALL THREAD STACKS-----"); } /** - * If there's a pending uncaught exception, consume and throw it now. Typically used to - * report an exception on a background thread as a failure for the currently running test. + * Prints the stack trace from all threads. */ - private static void maybeThrowPendingUncaughtException(boolean duringReset) { - final Throwable pending = sPendingUncaughtException.getAndSet(null); - if (pending != null) { - if (duringReset) { - throw new IllegalStateException( - "Found an uncaught exception during this test", pending); - } else { - throw new IllegalStateException( - "Found an uncaught exception before this test started", pending); + private static void onTestTimedOut() { + sStdErr.println("********* SLOW TEST DETECTED ********"); + dumpStacks(null, null); + } + + private static final Object sDumpStackLock = new Object(); + + /** + * Prints the stack trace from all threads. + */ + private static void dumpStacks( + @Nullable Thread exceptionThread, @Nullable Throwable throwable) { + cancelTimeout(); + synchronized (sDumpStackLock) { + final PrintStream out = sStdErr; + out.println("-----BEGIN ALL THREAD STACKS-----"); + + var stacks = Thread.getAllStackTraces(); + var threads = stacks.keySet().stream().sorted( + Comparator.comparingLong(Thread::getId)).collect(Collectors.toList()); + + // Put the test and the main thread at the top. + var testThread = sTestThread; + var mainThread = sMainThread; + if (mainThread != null) { + threads.remove(mainThread); + threads.add(0, mainThread); + } + if (testThread != null) { + threads.remove(testThread); + threads.add(0, testThread); + } + // Put the exception thread at the top. + // Also inject the stacktrace from the exception. + if (exceptionThread != null) { + threads.remove(exceptionThread); + threads.add(0, exceptionThread); + stacks.put(exceptionThread, throwable.getStackTrace()); + } + for (var th : threads) { + out.println(); + + out.print("Thread"); + if (th == exceptionThread) { + out.print(" [** EXCEPTION THREAD **]"); + } + out.print(": " + th.getName() + " / " + th); + out.println(); + + for (StackTraceElement e : stacks.get(th)) { + out.println("\tat " + e); + } } + out.println("-----END ALL THREAD STACKS-----"); } } @@ -545,13 +670,17 @@ public class RavenwoodRuntimeEnvironmentController { () -> Class.forName("org.mockito.Matchers")); } - // TODO: use the real UiAutomation class instead of a mock - private static UiAutomation createMockUiAutomation() { - sAdoptedPermissions = Collections.emptySet(); - var mock = mock(UiAutomation.class, inv -> { + static <T> T makeDefaultThrowMock(Class<T> clazz) { + return mock(clazz, inv -> { HostTestUtils.onThrowMethodCalled(); return null; }); + } + + // TODO: use the real UiAutomation class instead of a mock + private static UiAutomation createMockUiAutomation() { + sAdoptedPermissions = Collections.emptySet(); + var mock = makeDefaultThrowMock(UiAutomation.class); doAnswer(inv -> { sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS; return null; @@ -586,6 +715,23 @@ public class RavenwoodRuntimeEnvironmentController { } } + private static void reportUncaughtExceptions(Thread th, Throwable e) { + sStdErr.printf("Uncaught exception detected: %s: %s\n", + th, RavenwoodCommonUtils.getStackTraceString(e)); + + doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION); + } + + private static void doBugreport( + @Nullable Thread exceptionThread, @Nullable Throwable throwable, + boolean killSelf) { + // TODO: Print more information + dumpStacks(exceptionThread, throwable); + if (killSelf) { + System.exit(13); + } + } + private static void dumpJavaProperties() { Log.v(TAG, "JVM properties:"); dumpMap(System.getProperties()); @@ -601,7 +747,6 @@ public class RavenwoodRuntimeEnvironmentController { Log.v(TAG, " " + key + "=" + map.get(key)); } } - private static void dumpOtherInfo() { Log.v(TAG, "Other key information:"); var jloc = Locale.getDefault(); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java index 70c161c1f19a..819d93a9c336 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -45,6 +46,9 @@ public class RavenwoodSystemProperties { /** The default values. */ static final Map<String, String> sDefaultValues = new HashMap<>(); + static final Set<String> sReadableKeys = new HashSet<>(); + static final Set<String> sWritableKeys = new HashSet<>(); + private static final String[] PARTITIONS = { "bootimage", "odm", @@ -88,9 +92,24 @@ public class RavenwoodSystemProperties { ravenwoodProps.forEach((key, origValue) -> { final String value; - // If a value starts with "$$$", then this is a reference to the device-side value. if (origValue.startsWith("$$$")) { + // If a value starts with "$$$", then: + // - If it's "$$$r", the key is allowed to read. + // - If it's "$$$w", the key is allowed to write. + // - Otherwise, it's a reference to the device-side value. + // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded + // as a prefix match. var deviceKey = origValue.substring(3); + if ("r".equals(deviceKey)) { + sReadableKeys.add(key); + Log.v(TAG, key + " (readable)"); + return; + } else if ("w".equals(deviceKey)) { + sWritableKeys.add(key); + Log.v(TAG, key + " (writable)"); + return; + } + var deviceValue = deviceProps.get(deviceKey); if (deviceValue == null) { throw new RuntimeException("Failed to initialize system properties. Key '" @@ -131,50 +150,38 @@ public class RavenwoodSystemProperties { sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty); } - private static boolean isKeyReadable(String key) { - // All writable keys are also readable - if (isKeyWritable(key)) return true; + private static boolean checkAllowedInner(String key, Set<String> allowed) { + if (allowed.contains(key)) { + return true; + } - final String root = getKeyRoot(key); + // Also search for a prefix match. + for (var k : allowed) { + if (k.endsWith(".") && key.startsWith(k)) { + return true; + } + } + return false; + } - // This set is carefully curated to help identify situations where a test may - // accidentally depend on a default value of an obscure property whose owner hasn't - // decided how Ravenwood should behave. - if (root.startsWith("boot.")) return true; - if (root.startsWith("build.")) return true; - if (root.startsWith("product.")) return true; - if (root.startsWith("soc.")) return true; - if (root.startsWith("system.")) return true; + private static boolean checkAllowed(String key, Set<String> allowed) { + return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed); + } + private static boolean isKeyReadable(String key) { // All core values should be readable - if (sDefaultValues.containsKey(key)) return true; - - // Hardcoded allowlist - return switch (key) { - case "gsm.version.baseband", - "no.such.thing", - "qemu.sf.lcd_density", - "ro.bootloader", - "ro.hardware", - "ro.hw_timeout_multiplier", - "ro.odm.build.media_performance_class", - "ro.sf.lcd_density", - "ro.treble.enabled", - "ro.vndk.version", - "ro.icu.data.path" -> true; - default -> false; - }; + if (sDefaultValues.containsKey(key)) { + return true; + } + if (checkAllowed(key, sReadableKeys)) { + return true; + } + // All writable keys are also readable + return isKeyWritable(key); } private static boolean isKeyWritable(String key) { - final String root = getKeyRoot(key); - - if (root.startsWith("debug.")) return true; - - // For PropertyInvalidatedCache - if (root.startsWith("cache_key.")) return true; - - return false; + return checkAllowed(key, sWritableKeys); } static boolean isKeyAccessible(String key, boolean write) { diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java index 19c1bffaebcd..3e2c4051b792 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java @@ -15,7 +15,20 @@ */ package android.platform.test.ravenwood; +import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.Looper; + import com.android.ravenwood.common.RavenwoodCommonUtils; +import com.android.ravenwood.common.SneakyThrow; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; /** * Utilities for writing (bivalent) ravenwood tests. @@ -47,4 +60,129 @@ public class RavenwoodUtils { public static void loadJniLibrary(String libname) { RavenwoodCommonUtils.loadJniLibrary(libname); } + + private class MainHandlerHolder { + static Handler sMainHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Returns the main thread handler. + */ + public static Handler getMainHandler() { + return MainHandlerHolder.sMainHandler; + } + + /** + * Run a Callable on Handler and wait for it to complete. + */ + @Nullable + public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) { + var result = new AtomicReference<T>(); + var thrown = new AtomicReference<Throwable>(); + var latch = new CountDownLatch(1); + h.post(() -> { + try { + result.set(c.call()); + } catch (Throwable th) { + thrown.set(th); + } + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while waiting on the Runnable", e); + } + var th = thrown.get(); + if (th != null) { + SneakyThrow.sneakyThrow(th); + } + return result.get(); + } + + + /** + * Run a Runnable on Handler and wait for it to complete. + */ + @Nullable + public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) { + runOnHandlerSync(h, () -> { + r.run(); + return null; + }); + } + + /** + * Run a Callable on main thread and wait for it to complete. + */ + @Nullable + public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) { + return runOnHandlerSync(getMainHandler(), c); + } + + /** + * Run a Runnable on main thread and wait for it to complete. + */ + @Nullable + public static void runOnMainThreadSync(@NonNull Runnable r) { + runOnHandlerSync(getMainHandler(), r); + } + + public static class MockitoHelper { + private MockitoHelper() { + } + + /** + * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different + * method on. (Maybe we should do it in Ravenizer.) + */ + public static void verifyZeroInteractions(Object... mocks) { + if (RavenwoodRule.isOnRavenwood()) { + // Mockito 4 or later + reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class) + .callStatic(new Object[]{mocks}); + } else { + // Mockito 2 + reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class) + .callStatic(new Object[]{mocks}); + } + } + } + + + /** + * Wrap the given {@link Supplier} to become memoized. + * + * The underlying {@link Supplier} will only be invoked once, and that result will be cached + * and returned for any future requests. + */ + static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) { + return new Supplier<>() { + private T mInstance; + + @Override + public T get() { + synchronized (this) { + if (mInstance == null) { + mInstance = create(); + } + return mInstance; + } + } + + private T create() { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + /** Used by {@link #memoize(ThrowingSupplier)} */ + public interface ThrowingSupplier<T> { + /** */ + T get() throws Exception; + } } diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index a967a3fff0d7..893b354d4645 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -26,10 +26,12 @@ import java.io.FileInputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Objects; import java.util.function.Supplier; public class RavenwoodCommonUtils { @@ -329,4 +331,70 @@ public class RavenwoodCommonUtils { public static <T> T withDefault(@Nullable T value, @Nullable T def) { return value != null ? value : def; } + + /** + * Utility for calling a method with reflections. Used to call a method by name. + * Note, this intentionally does _not_ support non-public methods, as we generally + * shouldn't violate java visibility in ravenwood. + * + * @param <TTHIS> class owning the method. + */ + public static class ReflectedMethod<TTHIS> { + private final Class<TTHIS> mThisClass; + private final Method mMethod; + + private ReflectedMethod(Class<TTHIS> thisClass, Method method) { + mThisClass = thisClass; + mMethod = method; + } + + /** Factory method. */ + @SuppressWarnings("unchecked") + public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod( + @NonNull Class<TTHIS> clazz, @NonNull String methodName, + @NonNull Class<?>... argTypes) { + try { + return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes)); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** Factory method. */ + @SuppressWarnings("unchecked") + public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod( + @NonNull String className, @NonNull String methodName, + @NonNull Class<?>... argTypes) { + try { + return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** Call the instance method */ + @SuppressWarnings("unchecked") + public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) { + try { + return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** Call the static method */ + @SuppressWarnings("unchecked") + public <RET> RET callStatic(@NonNull Object... args) { + try { + return (RET) mMethod.invoke(null, args); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + /** Handy method to create an array */ + public static <T> T[] arr(@NonNull T... objects) { + return objects; + } } diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java index 7ab9cda378b7..855a4ff21671 100644 --- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java +++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java @@ -21,7 +21,6 @@ import android.util.Log.Level; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.RuntimeInit; import com.android.ravenwood.RavenwoodRuntimeNative; -import com.android.ravenwood.common.RavenwoodCommonUtils; import java.io.PrintStream; import java.text.SimpleDateFormat; @@ -164,7 +163,7 @@ public class Log_ravenwood { * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so * that we don't end up in a recursive loop. */ - private static PrintStream getRealOut() { + public static PrintStream getRealOut() { if (RuntimeInit.sOut$ravenwood != null) { return RuntimeInit.sOut$ravenwood; } else { diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java index eaadac6a8b92..50cfd3bbe863 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java +++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java @@ -57,4 +57,12 @@ public class VMRuntime { public int getTargetSdkVersion() { return RavenwoodRuntimeState.sTargetSdkLevel; } + + /** Ignored on ravenwood. */ + public void registerNativeAllocation(long bytes) { + } + + /** Ignored on ravenwood. */ + public void registerNativeFree(long bytes) { + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java index cf1a5138cbc6..985e00e8641d 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java +++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java @@ -97,6 +97,9 @@ public class NativeAllocationRegistry { if (referent == null) { throw new IllegalArgumentException("referent is null"); } + if (mFreeFunction == 0) { + return () -> {}; // do nothing + } if (nativePtr == 0) { throw new IllegalArgumentException("nativePtr is null"); } diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh index 3e86037d7c7b..8c394f51d8c4 100755 --- a/ravenwood/scripts/add-annotations.sh +++ b/ravenwood/scripts/add-annotations.sh @@ -35,7 +35,7 @@ set -e # We add this line to each methods found. # Note, if we used a single @, that'd be handled as an at file. Use # the double-at instead. -annotation="@@android.platform.test.annotations.DisabledOnRavenwood" +annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")" while getopts "t:" opt; do case "$opt" in t) diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp index 9dd7cc683719..182a7cf3d3de 100644 --- a/ravenwood/tests/coretest/Android.bp +++ b/ravenwood/tests/coretest/Android.bp @@ -33,3 +33,34 @@ android_ravenwood_test { }, auto_gen_config: true, } + +// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib, +// because that modules has too many dependencies and slow to build incrementally. +android_ravenwood_test { + name: "RavenwoodCoreTest-light", + + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "androidx.test.rules", + + // This library should be removed by Ravenizer + "mockito-target-minus-junit4", + ], + libs: [ + // We access internal private classes + "ravenwood-junit-impl", + ], + srcs: [ + "test/**/*.java", + "test/**/*.kt", + ], + + exclude_srcs: [ + "test/com/android/ravenwoodtest/runnercallbacktests/*", + ], + ravenizer: { + strip_mockito: true, + }, + auto_gen_config: true, +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java new file mode 100644 index 000000000000..68387d76b675 --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java @@ -0,0 +1,82 @@ +/* + * 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.ravenwoodtest.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.platform.test.ravenwood.RavenwoodUtils; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +public class RavenwoodMainThreadTest { + private static final boolean RUN_UNSAFE_TESTS = + "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS")); + + @Test + public void testRunOnMainThread() { + AtomicReference<Thread> thr = new AtomicReference<>(); + RavenwoodUtils.runOnMainThreadSync(() -> { + thr.set(Thread.currentThread()); + }); + var th = thr.get(); + assertThat(th).isNotNull(); + assertThat(th).isNotEqualTo(Thread.currentThread()); + } + + /** + * Sleep a long time on the main thread. This test would then "pass", but Ravenwood + * should show the stack traces. + * + * This is "unsafe" because this test is slow. + */ + @Test + public void testUnsafeMainThreadHang() { + assumeTrue(RUN_UNSAFE_TESTS); + + // The test should time out. + RavenwoodUtils.runOnMainThreadSync(() -> { + try { + Thread.sleep(30_000); + } catch (InterruptedException e) { + fail("Interrupted"); + } + }); + } + + /** + * AssertionError on the main thread would be swallowed and reported "normally". + * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills + * the process) + * + * This is "unsafe" only because this feature can be disabled via the env var. + */ + @Test + public void testUnsafeAssertFailureOnMainThread() { + assumeTrue(RUN_UNSAFE_TESTS); + + assertThrows(AssertionError.class, () -> { + RavenwoodUtils.runOnMainThreadSync(() -> { + fail(); + }); + }); + } +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java new file mode 100644 index 000000000000..421fb50e0c9a --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java @@ -0,0 +1,63 @@ +/* + * 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.ravenwoodtest.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod; + +import org.junit.Test; + +/** + * Tests for {@link ReflectedMethod}. + */ +public class RavenwoodReflectorTest { + /** test target */ + public class Target { + private final int mVar; + + /** test target */ + public Target(int var) { + mVar = var; + } + + /** test target */ + public int foo(int x) { + return x + mVar; + } + + /** test target */ + public static int bar(int x) { + return x + 1; + } + } + + /** Test for a non-static method call */ + @Test + public void testNonStatic() { + var obj = new Target(5); + + var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class); + assertThat((int) m.call(obj, 2)).isEqualTo(7); + } + + /** Test for a static method call */ + @Test + public void testStatic() { + var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class); + assertThat((int) m.callStatic(1)).isEqualTo(2); + } +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java new file mode 100644 index 000000000000..454f5a9576d9 --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java @@ -0,0 +1,65 @@ +/* + * 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.ravenwoodtest.coretest; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.os.SystemProperties; + +import org.junit.Test; + +public class RavenwoodSystemPropertiesTest { + @Test + public void testRead() { + assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1"); + } + + @Test + public void testWrite() { + SystemProperties.set("debug.xxx", "5"); + assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5"); + } + + private static void assertException(String expectedMessage, Runnable r) { + try { + r.run(); + fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown"); + } catch (RuntimeException e) { + if (e.getMessage().contains(expectedMessage)) { + return; + } + fail("Excepted exception with message '" + expectedMessage + "' but was '" + + e.getMessage() + "'"); + } + } + + + @Test + public void testReadDisallowed() { + assertException("Read access to system property 'nonexisitent' denied", () -> { + SystemProperties.get("nonexisitent"); + }); + } + + @Test + public void testWriteDisallowed() { + assertException("failed to set system property \"ro.board.first_api_level\" ", () -> { + SystemProperties.set("ro.board.first_api_level", "2"); + }); + } +} diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java index 30abaa2e7d38..b1a40f082656 100644 --- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java +++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java @@ -16,28 +16,27 @@ package com.android.ravenwoodtest; import android.platform.test.annotations.IgnoreUnderRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Assert; -import org.junit.Rule; +import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RavenwoodMinimumTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() - .setProcessApp() - .build(); - @Test public void testSimple() { Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid())); } @Test + public void testAssumeNot() { + Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid())); + } + + @Test @IgnoreUnderRavenwood public void testIgnored() { throw new RuntimeException("Shouldn't be executed under ravenwood"); diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop index 37c50f11f73f..512b459113da 100644 --- a/ravenwood/texts/ravenwood-build.prop +++ b/ravenwood/texts/ravenwood-build.prop @@ -8,9 +8,41 @@ ro.soc.manufacturer=Android ro.soc.model=Ravenwood ro.debuggable=1 -# For the graphics stack -ro.hwui.max_texture_allocation_size=104857600 persist.sys.locale=en-US +ro.product.locale=en-US + +ro.hwui.max_texture_allocation_size=104857600 + +# Allowlist control: +# This set is carefully curated to help identify situations where a test may +# accidentally depend on a default value of an obscure property whose owner hasn't +# decided how Ravenwood should behave. + +boot.=$$$r +build.=$$$r +product.=$$$r +soc.=$$$r +system.=$$$r +wm.debug.=$$$r +wm.extensions.=$$$r + +gsm.version.baseband=$$$r +no.such.thing=$$$r +qemu.sf.lcd_density=$$$r +ro.bootloader=$$$r +ro.hardware=$$$r +ro.hw_timeout_multiplier=$$$r +ro.odm.build.media_performance_class=$$$r +ro.sf.lcd_density=$$$r +ro.treble.enabled=$$$r +ro.vndk.version=$$$r +ro.icu.data.path=$$$r + +# Writable keys +debug.=$$$w + +# For PropertyInvalidatedCache +cache_key.=$$$w # The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too. # See RavenwoodSystemProperties. diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt index 8fdd3408f74d..64a0e2548e2e 100644 --- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt +++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt @@ -5,7 +5,7 @@ rule com.android.server.pm.pkg.AndroidPackageSplit @0 # Rename all other service internals so that tests can continue to statically # link services code when owners aren't ready to support on Ravenwood -rule com.android.server.** repackaged.@0 +rule com.android.server.** repackaged.services.@0 # TODO: support AIDL generated Parcelables via hoststubgen -rule android.hardware.power.stats.** repackaged.@0 +rule android.hardware.power.stats.** repackaged.services.@0 diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index bd34f33226a1..c182c2618fdf 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -149,7 +149,6 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba OperationStorage mOperationStorage; List<PackageInfo> mPackages; - PackageInfo mCurrentPackage; boolean mUpdateSchedule; CountDownLatch mLatch; FullBackupJob mJob; // if a scheduled job needs to be finished afterwards @@ -207,10 +206,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba for (String pkg : whichPackages) { try { PackageManager pm = backupManagerService.getPackageManager(); - PackageInfo info = pm.getPackageInfoAsUser(pkg, + PackageInfo packageInfo = pm.getPackageInfoAsUser(pkg, PackageManager.GET_SIGNING_CERTIFICATES, mUserId); - mCurrentPackage = info; - if (!mBackupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) { + if (!mBackupEligibilityRules.appIsEligibleForBackup(packageInfo.applicationInfo)) { // Cull any packages that have indicated that backups are not permitted, // that run as system-domain uids but do not define their own backup agents, // as well as any explicit mention of the 'special' shared-storage agent @@ -220,13 +218,13 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE, - mCurrentPackage, + packageInfo, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + /* extras= */ null); BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg, BackupManager.ERROR_BACKUP_NOT_ALLOWED); continue; - } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) { + } else if (!mBackupEligibilityRules.appGetsFullBackup(packageInfo)) { // Cull any packages that are found in the queue but now aren't supposed // to get full-data backup operations. if (DEBUG) { @@ -235,13 +233,13 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT, - mCurrentPackage, + packageInfo, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + /* extras= */ null); BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg, BackupManager.ERROR_BACKUP_NOT_ALLOWED); continue; - } else if (mBackupEligibilityRules.appIsStopped(info.applicationInfo)) { + } else if (mBackupEligibilityRules.appIsStopped(packageInfo.applicationInfo)) { // Cull any packages in the 'stopped' state: they've either just been // installed or have explicitly been force-stopped by the user. In both // cases we do not want to launch them for backup. @@ -250,21 +248,21 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED, - mCurrentPackage, + packageInfo, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + /* extras= */ null); BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg, BackupManager.ERROR_BACKUP_NOT_ALLOWED); continue; } - mPackages.add(info); + mPackages.add(packageInfo); } catch (NameNotFoundException e) { Slog.i(TAG, "Requested package " + pkg + " not found; ignoring"); mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND, - mCurrentPackage, + /* pkg= */ null, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + /* extras= */ null); } } @@ -352,10 +350,11 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } else { monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED; } - mBackupManagerMonitorEventSender - .monitorEvent(monitoringEvent, null, - BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); + mBackupManagerMonitorEventSender.monitorEvent( + monitoringEvent, + /* pkg= */ null, + BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + /* extras= */ null); mUpdateSchedule = false; backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED; return; @@ -367,8 +366,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED; mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT, - mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, - null); + /* pkg= */ null, + BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, + /* extras= */ null); return; } @@ -461,9 +461,10 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT, - mCurrentPackage, + currentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - mBackupManagerMonitorEventSender.putMonitoringExtra(null, + BackupManagerMonitorEventSender.putMonitoringExtra( + /* extras= */ null, BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR, preflightResult)); backupPackageStatus = (int) preflightResult; @@ -496,9 +497,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba + ": " + totalRead + " of " + quota); mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT, - mCurrentPackage, + currentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, - null); + /* extras= */ null); mBackupRunner.sendQuotaExceeded(totalRead, quota); } } @@ -645,9 +646,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba Slog.w(TAG, "Exception trying full transport backup", e); mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP, - mCurrentPackage, + /* pkg= */ null, BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - mBackupManagerMonitorEventSender.putMonitoringExtra(null, + BackupManagerMonitorEventSender.putMonitoringExtra(/* extras= */ null, BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP, Log.getStackTraceString(e))); @@ -966,9 +967,6 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } } - - // BackupRestoreTask interface: specifically, timeout detection - @Override public void execute() { /* intentionally empty */ } @@ -981,7 +979,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL, - mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null); + mTarget, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + /* extras= */ null); mIsCancelled = true; // Cancel tasks spun off by this task. mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll); diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java index c4519b1173eb..33668a6d5314 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java @@ -71,6 +71,7 @@ public class BackupManagerMonitorEventSender { mMonitor = monitor; } + @Nullable public IBackupManagerMonitor getMonitor() { return mMonitor; } @@ -87,9 +88,9 @@ public class BackupManagerMonitorEventSender { */ public void monitorEvent( int id, - PackageInfo pkg, + @Nullable PackageInfo pkg, int category, - Bundle extras) { + @Nullable Bundle extras) { try { Bundle bundle = new Bundle(); bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index b6fe0ad37078..e46bbe2871cd 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -160,6 +160,7 @@ import com.android.server.memory.ZramMaintenance; import com.android.server.pm.Installer; import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; +import com.android.server.storage.ImmutableVolumeInfo; import com.android.server.storage.StorageSessionController; import com.android.server.storage.StorageSessionController.ExternalStorageServiceException; import com.android.server.storage.WatchedVolumeInfo; @@ -777,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub break; } case H_VOLUME_UNMOUNT: { - final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj; + final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj; unmount(vol); break; } @@ -898,8 +899,14 @@ class StorageManagerService extends IStorageManager.Stub for (int i = 0; i < size; i++) { final WatchedVolumeInfo vol = mVolumes.valueAt(i); if (vol.getMountUserId() == userId) { + // Capture the volume before we set mount user id to null, + // so that StorageSessionController remove the session from + // the correct user (old mount user id) + final ImmutableVolumeInfo volToUnmount + = vol.getClonedImmutableVolumeInfo(); vol.setMountUserId(UserHandle.USER_NULL); - mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); + mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount) + .sendToTarget(); } } } @@ -1295,7 +1302,12 @@ class StorageManagerService extends IStorageManager.Stub } private void maybeRemountVolumes(int userId) { - List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>(); + // We need to keep 2 lists + // 1. List of volumes before we set the mount user Id so that + // StorageSessionController is able to remove the session from the correct user (old one) + // 2. List of volumes to mount which should have the up to date info + List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>(); + List<WatchedVolumeInfo> volumesToMount = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final WatchedVolumeInfo vol = mVolumes.valueAt(i); @@ -1303,16 +1315,19 @@ class StorageManagerService extends IStorageManager.Stub && vol.getMountUserId() != mCurrentUserId) { // If there's a visible secondary volume mounted, // we need to update the currentUserId and remount + // But capture the volume with the old user id first to use it in unmounting + volumesToUnmount.add(vol.getClonedImmutableVolumeInfo()); vol.setMountUserId(mCurrentUserId); - volumesToRemount.add(vol); + volumesToMount.add(vol); } } } - for (WatchedVolumeInfo vol : volumesToRemount) { - Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol); - mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget(); - mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget(); + for (int i = 0; i < volumesToMount.size(); i++) { + Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + + volumesToUnmount.get(i)); + mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget(); + mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget(); } } @@ -2430,10 +2445,10 @@ class StorageManagerService extends IStorageManager.Stub super.unmount_enforcePermission(); final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId); - unmount(vol); + unmount(vol.getClonedImmutableVolumeInfo()); } - private void unmount(WatchedVolumeInfo vol) { + private void unmount(ImmutableVolumeInfo vol) { try { try { if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { @@ -2444,7 +2459,7 @@ class StorageManagerService extends IStorageManager.Stub } extendWatchdogTimeout("#unmount might be slow"); mVold.unmount(vol.getId()); - mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo()); + mStorageSessionController.onVolumeUnmount(vol); } catch (Exception e) { Slog.wtf(TAG, e); } diff --git a/services/core/java/com/android/server/SystemTimeZone.java b/services/core/java/com/android/server/SystemTimeZone.java index dd07081bda12..c8810f672320 100644 --- a/services/core/java/com/android/server/SystemTimeZone.java +++ b/services/core/java/com/android/server/SystemTimeZone.java @@ -133,6 +133,7 @@ public final class SystemTimeZone { boolean timeZoneChanged = false; synchronized (SystemTimeZone.class) { String currentTimeZoneId = getTimeZoneId(); + @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence(); if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) { SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId); if (DEBUG) { @@ -145,6 +146,8 @@ public final class SystemTimeZone { String logMsg = "Time zone or confidence set: " + " (new) timeZoneId=" + timeZoneId + ", (new) confidence=" + confidence + + ", (old) timeZoneId=" + currentTimeZoneId + + ", (old) confidence=" + currentConfidence + ", logInfo=" + logInfo; addDebugLogEntry(logMsg); } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index bd7a0ac55117..b75b7ddf8181 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -2816,13 +2816,11 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (!checkNotifyPermission("notifyEmergencyNumberList()")) { return; } - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - if (!mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TELEPHONY_CALLING)) { - // TelephonyManager.getEmergencyNumberList() throws an exception if - // FEATURE_TELEPHONY_CALLING is not defined. - return; - } + if (!mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_CALLING)) { + // TelephonyManager.getEmergencyNumberList() throws an exception if + // FEATURE_TELEPHONY_CALLING is not defined. + return; } synchronized (mRecords) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 8b701f0e2069..b0b34d0ab9c4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19471,7 +19471,7 @@ public class ActivityManagerService extends IActivityManager.Stub /** * @hide */ - @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL") + @EnforcePermission(INTERACT_ACROSS_USERS_FULL) public IBinder refreshIntentCreatorToken(Intent intent) { refreshIntentCreatorToken_enforcePermission(); IBinder binder = intent.getCreatorToken(); diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 36035bdcddbc..78beb18263a7 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -832,7 +832,9 @@ class BroadcastQueueImpl extends BroadcastQueue { // If this receiver is going to be skipped, skip it now itself and don't even enqueue // it. - final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver); + final String skipReason = Flags.avoidNoteOpAtEnqueue() + ? mSkipPolicy.shouldSkipAtEnqueueMessage(r, receiver) + : mSkipPolicy.shouldSkipMessage(r, receiver); if (skipReason != null) { setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED, "skipped by policy at enqueue: " + skipReason); diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index d2af84cf3d30..b0d5994cc60b 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -71,10 +71,20 @@ public class BroadcastSkipPolicy { * {@code null} if it can proceed. */ public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) { + return shouldSkipMessage(r, target, false /* preflight */); + } + + public @Nullable String shouldSkipAtEnqueueMessage(@NonNull BroadcastRecord r, + @NonNull Object target) { + return shouldSkipMessage(r, target, true /* preflight */); + } + + private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target, + boolean preflight) { if (target instanceof BroadcastFilter) { - return shouldSkipMessage(r, (BroadcastFilter) target); + return shouldSkipMessage(r, (BroadcastFilter) target, preflight); } else { - return shouldSkipMessage(r, (ResolveInfo) target); + return shouldSkipMessage(r, (ResolveInfo) target, preflight); } } @@ -86,7 +96,7 @@ public class BroadcastSkipPolicy { * {@code null} if it can proceed. */ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, - @NonNull ResolveInfo info) { + @NonNull ResolveInfo info, boolean preflight) { final BroadcastOptions brOptions = r.options; final ComponentName component = new ComponentName( info.activityInfo.applicationInfo.packageName, @@ -134,15 +144,23 @@ public class BroadcastSkipPolicy { + " requires " + info.activityInfo.permission; } } else if (info.activityInfo.permission != null) { - final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission); - if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode, - r.callingUid, r.callerPackage, r.callerFeatureId, - "Broadcast delivered to " + info.activityInfo.name) - != AppOpsManager.MODE_ALLOWED) { - return "Appop Denial: broadcasting " - + broadcastDescription(r, component) - + " requires appop " + AppOpsManager.permissionToOp( - info.activityInfo.permission); + final String op = AppOpsManager.permissionToOp(info.activityInfo.permission); + if (op != null) { + final int mode; + if (preflight) { + mode = mService.getAppOpsManager().checkOpNoThrow(op, + r.callingUid, r.callerPackage, r.callerFeatureId); + } else { + mode = mService.getAppOpsManager().noteOpNoThrow(op, + r.callingUid, r.callerPackage, r.callerFeatureId, + "Broadcast delivered to " + info.activityInfo.name); + } + if (mode != AppOpsManager.MODE_ALLOWED) { + return "Appop Denial: broadcasting " + + broadcastDescription(r, component) + + " requires appop " + AppOpsManager.permissionToOp( + info.activityInfo.permission); + } } } @@ -250,8 +268,8 @@ public class BroadcastSkipPolicy { perm = PackageManager.PERMISSION_DENIED; } - int appOp = AppOpsManager.permissionToOpCode(excludedPermission); - if (appOp != AppOpsManager.OP_NONE) { + final String appOp = AppOpsManager.permissionToOp(excludedPermission); + if (appOp != null) { // When there is an app op associated with the permission, // skip when both the permission and the app op are // granted. @@ -259,7 +277,7 @@ public class BroadcastSkipPolicy { mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid, info.activityInfo.packageName) - == AppOpsManager.MODE_ALLOWED)) { + == AppOpsManager.MODE_ALLOWED)) { return "Skipping delivery to " + info.activityInfo.packageName + " due to excluded permission " + excludedPermission; } @@ -292,9 +310,10 @@ public class BroadcastSkipPolicy { createAttributionSourcesForResolveInfo(info); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - perm = hasPermissionForDataDelivery( + perm = hasPermission( requiredPermission, "Broadcast delivered to " + info.activityInfo.name, + preflight, attributionSources) ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED; @@ -308,10 +327,14 @@ public class BroadcastSkipPolicy { } } } - if (r.appOp != AppOpsManager.OP_NONE) { - if (!noteOpForManifestReceiver(r.appOp, r, info, component)) { + if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) { + final String op = AppOpsManager.opToPublicName(r.appOp); + final boolean appOpAllowed = preflight + ? checkOpForManifestReceiver(r.appOp, op, r, info, component) + : noteOpForManifestReceiver(r.appOp, op, r, info, component); + if (!appOpAllowed) { return "Skipping delivery to " + info.activityInfo.packageName - + " due to required appop " + r.appOp; + + " due to required appop " + AppOpsManager.opToName(r.appOp); } } @@ -338,7 +361,7 @@ public class BroadcastSkipPolicy { * {@code null} if it can proceed. */ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, - @NonNull BroadcastFilter filter) { + @NonNull BroadcastFilter filter, boolean preflight) { if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) { return "Compat change filtered: broadcasting " + r.intent.toString() + " to uid " + filter.owningUid + " due to compat change " @@ -372,18 +395,25 @@ public class BroadcastSkipPolicy { + " requires " + filter.requiredPermission + " due to registered receiver " + filter; } else { - final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission); - if (opCode != AppOpsManager.OP_NONE - && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid, - r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver") - != AppOpsManager.MODE_ALLOWED) { - return "Appop Denial: broadcasting " - + r.intent.toString() - + " from " + r.callerPackage + " (pid=" - + r.callingPid + ", uid=" + r.callingUid + ")" - + " requires appop " + AppOpsManager.permissionToOp( - filter.requiredPermission) - + " due to registered receiver " + filter; + final String op = AppOpsManager.permissionToOp(filter.requiredPermission); + if (op != null) { + final int mode; + if (preflight) { + mode = mService.getAppOpsManager().checkOpNoThrow(op, + r.callingUid, r.callerPackage, r.callerFeatureId); + } else { + mode = mService.getAppOpsManager().noteOpNoThrow(op, r.callingUid, + r.callerPackage, r.callerFeatureId, + "Broadcast sent to protected receiver"); + } + if (mode != AppOpsManager.MODE_ALLOWED) { + return "Appop Denial: broadcasting " + + r.intent + + " from " + r.callerPackage + " (pid=" + + r.callingPid + ", uid=" + r.callingUid + ")" + + " requires appop " + op + + " due to registered receiver " + filter; + } } } } @@ -433,9 +463,10 @@ public class BroadcastSkipPolicy { .build(); for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; - final int perm = hasPermissionForDataDelivery( + final int perm = hasPermission( requiredPermission, "Broadcast delivered to registered receiver " + filter.receiverId, + preflight, attributionSource) ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED; @@ -471,8 +502,8 @@ public class BroadcastSkipPolicy { final int perm = checkComponentPermission(excludedPermission, filter.receiverList.pid, filter.receiverList.uid, -1, true); - int appOp = AppOpsManager.permissionToOpCode(excludedPermission); - if (appOp != AppOpsManager.OP_NONE) { + final String appOp = AppOpsManager.permissionToOp(excludedPermission); + if (appOp != null) { // When there is an app op associated with the permission, // skip when both the permission and the app op are // granted. @@ -480,14 +511,13 @@ public class BroadcastSkipPolicy { mService.getAppOpsManager().checkOpNoThrow(appOp, filter.receiverList.uid, filter.packageName) - == AppOpsManager.MODE_ALLOWED)) { + == AppOpsManager.MODE_ALLOWED)) { return "Appop Denial: receiving " - + r.intent.toString() + + r.intent + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" - + " excludes appop " + AppOpsManager.permissionToOp( - excludedPermission) + + " excludes appop " + appOp + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } @@ -496,7 +526,7 @@ public class BroadcastSkipPolicy { // skip when permission is granted. if (perm == PackageManager.PERMISSION_GRANTED) { return "Permission Denial: receiving " - + r.intent.toString() + + r.intent + " to " + filter.receiverList.app + " (pid=" + filter.receiverList.pid + ", uid=" + filter.receiverList.uid + ")" @@ -523,19 +553,27 @@ public class BroadcastSkipPolicy { } // If the broadcast also requires an app op check that as well. - if (r.appOp != AppOpsManager.OP_NONE - && mService.getAppOpsManager().noteOpNoThrow(r.appOp, - filter.receiverList.uid, filter.packageName, filter.featureId, - "Broadcast delivered to registered receiver " + filter.receiverId) - != AppOpsManager.MODE_ALLOWED) { - return "Appop Denial: receiving " - + r.intent.toString() - + " to " + filter.receiverList.app - + " (pid=" + filter.receiverList.pid - + ", uid=" + filter.receiverList.uid + ")" - + " requires appop " + AppOpsManager.opToName(r.appOp) - + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"; + if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) { + final String op = AppOpsManager.opToPublicName(r.appOp); + final int mode; + if (preflight) { + mode = mService.getAppOpsManager().checkOpNoThrow(op, + filter.receiverList.uid, filter.packageName, filter.featureId); + } else { + mode = mService.getAppOpsManager().noteOpNoThrow(op, + filter.receiverList.uid, filter.packageName, filter.featureId, + "Broadcast delivered to registered receiver " + filter.receiverId); + } + if (mode != AppOpsManager.MODE_ALLOWED) { + return "Appop Denial: receiving " + + r.intent + + " to " + filter.receiverList.app + + " (pid=" + filter.receiverList.pid + + ", uid=" + filter.receiverList.uid + ")" + + " requires appop " + AppOpsManager.opToName(r.appOp) + + " due to sender " + r.callerPackage + + " (uid " + r.callingUid + ")"; + } } // Ensure that broadcasts are only sent to other apps if they are explicitly marked as @@ -572,14 +610,14 @@ public class BroadcastSkipPolicy { + ", uid=" + r.callingUid + ") to " + component.flattenToShortString(); } - private boolean noteOpForManifestReceiver(int appOp, BroadcastRecord r, ResolveInfo info, - ComponentName component) { + private boolean noteOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r, + ResolveInfo info, ComponentName component) { if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) { - return noteOpForManifestReceiverInner(appOp, r, info, component, null); + return noteOpForManifestReceiverInner(opCode, appOp, r, info, component, null); } else { // Attribution tags provided, noteOp each tag for (String tag : info.activityInfo.attributionTags) { - if (!noteOpForManifestReceiverInner(appOp, r, info, component, tag)) { + if (!noteOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) { return false; } } @@ -587,8 +625,8 @@ public class BroadcastSkipPolicy { } } - private boolean noteOpForManifestReceiverInner(int appOp, BroadcastRecord r, ResolveInfo info, - ComponentName component, String tag) { + private boolean noteOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r, + ResolveInfo info, ComponentName component, String tag) { if (mService.getAppOpsManager().noteOpNoThrow(appOp, info.activityInfo.applicationInfo.uid, info.activityInfo.packageName, @@ -598,7 +636,37 @@ public class BroadcastSkipPolicy { Slog.w(TAG, "Appop Denial: receiving " + r.intent + " to " + component.flattenToShortString() - + " requires appop " + AppOpsManager.opToName(appOp) + + " requires appop " + AppOpsManager.opToName(opCode) + + " due to sender " + r.callerPackage + + " (uid " + r.callingUid + ")"); + return false; + } + return true; + } + + private boolean checkOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r, + ResolveInfo info, ComponentName component) { + if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) { + return checkOpForManifestReceiverInner(opCode, appOp, r, info, component, null); + } else { + // Attribution tags provided, noteOp each tag + for (String tag : info.activityInfo.attributionTags) { + if (!checkOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) { + return false; + } + } + return true; + } + } + + private boolean checkOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r, + ResolveInfo info, ComponentName component, String tag) { + if (mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid, + info.activityInfo.packageName, tag) != AppOpsManager.MODE_ALLOWED) { + Slog.w(TAG, "Appop Denial: receiving " + + r.intent + " to " + + component.flattenToShortString() + + " requires appop " + AppOpsManager.opToName(opCode) + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"); return false; @@ -694,9 +762,10 @@ public class BroadcastSkipPolicy { return mPermissionManager; } - private boolean hasPermissionForDataDelivery( + private boolean hasPermission( @NonNull String permission, @NonNull String message, + boolean preflight, @NonNull AttributionSource... attributionSources) { final PermissionManager permissionManager = getPermissionManager(); if (permissionManager == null) { @@ -704,9 +773,14 @@ public class BroadcastSkipPolicy { } for (AttributionSource attributionSource : attributionSources) { - final int permissionCheckResult = - permissionManager.checkPermissionForDataDelivery( - permission, attributionSource, message); + final int permissionCheckResult; + if (preflight) { + permissionCheckResult = permissionManager.checkPermissionForPreflight( + permission, attributionSource); + } else { + permissionCheckResult = permissionManager.checkPermissionForDataDelivery( + permission, attributionSource, message); + } if (permissionCheckResult != PackageManager.PERMISSION_GRANTED) { return false; } diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig index 7f169db7dcec..68e21a35a531 100644 --- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig +++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig @@ -15,4 +15,15 @@ flag { description: "Limit the scope of receiver priorities to within a process" is_fixed_read_only: true bug: "369487976" +} + +flag { + name: "avoid_note_op_at_enqueue" + namespace: "backstage_power" + description: "Avoid triggering noteOp while enqueueing a broadcast" + is_fixed_read_only: true + bug: "268016162" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3cb2125f7820..0f1228f44b0d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -1164,9 +1164,11 @@ public class AudioService extends IAudioService.Stub @GuardedBy("mAccessibilityServiceUidsLock") private int[] mAccessibilityServiceUids; + // Input Method + private final Object mInputMethodServiceUidLock = new Object(); // Uid of the active input method service to check if caller is the one or not. + @GuardedBy("mInputMethodServiceUidLock") private int mInputMethodServiceUid = android.os.Process.INVALID_UID; - private final Object mInputMethodServiceUidLock = new Object(); private int mEncodedSurroundMode; private String mEnabledSurroundFormats; @@ -11405,7 +11407,7 @@ public class AudioService extends IAudioService.Stub /** see {@link AudioManager#getFocusDuckedUidsForTest()} */ @Override - @EnforcePermission("android.permission.QUERY_AUDIO_STATE") + @EnforcePermission(QUERY_AUDIO_STATE) public @NonNull List<Integer> getFocusDuckedUidsForTest() { super.getFocusDuckedUidsForTest_enforcePermission(); return mPlaybackMonitor.getFocusDuckedUids(); @@ -11432,7 +11434,7 @@ public class AudioService extends IAudioService.Stub * @see AudioManager#getFocusFadeOutDurationForTest() * @return the fade out duration, in ms */ - @EnforcePermission("android.permission.QUERY_AUDIO_STATE") + @EnforcePermission(QUERY_AUDIO_STATE) public long getFocusFadeOutDurationForTest() { super.getFocusFadeOutDurationForTest_enforcePermission(); return mMediaFocusControl.getFocusFadeOutDurationForTest(); @@ -11445,7 +11447,7 @@ public class AudioService extends IAudioService.Stub * @return the time gap after a fade out completion on focus loss, and fade in start, in ms */ @Override - @EnforcePermission("android.permission.QUERY_AUDIO_STATE") + @EnforcePermission(QUERY_AUDIO_STATE) public long getFocusUnmuteDelayAfterFadeOutForTest() { super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission(); return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest(); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 940bcb4c6ba1..f40d0dd18213 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -43,7 +43,10 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import java.util.Collection; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; /** * A class that represents a broker for the endpoint registered by the client app. This class @@ -111,6 +114,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final boolean mRemoteInitiated; + /** + * The set of seq # for pending reliable messages started by this endpoint for this session. + */ + private final Set<Integer> mPendingSequenceNumbers = new HashSet<>(); + SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) { mRemoteEndpointInfo = remoteEndpointInfo; mRemoteInitiated = remoteInitiated; @@ -131,6 +139,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub public boolean isActive() { return mSessionState == SessionState.ACTIVE; } + + public boolean isReliableMessagePending(int sequenceNumber) { + return mPendingSequenceNumbers.contains(sequenceNumber); + } + + public void setReliableMessagePending(int sequenceNumber) { + mPendingSequenceNumbers.add(sequenceNumber); + } + + public void setReliableMessageCompleted(int sequenceNumber) { + mPendingSequenceNumbers.remove(sequenceNumber); + } + + public void forEachPendingReliableMessage(Consumer<Integer> consumer) { + for (int sequenceNumber : mPendingSequenceNumbers) { + consumer.accept(sequenceNumber); + } + } } /** A map between a session ID which maps to its current state. */ @@ -208,10 +234,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub try { mSessionInfoMap.put(sessionId, new SessionInfo(destination, false)); mHubInterface.openEndpointSession( - sessionId, - halEndpointInfo.id, - mHalEndpointInfo.id, - serviceDescriptor); + sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor); } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) { Log.e(TAG, "Exception while calling HAL openEndpointSession", e); cleanupSessionResources(sessionId); @@ -286,34 +309,42 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub public void sendMessage( int sessionId, HubMessage message, IContextHubTransactionCallback callback) { super.sendMessage_enforcePermission(); - Message halMessage = ContextHubServiceUtil.createHalMessage(message); - if (!isSessionActive(sessionId)) { - throw new SecurityException( - "sendMessage called on inactive session (id= " + sessionId + ")"); - } - - if (callback == null) { - try { - mHubInterface.sendMessageToEndpoint(sessionId, halMessage); - } catch (RemoteException e) { - Log.w(TAG, "Exception while sending message on session " + sessionId, e); + synchronized (mOpenSessionLock) { + SessionInfo info = mSessionInfoMap.get(sessionId); + if (info == null) { + throw new IllegalArgumentException( + "sendMessage for invalid session id=" + sessionId); } - } else { - ContextHubServiceTransaction transaction = - mTransactionManager.createSessionMessageTransaction( - mHubInterface, sessionId, halMessage, mPackageName, callback); - try { - mTransactionManager.addTransaction(transaction); - } catch (IllegalStateException e) { - Log.e( - TAG, - "Unable to add a transaction in sendMessageToEndpoint " - + "(session ID = " - + sessionId - + ")", - e); - transaction.onTransactionComplete( - ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE); + if (!info.isActive()) { + throw new SecurityException( + "sendMessage called on inactive session (id= " + sessionId + ")"); + } + + Message halMessage = ContextHubServiceUtil.createHalMessage(message); + if (callback == null) { + try { + mHubInterface.sendMessageToEndpoint(sessionId, halMessage); + } catch (RemoteException e) { + Log.w(TAG, "Exception while sending message on session " + sessionId, e); + } + } else { + ContextHubServiceTransaction transaction = + mTransactionManager.createSessionMessageTransaction( + mHubInterface, sessionId, halMessage, mPackageName, callback); + try { + mTransactionManager.addTransaction(transaction); + info.setReliableMessagePending(transaction.getMessageSequenceNumber()); + } catch (IllegalStateException e) { + Log.e( + TAG, + "Unable to add a transaction in sendMessageToEndpoint " + + "(session ID = " + + sessionId + + ")", + e); + transaction.onTransactionComplete( + ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE); + } } } } @@ -393,7 +424,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub int id = mSessionInfoMap.keyAt(i); int count = i + 1; sb.append( - " " + count + ". id=" + " " + + count + + ". id=" + id + ", remote:" + mSessionInfoMap.get(id).getRemoteEndpointInfo()); @@ -461,13 +494,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /* package */ void onMessageReceived(int sessionId, HubMessage message) { byte code = onMessageReceivedInternal(sessionId, message); if (code != ErrorCode.OK && message.isResponseRequired()) { - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), code); + sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code); } } /* package */ void onMessageDeliveryStatusReceived( int sessionId, int sequenceNumber, byte errorCode) { + synchronized (mOpenSessionLock) { + SessionInfo info = mSessionInfoMap.get(sessionId); + if (info == null || !info.isActive()) { + Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId); + return; + } + if (!info.isReliableMessagePending(sequenceNumber)) { + Log.w(TAG, "Received delivery status for unknown seq: " + sequenceNumber); + return; + } + info.setReliableMessageCompleted(sequenceNumber); + } mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); } @@ -492,7 +536,6 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub onCloseEndpointSession(id, Reason.HUB_RESET); } } - // TODO(b/390029594): Cancel any ongoing reliable communication transactions } private Optional<Byte> onEndpointSessionOpenRequestInternal( @@ -515,9 +558,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - boolean success = invokeCallback( - (consumer) -> - consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor)); + boolean success = + invokeCallback( + (consumer) -> + consumer.onSessionOpenRequest( + sessionId, initiator, serviceDescriptor)); return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED); } @@ -590,8 +635,15 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private boolean cleanupSessionResources(int sessionId) { synchronized (mOpenSessionLock) { SessionInfo info = mSessionInfoMap.get(sessionId); - if (info != null && !info.isRemoteInitiated()) { - mEndpointManager.returnSessionId(sessionId); + if (info != null) { + if (!info.isRemoteInitiated()) { + mEndpointManager.returnSessionId(sessionId); + } + info.forEachPendingReliableMessage( + (sequenceNumber) -> { + mTransactionManager.onMessageDeliveryResponse( + sequenceNumber, /* success= */ false); + }); mSessionInfoMap.remove(sessionId); } return info != null; @@ -646,10 +698,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub try { mWakeLock.release(); } catch (RuntimeException e) { - Log.e( - TAG, - "Releasing the wakelock for all acquisitions fails - ", - e); + Log.e(TAG, "Releasing the wakelock for all acquisitions fails - ", e); break; } } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java index a430a82fc13b..6a1db0223db5 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java @@ -29,6 +29,7 @@ import android.os.SystemClock; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.time.Duration; import java.util.ArrayDeque; @@ -165,52 +166,61 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Creates a transaction for loading a nanoapp. * - * @param contextHubId the ID of the hub to load the nanoapp to - * @param nanoAppBinary the binary of the nanoapp to load + * @param contextHubId the ID of the hub to load the nanoapp to + * @param nanoAppBinary the binary of the nanoapp to load * @param onCompleteCallback the client on complete callback * @return the generated transaction */ /* package */ ContextHubServiceTransaction createLoadTransaction( - int contextHubId, NanoAppBinary nanoAppBinary, - IContextHubTransactionCallback onCompleteCallback, String packageName) { + int contextHubId, + NanoAppBinary nanoAppBinary, + IContextHubTransactionCallback onCompleteCallback, + String packageName) { return new ContextHubServiceTransaction( - mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP, - nanoAppBinary.getNanoAppId(), packageName) { + mNextAvailableId.getAndIncrement(), + ContextHubTransaction.TYPE_LOAD_NANOAPP, + nanoAppBinary.getNanoAppId(), + packageName) { @Override - /* package */ int onTransact() { + /* package */ int onTransact() { try { return mContextHubProxy.loadNanoapp( contextHubId, nanoAppBinary, this.getTransactionId()); } catch (RemoteException e) { - Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" + - Long.toHexString(nanoAppBinary.getNanoAppId()), e); + Log.e( + TAG, + "RemoteException while trying to load nanoapp with ID 0x" + + Long.toHexString(nanoAppBinary.getNanoAppId()), + e); return ContextHubTransaction.RESULT_FAILED_UNKNOWN; } } @Override - /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { + /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { ContextHubStatsLog.write( ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppBinary.getNanoAppId(), nanoAppBinary.getNanoAppVersion(), ContextHubStatsLog - .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD, + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD, toStatsTransactionResult(result)); - ContextHubEventLogger.getInstance().logNanoappLoad( - contextHubId, - nanoAppBinary.getNanoAppId(), - nanoAppBinary.getNanoAppVersion(), - nanoAppBinary.getBinary().length, - result == ContextHubTransaction.RESULT_SUCCESS); + ContextHubEventLogger.getInstance() + .logNanoappLoad( + contextHubId, + nanoAppBinary.getNanoAppId(), + nanoAppBinary.getNanoAppVersion(), + nanoAppBinary.getBinary().length, + result == ContextHubTransaction.RESULT_SUCCESS); if (result == ContextHubTransaction.RESULT_SUCCESS) { // NOTE: The legacy JNI code used to do a query right after a load success // to synchronize the service cache. Instead store the binary that was // requested to load to update the cache later without doing a query. mNanoAppStateManager.addNanoAppInstance( - contextHubId, nanoAppBinary.getNanoAppId(), + contextHubId, + nanoAppBinary.getNanoAppId(), nanoAppBinary.getNanoAppVersion()); } try { @@ -228,42 +238,51 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Creates a transaction for unloading a nanoapp. * - * @param contextHubId the ID of the hub to unload the nanoapp from - * @param nanoAppId the ID of the nanoapp to unload + * @param contextHubId the ID of the hub to unload the nanoapp from + * @param nanoAppId the ID of the nanoapp to unload * @param onCompleteCallback the client on complete callback * @return the generated transaction */ /* package */ ContextHubServiceTransaction createUnloadTransaction( - int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, + int contextHubId, + long nanoAppId, + IContextHubTransactionCallback onCompleteCallback, String packageName) { return new ContextHubServiceTransaction( - mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP, - nanoAppId, packageName) { + mNextAvailableId.getAndIncrement(), + ContextHubTransaction.TYPE_UNLOAD_NANOAPP, + nanoAppId, + packageName) { @Override - /* package */ int onTransact() { + /* package */ int onTransact() { try { return mContextHubProxy.unloadNanoapp( contextHubId, nanoAppId, this.getTransactionId()); } catch (RemoteException e) { - Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" + - Long.toHexString(nanoAppId), e); + Log.e( + TAG, + "RemoteException while trying to unload nanoapp with ID 0x" + + Long.toHexString(nanoAppId), + e); return ContextHubTransaction.RESULT_FAILED_UNKNOWN; } } @Override - /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { + /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { ContextHubStatsLog.write( - ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId, + ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, + nanoAppId, 0 /* nanoappVersion */, ContextHubStatsLog - .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD, + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD, toStatsTransactionResult(result)); - ContextHubEventLogger.getInstance().logNanoappUnload( - contextHubId, - nanoAppId, - result == ContextHubTransaction.RESULT_SUCCESS); + ContextHubEventLogger.getInstance() + .logNanoappUnload( + contextHubId, + nanoAppId, + result == ContextHubTransaction.RESULT_SUCCESS); if (result == ContextHubTransaction.RESULT_SUCCESS) { mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId); @@ -283,31 +302,37 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Creates a transaction for enabling a nanoapp. * - * @param contextHubId the ID of the hub to enable the nanoapp on - * @param nanoAppId the ID of the nanoapp to enable + * @param contextHubId the ID of the hub to enable the nanoapp on + * @param nanoAppId the ID of the nanoapp to enable * @param onCompleteCallback the client on complete callback * @return the generated transaction */ /* package */ ContextHubServiceTransaction createEnableTransaction( - int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, + int contextHubId, + long nanoAppId, + IContextHubTransactionCallback onCompleteCallback, String packageName) { return new ContextHubServiceTransaction( - mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP, + mNextAvailableId.getAndIncrement(), + ContextHubTransaction.TYPE_ENABLE_NANOAPP, packageName) { @Override - /* package */ int onTransact() { + /* package */ int onTransact() { try { return mContextHubProxy.enableNanoapp( contextHubId, nanoAppId, this.getTransactionId()); } catch (RemoteException e) { - Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" + - Long.toHexString(nanoAppId), e); + Log.e( + TAG, + "RemoteException while trying to enable nanoapp with ID 0x" + + Long.toHexString(nanoAppId), + e); return ContextHubTransaction.RESULT_FAILED_UNKNOWN; } } @Override - /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { + /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { try { onCompleteCallback.onTransactionComplete(result); } catch (RemoteException e) { @@ -320,31 +345,37 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Creates a transaction for disabling a nanoapp. * - * @param contextHubId the ID of the hub to disable the nanoapp on - * @param nanoAppId the ID of the nanoapp to disable + * @param contextHubId the ID of the hub to disable the nanoapp on + * @param nanoAppId the ID of the nanoapp to disable * @param onCompleteCallback the client on complete callback * @return the generated transaction */ /* package */ ContextHubServiceTransaction createDisableTransaction( - int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback, + int contextHubId, + long nanoAppId, + IContextHubTransactionCallback onCompleteCallback, String packageName) { return new ContextHubServiceTransaction( - mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP, + mNextAvailableId.getAndIncrement(), + ContextHubTransaction.TYPE_DISABLE_NANOAPP, packageName) { @Override - /* package */ int onTransact() { + /* package */ int onTransact() { try { return mContextHubProxy.disableNanoapp( contextHubId, nanoAppId, this.getTransactionId()); } catch (RemoteException e) { - Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" + - Long.toHexString(nanoAppId), e); + Log.e( + TAG, + "RemoteException while trying to disable nanoapp with ID 0x" + + Long.toHexString(nanoAppId), + e); return ContextHubTransaction.RESULT_FAILED_UNKNOWN; } } @Override - /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { + /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { try { onCompleteCallback.onTransactionComplete(result); } catch (RemoteException e) { @@ -447,18 +478,20 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Creates a transaction for querying for a list of nanoapps. * - * @param contextHubId the ID of the hub to query + * @param contextHubId the ID of the hub to query * @param onCompleteCallback the client on complete callback * @return the generated transaction */ /* package */ ContextHubServiceTransaction createQueryTransaction( - int contextHubId, IContextHubTransactionCallback onCompleteCallback, + int contextHubId, + IContextHubTransactionCallback onCompleteCallback, String packageName) { return new ContextHubServiceTransaction( - mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS, + mNextAvailableId.getAndIncrement(), + ContextHubTransaction.TYPE_QUERY_NANOAPPS, packageName) { @Override - /* package */ int onTransact() { + /* package */ int onTransact() { try { return mContextHubProxy.queryNanoapps(contextHubId); } catch (RemoteException e) { @@ -468,12 +501,12 @@ import java.util.concurrent.atomic.AtomicInteger; } @Override - /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { + /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) { onQueryResponse(result, Collections.emptyList()); } @Override - /* package */ void onQueryResponse( + /* package */ void onQueryResponse( @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) { try { onCompleteCallback.onQueryResponse(result, nanoAppStateList); @@ -539,6 +572,14 @@ import java.util.concurrent.atomic.AtomicInteger; } } + @VisibleForTesting + /* package */ + int numReliableMessageTransactionPending() { + synchronized (mReliableMessageLock) { + return mReliableMessageTransactionMap.size(); + } + } + /** * Handles a transaction response from a Context Hub. * @@ -585,18 +626,21 @@ import java.util.concurrent.atomic.AtomicInteger; void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) { if (!Flags.reliableMessageRetrySupportService()) { TransactionAcceptConditions conditions = - transaction -> transaction.getTransactionType() - == ContextHubTransaction.TYPE_RELIABLE_MESSAGE - && transaction.getMessageSequenceNumber() - == messageSequenceNumber; + transaction -> + transaction.getTransactionType() + == ContextHubTransaction.TYPE_RELIABLE_MESSAGE + && transaction.getMessageSequenceNumber() + == messageSequenceNumber; ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions); if (transaction == null) { - Log.w(TAG, "Received unexpected message delivery response (expected" - + " message sequence number = " - + messageSequenceNumber - + ", received messageSequenceNumber = " - + messageSequenceNumber - + ")"); + Log.w( + TAG, + "Received unexpected message delivery response (expected" + + " message sequence number = " + + messageSequenceNumber + + ", received messageSequenceNumber = " + + messageSequenceNumber + + ")"); return; } @@ -640,8 +684,10 @@ import java.util.concurrent.atomic.AtomicInteger; */ /* package */ void onQueryResponse(List<NanoAppState> nanoAppStateList) { - TransactionAcceptConditions conditions = transaction -> - transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS; + TransactionAcceptConditions conditions = + transaction -> + transaction.getTransactionType() + == ContextHubTransaction.TYPE_QUERY_NANOAPPS; ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions); if (transaction == null) { Log.w(TAG, "Received unexpected query response"); @@ -968,24 +1014,33 @@ import java.util.concurrent.atomic.AtomicInteger; private int toStatsTransactionResult(@ContextHubTransaction.Result int result) { switch (result) { case ContextHubTransaction.RESULT_SUCCESS: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS; case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS; case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED; case ContextHubTransaction.RESULT_FAILED_BUSY: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY; case ContextHubTransaction.RESULT_FAILED_AT_HUB: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB; case ContextHubTransaction.RESULT_FAILED_TIMEOUT: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT; case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE; case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE: - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE; case ContextHubTransaction.RESULT_FAILED_UNKNOWN: default: /* fall through */ - return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN; + return ContextHubStatsLog + .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN; } } diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java index 7a96195528d5..993704988d86 100644 --- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java +++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java @@ -21,6 +21,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabaseCorruptException; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; @@ -204,6 +205,11 @@ class WatchlistReportDbHelper extends SQLiteOpenHelper { return false; } final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp; - return db.delete(WhiteListReportContract.TABLE, clause, null) != 0; + try { + return db.delete(WhiteListReportContract.TABLE, clause, null) != 0; + } catch (SQLiteDatabaseCorruptException e) { + Slog.e(TAG, "Error deleting records", e); + return false; + } } } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 3f2c2228e453..dd52cce9e927 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -46,6 +46,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; public class ConditionProviders extends ManagedServices { @@ -202,7 +203,14 @@ public class ConditionProviders extends ManagedServices { @Override protected void loadDefaultsFromConfig() { - String defaultDndAccess = mContext.getResources().getString( + for (String dndPackage : getDefaultDndAccessPackages(mContext)) { + addDefaultComponentOrPackage(dndPackage); + } + } + + static List<String> getDefaultDndAccessPackages(Context context) { + ArrayList<String> packages = new ArrayList<>(); + String defaultDndAccess = context.getResources().getString( R.string.config_defaultDndAccessPackages); if (defaultDndAccess != null) { String[] dnds = defaultDndAccess.split(ManagedServices.ENABLED_SERVICES_SEPARATOR); @@ -210,9 +218,10 @@ public class ConditionProviders extends ManagedServices { if (TextUtils.isEmpty(dnds[i])) { continue; } - addDefaultComponentOrPackage(dnds[i]); + packages.add(dnds[i]); } } + return packages; } @Override diff --git a/services/core/java/com/android/server/notification/ZenConfigTrimmer.java b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java new file mode 100644 index 000000000000..d65954d11646 --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java @@ -0,0 +1,109 @@ +/* + * 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.notification; + +import android.content.Context; +import android.os.Parcel; +import android.service.notification.SystemZenRules; +import android.service.notification.ZenModeConfig; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +class ZenConfigTrimmer { + + private static final String TAG = "ZenConfigTrimmer"; + private static final int MAXIMUM_PARCELED_SIZE = 150_000; // bytes + + private final HashSet<String> mTrustedPackages; + + ZenConfigTrimmer(Context context) { + mTrustedPackages = new HashSet<>(); + mTrustedPackages.add(SystemZenRules.PACKAGE_ANDROID); + mTrustedPackages.addAll(ConditionProviders.getDefaultDndAccessPackages(context)); + } + + void trimToMaximumSize(ZenModeConfig config) { + Map<String, PackageRules> rulesPerPackage = new HashMap<>(); + for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) { + PackageRules pkgRules = rulesPerPackage.computeIfAbsent(rule.pkg, PackageRules::new); + pkgRules.mRules.add(rule); + } + + int totalSize = 0; + for (PackageRules pkgRules : rulesPerPackage.values()) { + totalSize += pkgRules.dataSize(); + } + + if (totalSize > MAXIMUM_PARCELED_SIZE) { + List<PackageRules> deletionCandidates = new ArrayList<>(); + for (PackageRules pkgRules : rulesPerPackage.values()) { + if (!mTrustedPackages.contains(pkgRules.mPkg)) { + deletionCandidates.add(pkgRules); + } + } + deletionCandidates.sort(Comparator.comparingInt(PackageRules::dataSize).reversed()); + + evictPackagesFromConfig(config, deletionCandidates, totalSize); + } + } + + private static void evictPackagesFromConfig(ZenModeConfig config, + List<PackageRules> deletionCandidates, int currentSize) { + while (currentSize > MAXIMUM_PARCELED_SIZE && !deletionCandidates.isEmpty()) { + PackageRules rulesToDelete = deletionCandidates.removeFirst(); + Slog.w(TAG, String.format("Evicting %s zen rules from package '%s' (%s bytes)", + rulesToDelete.mRules.size(), rulesToDelete.mPkg, rulesToDelete.dataSize())); + + for (ZenModeConfig.ZenRule rule : rulesToDelete.mRules) { + config.automaticRules.remove(rule.id); + } + + currentSize -= rulesToDelete.dataSize(); + } + } + + private static class PackageRules { + private final String mPkg; + private final List<ZenModeConfig.ZenRule> mRules; + private int mParceledSize = -1; + + PackageRules(String pkg) { + mPkg = pkg; + mRules = new ArrayList<>(); + } + + private int dataSize() { + if (mParceledSize >= 0) { + return mParceledSize; + } + Parcel parcel = Parcel.obtain(); + try { + parcel.writeParcelableList(mRules, 0); + mParceledSize = parcel.dataSize(); + return mParceledSize; + } finally { + parcel.recycle(); + } + } + } +} diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 889df512dd60..8b09c2acb96a 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -48,6 +48,7 @@ import static android.service.notification.ZenModeConfig.isImplicitRuleId; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.server.notification.Flags.preventZenDeviceEffectsWhileDriving; +import static com.android.server.notification.Flags.limitZenConfigSize; import static java.util.Objects.requireNonNull; @@ -192,6 +193,7 @@ public class ZenModeHelper { private final ConditionProviders.Config mServiceConfig; private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver; private final ZenModeEventLogger mZenModeEventLogger; + private final ZenConfigTrimmer mConfigTrimmer; @VisibleForTesting protected int mZenMode; @VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy; @@ -226,6 +228,7 @@ public class ZenModeHelper { mClock = clock; addCallback(mMetrics); mAppOps = context.getSystemService(AppOpsManager.class); + mConfigTrimmer = new ZenConfigTrimmer(mContext); mDefaultConfig = Flags.modesUi() ? ZenModeConfig.getDefaultConfig() @@ -2061,20 +2064,20 @@ public class ZenModeHelper { Log.w(TAG, "Invalid config in setConfigLocked; " + config); return false; } + if (limitZenConfigSize() && (origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP)) { + mConfigTrimmer.trimToMaximumSize(config); + } + if (config.user != mUser) { // simply store away for background users - synchronized (mConfigLock) { - mConfigs.put(config.user, config); - } + mConfigs.put(config.user, config); if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user); return true; } // handle CPS backed conditions - danger! may modify config mConditions.evaluateConfig(config, null, false /*processSubscriptions*/); - synchronized (mConfigLock) { - mConfigs.put(config.user, config); - } + mConfigs.put(config.user, config); if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable()); ZenLog.traceConfig(origin, reason, triggeringComponent, mConfig, config, callingUid); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 76cd5c88b388..346d65a06cc9 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -212,6 +212,16 @@ flag { } flag { + name: "limit_zen_config_size" + namespace: "systemui" + description: "Enforce a maximum (serialized) size for the Zen configuration" + bug: "387498139" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "managed_services_concurrent_multiuser" namespace: "systemui" description: "Enables ManagedServices to support Concurrent multi user environment" diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 80c2d415d370..5a6d7a245f56 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2937,28 +2937,20 @@ public class UserManagerService extends IUserManager.Stub { int flags = UserManager.SWITCHABILITY_STATUS_OK; - t.traceBegin("TM.isInCall"); - final long identity = Binder.clearCallingIdentity(); - try { - final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class); - if (com.android.internal.telephony.flags - .Flags.enforceTelephonyFeatureMappingForPublicApis()) { - if (mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TELECOM)) { - if (telecomManager != null && telecomManager.isInCall()) { - flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL; - } - } - } else { + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) { + t.traceBegin("TM.isInCall"); + final long identity = Binder.clearCallingIdentity(); + try { + final TelecomManager telecomManager = mContext.getSystemService( + TelecomManager.class); if (telecomManager != null && telecomManager.isInCall()) { flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL; } + } finally { + Binder.restoreCallingIdentity(identity); } - } finally { - Binder.restoreCallingIdentity(identity); + t.traceEnd(); } - t.traceEnd(); - t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH"); if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) { flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED; diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index fab19b6b8201..1afbb34c5f09 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -160,8 +160,10 @@ public interface StatusBarManagerInternal { * @param displayId The changed display Id. * @param rootDisplayAreaId The changed display area Id. * @param isImmersiveMode {@code true} if the display area get into immersive mode. + * @param windowType The window type of the controlling window. */ - void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode); + void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode, + int windowType); /** * Show a rotation suggestion that a user may approve to rotate the screen. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index da9d01675984..798c794edaf5 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -732,7 +732,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void immersiveModeChanged(int displayId, int rootDisplayAreaId, - boolean isImmersiveMode) { + boolean isImmersiveMode, int windowType) { if (mBar == null) { return; } @@ -746,7 +746,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D if (!CLIENT_TRANSIENT) { // Only call from here when the client transient is not enabled. try { - mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode); + mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, windowType); } catch (RemoteException ex) { } } diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java index 94e52cd1033a..d4b20fb9bcfc 100644 --- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java +++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java @@ -68,6 +68,10 @@ public class WatchedVolumeInfo extends WatchableImpl { return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo); } + public ImmutableVolumeInfo getClonedImmutableVolumeInfo() { + return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo.clone()); + } + public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted); } diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java index bda3d442956b..621a128a736e 100644 --- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java @@ -51,6 +51,7 @@ import java.util.NoSuchElementException; final class VendorVibrationSession extends IVibrationSession.Stub implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient { private static final String TAG = "VendorVibrationSession"; + private static final boolean DEBUG = false; /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */ interface VibratorManagerHooks { @@ -73,8 +74,8 @@ final class VendorVibrationSession extends IVibrationSession.Stub private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport(); private final int[] mVibratorIds; private final long mCreateUptime; - private final long mCreateTime; // for debugging - private final IVibrationSessionCallback mCallback; + private final long mCreateTime; + private final VendorCallbackWrapper mCallback; private final CallerInfo mCallerInfo; private final VibratorManagerHooks mManagerHooks; private final DeviceAdapter mDeviceAdapter; @@ -88,11 +89,11 @@ final class VendorVibrationSession extends IVibrationSession.Stub @GuardedBy("mLock") private boolean mEndedByVendor; @GuardedBy("mLock") - private long mStartTime; // for debugging + private long mStartTime; @GuardedBy("mLock") private long mEndUptime; @GuardedBy("mLock") - private long mEndTime; // for debugging + private long mEndTime; @GuardedBy("mLock") private VibrationStepConductor mConductor; @@ -103,7 +104,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub mCreateTime = System.currentTimeMillis(); mVibratorIds = deviceAdapter.getAvailableVibratorIds(); mHandler = handler; - mCallback = callback; + mCallback = new VendorCallbackWrapper(callback, handler); mCallerInfo = callerInfo; mManagerHooks = managerHooks; mDeviceAdapter = deviceAdapter; @@ -119,7 +120,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void finishSession() { - Slog.d(TAG, "Session finish requested, ending vibration session..."); + if (DEBUG) { + Slog.d(TAG, "Session finish requested, ending vibration session..."); + } // Do not abort session in HAL, wait for ongoing vibration requests to complete. // This might take a while to end the session, but it can be aborted by cancelSession. requestEndSession(Status.FINISHED, /* shouldAbort= */ false, /* isVendorRequest= */ true); @@ -127,7 +130,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void cancelSession() { - Slog.d(TAG, "Session cancel requested, aborting vibration session..."); + if (DEBUG) { + Slog.d(TAG, "Session cancel requested, aborting vibration session..."); + } // Always abort session in HAL while cancelling it. // This might be triggered after finishSession was already called. requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true, @@ -156,7 +161,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public IBinder getCallerToken() { - return mCallback.asBinder(); + return mCallback.getBinderToken(); } @Override @@ -176,36 +181,30 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void onCancel() { - Slog.d(TAG, "Session cancellation signal received, aborting vibration session..."); + if (DEBUG) { + Slog.d(TAG, "Session cancellation signal received, aborting vibration session..."); + } requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true, /* isVendorRequest= */ true); } @Override public void binderDied() { - Slog.d(TAG, "Session binder died, aborting vibration session..."); + if (DEBUG) { + Slog.d(TAG, "Session binder died, aborting vibration session..."); + } requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true, /* isVendorRequest= */ false); } @Override public boolean linkToDeath() { - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - Slog.e(TAG, "Error linking session to token death", e); - return false; - } - return true; + return mCallback.linkToDeath(this); } @Override public void unlinkToDeath() { - try { - mCallback.asBinder().unlinkToDeath(this, 0); - } catch (NoSuchElementException e) { - Slog.wtf(TAG, "Failed to unlink session to token death", e); - } + mCallback.unlinkToDeath(this); } @Override @@ -219,26 +218,37 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) { - Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId - + " on vibrator " + vibratorId + ", ignoring..."); + if (DEBUG) { + Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + + " step " + stepId + " on vibrator " + vibratorId + ", ignoring..."); + } } @Override public void notifySyncedVibratorsCallback(long vibrationId) { - Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId - + ", ignoring..."); + if (DEBUG) { + Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId + + ", ignoring..."); + } } @Override public void notifySessionCallback() { - Slog.d(TAG, "Session callback received, ending vibration session..."); + if (DEBUG) { + Slog.d(TAG, "Session callback received, ending vibration session..."); + } synchronized (mLock) { // If end was not requested then the HAL has cancelled the session. - maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON, + notifyEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON, /* isVendorRequest= */ false); maybeSetStatusToRequestedLocked(); clearVibrationConductor(); - mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId)); + final Status endStatus = mStatus; + mHandler.post(() -> { + mManagerHooks.onSessionReleased(mSessionId); + // Only trigger client callback after session is released in the manager. + mCallback.notifyFinished(endStatus); + }); } } @@ -271,7 +281,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub public boolean isEnded() { synchronized (mLock) { - return mStatus != Status.RUNNING; + return mEndTime > 0; } } @@ -297,19 +307,17 @@ final class VendorVibrationSession extends IVibrationSession.Stub // Session already ended, skip start callbacks. isAlreadyEnded = true; } else { + if (DEBUG) { + Slog.d(TAG, "Session started at the HAL"); + } mStartTime = System.currentTimeMillis(); - // Run client callback in separate thread. - mHandler.post(() -> { - try { - mCallback.onStarted(this); - } catch (RemoteException e) { - Slog.e(TAG, "Error notifying vendor session started", e); - } - }); + mCallback.notifyStarted(this); } } if (isAlreadyEnded) { - Slog.d(TAG, "Session already ended after starting the HAL, aborting..."); + if (DEBUG) { + Slog.d(TAG, "Session already ended after starting the HAL, aborting..."); + } mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true)); } } @@ -337,8 +345,10 @@ final class VendorVibrationSession extends IVibrationSession.Stub public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) { synchronized (mLock) { if (mConductor != null) { - Slog.d(TAG, "Session still dispatching previous vibration, new vibration " - + conductor.getVibration().id + " ignored"); + if (DEBUG) { + Slog.d(TAG, "Session still dispatching previous vibration, new vibration " + + conductor.getVibration().id + " ignored"); + } return false; } mConductor = conductor; @@ -347,53 +357,45 @@ final class VendorVibrationSession extends IVibrationSession.Stub } private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) { - Slog.d(TAG, "Session end request received with status " + status); - boolean shouldTriggerSessionHook = false; + if (DEBUG) { + Slog.d(TAG, "Session end request received with status " + status); + } synchronized (mLock) { - maybeSetEndRequestLocked(status, isVendorRequest); + notifyEndRequestLocked(status, isVendorRequest); if (!isEnded() && isStarted()) { // Trigger session hook even if it was already triggered, in case a second request // is aborting the ongoing/ending session. This might cause it to end right away. // Wait for HAL callback before setting the end status. - shouldTriggerSessionHook = true; + if (DEBUG) { + Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort); + } + mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort)); } else { - // Session not active in the HAL, set end status right away. + // Session not active in the HAL, try to set end status right away. maybeSetStatusToRequestedLocked(); + // Use status used to end this session, which might be different from requested. + mCallback.notifyFinished(mStatus); } } - if (shouldTriggerSessionHook) { - Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort); - mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort)); - } } @GuardedBy("mLock") - private void maybeSetEndRequestLocked(Status status, boolean isVendorRequest) { + private void notifyEndRequestLocked(Status status, boolean isVendorRequest) { if (mEndStatusRequest != null) { - // End already requested, keep first requested status and time. + // End already requested, keep first requested status. return; } - Slog.d(TAG, "Session end request accepted for status " + status); + if (DEBUG) { + Slog.d(TAG, "Session end request accepted for status " + status); + } mEndStatusRequest = status; mEndedByVendor = isVendorRequest; - mEndTime = System.currentTimeMillis(); - mEndUptime = SystemClock.uptimeMillis(); + mCallback.notifyFinishing(); if (mConductor != null) { // Vibration is being dispatched when session end was requested, cancel it. mConductor.notifyCancelled(new Vibration.EndInfo(status), /* immediate= */ status != Status.FINISHED); } - if (isStarted()) { - // Only trigger "finishing" callback if session started. - // Run client callback in separate thread. - mHandler.post(() -> { - try { - mCallback.onFinishing(); - } catch (RemoteException e) { - Slog.e(TAG, "Error notifying vendor session is finishing", e); - } - }); - } } @GuardedBy("mLock") @@ -406,40 +408,123 @@ final class VendorVibrationSession extends IVibrationSession.Stub // No end status was requested, nothing to set. return; } - Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest); + if (DEBUG) { + Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest); + } mStatus = mEndStatusRequest; - // Run client callback in separate thread. - final Status endStatus = mStatus; - mHandler.post(() -> { + mEndTime = System.currentTimeMillis(); + mEndUptime = SystemClock.uptimeMillis(); + } + + /** + * Wrapper class to handle client callbacks asynchronously. + * + * <p>This class is also responsible for link/unlink to the client process binder death, and for + * making sure the callbacks are only triggered once. The conversion between session status and + * the API status code is also defined here. + */ + private static final class VendorCallbackWrapper { + private final IVibrationSessionCallback mCallback; + private final Handler mHandler; + + private boolean mIsStarted; + private boolean mIsFinishing; + private boolean mIsFinished; + + VendorCallbackWrapper(@NonNull IVibrationSessionCallback callback, + @NonNull Handler handler) { + mCallback = callback; + mHandler = handler; + } + + synchronized IBinder getBinderToken() { + return mCallback.asBinder(); + } + + synchronized boolean linkToDeath(DeathRecipient recipient) { try { - mCallback.onFinished(toSessionStatus(endStatus)); + mCallback.asBinder().linkToDeath(recipient, 0); } catch (RemoteException e) { - Slog.e(TAG, "Error notifying vendor session finished", e); + Slog.e(TAG, "Error linking session to token death", e); + return false; + } + return true; + } + + synchronized void unlinkToDeath(DeathRecipient recipient) { + try { + mCallback.asBinder().unlinkToDeath(recipient, 0); + } catch (NoSuchElementException e) { + Slog.wtf(TAG, "Failed to unlink session to token death", e); + } + } + + synchronized void notifyStarted(IVibrationSession session) { + if (mIsStarted) { + return; + } + mIsStarted = true; + mHandler.post(() -> { + try { + mCallback.onStarted(session); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session started", e); + } + }); + } + + synchronized void notifyFinishing() { + if (!mIsStarted || mIsFinishing || mIsFinished) { + // Ignore if never started or if already finishing or finished. + return; } - }); - } - - @android.os.vibrator.VendorVibrationSession.Status - private static int toSessionStatus(Status status) { - // Exhaustive switch to cover all possible internal status. - return switch (status) { - case FINISHED - -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS; - case IGNORED_UNSUPPORTED - -> STATUS_UNSUPPORTED; - case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER, - CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF, - CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON - -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED; - case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING, - IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE, - IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED, - IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER - -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED; - case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING, - IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING - -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR; - }; + mIsFinishing = true; + mHandler.post(() -> { + try { + mCallback.onFinishing(); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session is finishing", e); + } + }); + } + + synchronized void notifyFinished(Status status) { + if (mIsFinished) { + return; + } + mIsFinished = true; + mHandler.post(() -> { + try { + mCallback.onFinished(toSessionStatus(status)); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session finished", e); + } + }); + } + + @android.os.vibrator.VendorVibrationSession.Status + private static int toSessionStatus(Status status) { + // Exhaustive switch to cover all possible internal status. + return switch (status) { + case FINISHED + -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS; + case IGNORED_UNSUPPORTED + -> STATUS_UNSUPPORTED; + case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER, + CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF, + CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON + -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED; + case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING, + IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE, + IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED, + IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER + -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED; + case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, + IGNORED_ERROR_SCHEDULING, IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, + FINISHED_UNEXPECTED, RUNNING + -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR; + }; + } } /** @@ -499,7 +584,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void logMetrics(VibratorFrameworkStatsLogger statsLogger) { if (mStartTime > 0) { - // Only log sessions that have started. + // Only log sessions that have started in the HAL. statsLogger.logVibrationVendorSessionStarted(mCallerInfo.uid); statsLogger.logVibrationVendorSessionVibrations(mCallerInfo.uid, mVibrations.size()); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 124097938ff8..57b82c38c5b8 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2326,13 +2326,16 @@ final class ActivityRecord extends WindowToken { if (isActivityTypeHome()) { // The snapshot of home is only used once because it won't be updated while screen // is on (see {@link TaskSnapshotController#screenTurningOff}). - mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId); final Transition transition = mTransitionController.getCollectingTransition(); if (transition != null && (transition.getFlags() & WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) { + mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId); // Only use snapshot of home as starting window when unlocking directly. return false; } + // Add a reference before removing snapshot from cache. + snapshot.addReference(TaskSnapshot.REFERENCE_WRITE_TO_PARCEL); + mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId); } return createSnapshot(snapshot, typeParameter); } diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index aed5e140703c..f51e60c101e4 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -144,9 +144,9 @@ public class BackgroundActivityStartController { .setPendingIntentCreatorBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); - private ActivityTaskManagerService mService; + private final ActivityTaskManagerService mService; - private ActivityTaskSupervisor mSupervisor; + private final ActivityTaskSupervisor mSupervisor; @GuardedBy("mStrictModeBalCallbacks") private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>> mStrictModeBalCallbacks = new SparseArray<>(); @@ -279,16 +279,24 @@ public class BackgroundActivityStartController { mSupervisor = supervisor; } + private ActivityTaskManagerService getService() { + return mService; + } + + private ActivityTaskSupervisor getSupervisor() { + return mSupervisor; + } + private boolean isHomeApp(int uid, @Nullable String packageName) { - if (mService.mHomeProcess != null) { + if (getService().mHomeProcess != null) { // Fast check - return uid == mService.mHomeProcess.mUid; + return uid == getService().mHomeProcess.mUid; } if (packageName == null) { return false; } ComponentName activity = - mService.getPackageManagerInternalLocked() + getService().getPackageManagerInternalLocked() .getDefaultHomeActivity(UserHandle.getUserId(uid)); return activity != null && packageName.equals(activity.getPackageName()); } @@ -342,7 +350,8 @@ public class BackgroundActivityStartController { mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess; mOriginatingPendingIntent = originatingPendingIntent; mIntent = intent; - mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); + mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid, + realCallingPid); mIsCallForResult = resultRecord != null; mCheckedOptions = checkedOptions; @BackgroundActivityStartMode int callerBackgroundActivityStartMode = @@ -401,13 +410,13 @@ public class BackgroundActivityStartController { checkedOptions, realCallingUid, mRealCallingPackage); } - mAppSwitchState = mService.getBalAppSwitchesState(); - mCallingUidProcState = mService.mActiveUids.getUidState(callingUid); + mAppSwitchState = getService().getBalAppSwitchesState(); + mCallingUidProcState = getService().mActiveUids.getUidState(callingUid); mIsCallingUidPersistentSystemProcess = mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; mCallingUidHasVisibleActivity = - mService.mVisibleActivityProcessTracker.hasVisibleActivity(callingUid); - mCallingUidHasNonAppVisibleWindow = mService.mActiveUids.hasNonAppVisibleWindow( + getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid); + mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow( callingUid); if (realCallingUid == NO_PROCESS_UID) { // no process provided @@ -422,16 +431,17 @@ public class BackgroundActivityStartController { mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow; // In the PendingIntent case callerApp is not passed in, so resolve it ourselves. mRealCallerApp = callerApp == null - ? mService.getProcessController(realCallingPid, realCallingUid) + ? getService().getProcessController(realCallingPid, realCallingUid) : callerApp; mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess; } else { - mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid); + mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid); mRealCallingUidHasVisibleActivity = - mService.mVisibleActivityProcessTracker.hasVisibleActivity(realCallingUid); + getService().mVisibleActivityProcessTracker.hasVisibleActivity( + realCallingUid); mRealCallingUidHasNonAppVisibleWindow = - mService.mActiveUids.hasNonAppVisibleWindow(realCallingUid); - mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid); + getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid); + mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid); mIsRealCallingUidPersistentSystemProcess = mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; } @@ -481,7 +491,7 @@ public class BackgroundActivityStartController { if (uid == 0) { return "root[debugOnly]"; } - String name = mService.getPackageManagerInternalLocked().getNameForUid(uid); + String name = getService().getPackageManagerInternalLocked().getNameForUid(uid); if (name == null) { name = "uid=" + uid; } @@ -783,7 +793,7 @@ public class BackgroundActivityStartController { Process.getAppUidForSdkSandboxUid(state.mRealCallingUid); // realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition // to realCallingUid when calculating resultForRealCaller below. - if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) { + if (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) { state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX, /*background*/ false, "uid in SDK sandbox has visible (non-toast) window")); @@ -1000,30 +1010,28 @@ public class BackgroundActivityStartController { * or {@link #BAL_BLOCK} if the launch should be blocked */ BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) { + boolean evaluateVisibleOnly = balAdditionalStartModes() + && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; + if (evaluateVisibleOnly) { + return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible, + mCheckCallerProcessAllowsForeground); + } if (state.isPendingIntent()) { // PendingIntents should mostly be allowed by the sender (real caller) or a permission // the creator of the PendingIntent has. Visibility should be the exceptional case, so // test it last (this does not change the result, just the bal code). - BalVerdict result = BalVerdict.BLOCK; - if (!(balAdditionalStartModes() - && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { - result = checkBackgroundActivityStartAllowedByCallerInBackground(state); - } - if (result == BalVerdict.BLOCK) { - result = checkBackgroundActivityStartAllowedByCallerInForeground(state); - - } - return result; - } else { - BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state); - if (result == BalVerdict.BLOCK && !(balAdditionalStartModes() - && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { - result = checkBackgroundActivityStartAllowedByCallerInBackground(state); - } - return result; + return evaluateChain(state, mCheckCallerIsAllowlistedUid, + mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission, + mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp, + mCheckCallerProcessAllowsBackground, mCheckCallerVisible, + mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground); } + return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible, + mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid, + mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission, + mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp, + mCheckCallerProcessAllowsBackground); } interface BalExemptionCheck { @@ -1061,7 +1069,7 @@ public class BackgroundActivityStartController { if (state.mCallingUidHasNonAppVisibleWindow) { return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, /*background*/ false, "callingUid has non-app visible window " - + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid)); + + getService().mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid)); } return BalVerdict.BLOCK; }; @@ -1090,7 +1098,7 @@ public class BackgroundActivityStartController { final int callingAppId = UserHandle.getAppId(state.mCallingUid); // IME should always be allowed to start activity, like IME settings. final WindowState imeWindow = - mService.mRootWindowContainer.getCurrentInputMethodWindow(); + getService().mRootWindowContainer.getCurrentInputMethodWindow(); if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ false, @@ -1104,23 +1112,23 @@ public class BackgroundActivityStartController { } // don't abort if the caller has the same uid as the recents component - if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) { + if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Recents Component"); } // don't abort if the callingUid is the device owner - if (mService.isDeviceOwner(state.mCallingUid)) { + if (getService().isDeviceOwner(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Device Owner"); } // don't abort if the callingUid is a affiliated profile owner - if (mService.isAffiliatedProfileOwner(state.mCallingUid)) { + if (getService().isAffiliatedProfileOwner(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Affiliated Profile Owner"); } // don't abort if the callingUid has companion device final int callingUserId = UserHandle.getUserId(state.mCallingUid); - if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) { + if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Companion App"); } @@ -1138,7 +1146,7 @@ public class BackgroundActivityStartController { }; private final BalExemptionCheck mCheckCallerHasSawPermission = state -> { // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission - if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid, + if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid, state.mCallingPackage)) { return new BalVerdict(BAL_ALLOW_SAW_PERMISSION, /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted"); @@ -1148,7 +1156,7 @@ public class BackgroundActivityStartController { private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> { // don't abort if the callingUid and callingPackage have the // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop - if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow( + if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow( AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION, state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, @@ -1170,34 +1178,18 @@ public class BackgroundActivityStartController { * @return A code denoting which BAL rule allows an activity to be started, * or {@link #BAL_BLOCK} if the launch should be blocked */ - BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) { - return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible, - mCheckCallerProcessAllowsForeground); - } - - /** - * @return A code denoting which BAL rule allows an activity to be started, - * or {@link #BAL_BLOCK} if the launch should be blocked - */ - BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) { - return evaluateChain(state, mCheckCallerIsAllowlistedUid, - mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission, - mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp, - mCheckCallerProcessAllowsBackground); - } - - /** - * @return A code denoting which BAL rule allows an activity to be started, - * or {@link #BAL_BLOCK} if the launch should be blocked - */ BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) { - BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state); - if (result == BalVerdict.BLOCK && !(balAdditionalStartModes() + boolean evaluateVisibleOnly = balAdditionalStartModes() && state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { - result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state); + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; + if (evaluateVisibleOnly) { + return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible, + mCheckRealCallerProcessAllowsBalForeground); } - return result; + return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible, + mCheckRealCallerProcessAllowsBalForeground, mCheckRealCallerBalPermission, + mCheckRealCallerSawPermission, mCheckRealCallerAllowlistedUid, + mCheckRealCallerAllowlistedComponent, mCheckRealCallerProcessAllowsBalBackground); } private final BalExemptionCheck mCheckRealCallerVisible = state -> { @@ -1218,21 +1210,20 @@ public class BackgroundActivityStartController { if (state.mRealCallingUidHasNonAppVisibleWindow) { return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, /*background*/ false, "realCallingUid has non-app visible window " - + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mRealCallingUid)); + + getService().mActiveUids.getNonAppVisibleWindowDetails( + state.mRealCallingUid)); } return BalVerdict.BLOCK; }; - private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground = state -> { - // Don't abort if the realCallerApp or other processes of that uid are considered to be in - // the foreground. - return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND); - }; + // Don't abort if the realCallerApp or other processes of that uid are considered to be in + // the foreground. + private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground = + state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND); - private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground = state -> { - // don't abort if the callerApp or other processes of that uid are allowed in any way - return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND); - }; + // don't abort if the callerApp or other processes of that uid are allowed in any way + private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground = + state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND); private final BalExemptionCheck mCheckRealCallerBalPermission = state -> { boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() @@ -1251,7 +1242,7 @@ public class BackgroundActivityStartController { == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; // don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission if (allowAlways - && mService.hasSystemAlertWindowPermission(state.mRealCallingUid, + && getService().hasSystemAlertWindowPermission(state.mRealCallingUid, state.mRealCallingPid, state.mRealCallingPackage)) { return new BalVerdict(BAL_ALLOW_SAW_PERMISSION, /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted"); @@ -1276,7 +1267,7 @@ public class BackgroundActivityStartController { private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> { // don't abort if the realCallingUid is an associated companion app - if (mService.isAssociatedCompanionApp( + if (getService().isAssociatedCompanionApp( UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ false, @@ -1285,25 +1276,6 @@ public class BackgroundActivityStartController { return BalVerdict.BLOCK; }; - /** - * @return A code denoting which BAL rule allows an activity to be started, - * or {@link #BAL_BLOCK} if the launch should be blocked - */ - BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) { - return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible, - mCheckRealCallerProcessAllowsBalForeground); - } - - /** - * @return A code denoting which BAL rule allows an activity to be started, - * or {@link #BAL_BLOCK} if the launch should be blocked - */ - BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) { - return evaluateChain(state, mCheckRealCallerBalPermission, mCheckRealCallerSawPermission, - mCheckRealCallerAllowlistedUid, mCheckRealCallerAllowlistedComponent, - mCheckRealCallerProcessAllowsBalBackground); - } - @VisibleForTesting boolean hasBalPermission(int uid, int pid) { return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, pid, uid) == PERMISSION_GRANTED; @@ -1329,7 +1301,7 @@ public class BackgroundActivityStartController { } else { // only if that one wasn't allowed, check the other ones final ArraySet<WindowProcessController> uidProcesses = - mService.mProcessMap.getProcesses(app.mUid); + getService().mProcessMap.getProcesses(app.mUid); if (uidProcesses != null) { for (int i = uidProcesses.size() - 1; i >= 0; i--) { final WindowProcessController proc = uidProcesses.valueAt(i); @@ -1500,7 +1472,7 @@ public class BackgroundActivityStartController { if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) { String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK + (enforceBlock ? " blocked " : " would block ") - + getApplicationLabel(mService.mContext.getPackageManager(), + + getApplicationLabel(getService().mContext.getPackageManager(), launchedFromPackageName); showToast(toastText); @@ -1522,7 +1494,7 @@ public class BackgroundActivityStartController { } @VisibleForTesting void showToast(String toastText) { - UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, + UiThread.getHandler().post(() -> Toast.makeText(getService().mContext, toastText, Toast.LENGTH_LONG).show()); } @@ -1599,7 +1571,7 @@ public class BackgroundActivityStartController { return; } - String packageName = mService.mContext.getPackageManager().getNameForUid(callingUid); + String packageName = getService().mContext.getPackageManager().getNameForUid(callingUid); BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID, INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic()); @BalCode int balCode = checkBackgroundActivityStartAllowedByCaller(state).mCode; @@ -1660,7 +1632,7 @@ public class BackgroundActivityStartController { boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags .shouldRestrictActivitySwitch(callingUid) && bas.mTopActivityOptedIn; - PackageManager pm = mService.mContext.getPackageManager(); + PackageManager pm = getService().mContext.getPackageManager(); String callingPackage = pm.getNameForUid(callingUid); final CharSequence callingLabel; if (callingPackage == null) { @@ -1821,7 +1793,7 @@ public class BackgroundActivityStartController { return bas.optedIn(ar); } - PackageManager pm = mService.mContext.getPackageManager(); + PackageManager pm = getService().mContext.getPackageManager(); ApplicationInfo applicationInfo; final int sourceUserId = UserHandle.getUserId(sourceUid); @@ -1878,7 +1850,7 @@ public class BackgroundActivityStartController { if (sourceRecord == null) { joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage); - String realCallingPackage = mService.mContext.getPackageManager().getNameForUid( + String realCallingPackage = getService().mContext.getPackageManager().getNameForUid( realCallingUid); joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage); } else { @@ -1913,7 +1885,7 @@ public class BackgroundActivityStartController { joiner.add(prefix + "BalCode: " + balCodeToString(balCode)); joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod); joiner.add(prefix + "LastResumedActivity: " - + recordToString.apply(mService.mLastResumedActivity)); + + recordToString.apply(getService().mLastResumedActivity)); joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement()); if (mTopFinishedActivity != null) { @@ -1986,7 +1958,7 @@ public class BackgroundActivityStartController { } private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) { - if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) { + if (finalVerdict.blocks() && getService().isActivityStartsLoggingEnabled()) { // log aborted activity start to TRON mSupervisor .getActivityMetricsLogger() @@ -2222,7 +2194,7 @@ public class BackgroundActivityStartController { return -1; } try { - PackageManager pm = mService.mContext.getPackageManager(); + PackageManager pm = getService().mContext.getPackageManager(); return pm.getTargetSdkVersion(packageName); } catch (Exception e) { return -1; @@ -2243,8 +2215,8 @@ public class BackgroundActivityStartController { this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1; this.mDebugInfo = getDebugStringForActivityRecord(ar); - mService.mH.postDelayed(() -> { - synchronized (mService.mGlobalLock) { + getService().mH.postDelayed(() -> { + synchronized (getService().mGlobalLock) { if (mTaskIdToFinishedActivity.get(taskId) == this) { mTaskIdToFinishedActivity.remove(taskId); } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index c2255d8d011a..dc42b32967e2 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -79,7 +79,7 @@ public final class DesktopModeHelper { } @VisibleForTesting - static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { if (!shouldEnforceDeviceRestrictions()) { return true; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a874ef6039f9..50f12c305587 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -157,6 +157,7 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFi import static com.android.server.wm.utils.RegionUtils.forEachRectReverse; import static com.android.server.wm.utils.RegionUtils.rectListToRegion; import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import android.annotation.IntDef; import android.annotation.NonNull; @@ -3835,13 +3836,18 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Looking for the focused window on this display if the top focused display hasn't been - * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed. + * found yet (topFocusedDisplayId is INVALID_DISPLAY), per-display focused was allowed, or + * the display is presenting. The last one is needed to update system bar visibility in response + * to presentation visibility because per-display focus is needed to change system bar + * visibility, but the display shouldn't get global focus when a presentation gets shown. * * @param topFocusedDisplayId Id of the top focused display. * @return The focused window or null if there isn't any or no need to seek. */ WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) { - return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY) + return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY + || (enablePresentationForConnectedDisplays() + && mWmService.mPresentationController.isPresentationVisible(mDisplayId))) ? findFocusedWindow() : null; } @@ -6932,6 +6938,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** The actual requested visible inset types for this display */ private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + private @InsetsType int mAnimatingTypes = 0; + /** The component name of the top focused window on this display */ private ComponentName mTopFocusedComponentName = null; @@ -7069,6 +7077,18 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } return 0; } + + @Override + public @InsetsType int getAnimatingTypes() { + return mAnimatingTypes; + } + + @Override + public void setAnimatingTypes(@InsetsType int animatingTypes) { + if (mAnimatingTypes != animatingTypes) { + mAnimatingTypes = animatingTypes; + } + } } MagnificationSpec getMagnificationSpec() { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 4908df025dd1..ec5b503fbb9b 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2564,7 +2564,7 @@ public class DisplayPolicy { final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId; // TODO(b/277290737): Move this to the client side, instead of using a proxy. callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(), - rootDisplayAreaId, isImmersiveMode)); + rootDisplayAreaId, isImmersiveMode, win.getWindowType())); } // Show transient bars for panic if needed. diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java index cee49676eeae..6462a37ae33f 100644 --- a/services/core/java/com/android/server/wm/InsetsControlTarget.java +++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java @@ -97,6 +97,20 @@ interface InsetsControlTarget extends InsetsTarget { @NonNull ImeTracker.Token statsToken) { } + /** + * @return {@link WindowInsets.Type.InsetsType}s which are currently animating (showing or + * hiding). + */ + default @InsetsType int getAnimatingTypes() { + return 0; + } + + /** + * @param animatingTypes the {@link InsetsType}s, that are currently animating + */ + default void setAnimatingTypes(@InsetsType int animatingTypes) { + } + /** Returns {@code target.getWindow()}, or null if {@code target} is {@code null}. */ static WindowState asWindowOrNull(InsetsControlTarget target) { return target != null ? target.getWindow() : null; diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 009d482ba316..28722141dcd3 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -790,8 +790,6 @@ class InsetsPolicy { private final Handler mHandler; private final String mName; - private boolean mInsetsAnimationRunning; - Host(Handler handler, String name) { mHandler = handler; mName = name; @@ -901,10 +899,5 @@ class InsetsPolicy { public IBinder getWindowToken() { return null; } - - @Override - public void notifyAnimationRunningStateChanged(boolean running) { - mInsetsAnimationRunning = running; - } } } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 164abab992d8..5e0395f70e65 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -225,13 +225,16 @@ class InsetsStateController { for (int i = mProviders.size() - 1; i >= 0; i--) { final InsetsSourceProvider provider = mProviders.valueAt(i); final @InsetsType int type = provider.getSource().getType(); + final boolean isImeProvider = type == WindowInsets.Type.ime(); if ((type & changedTypes) != 0) { - final boolean isImeProvider = type == WindowInsets.Type.ime(); changed |= provider.updateClientVisibility( - caller, isImeProvider ? statsToken : null) + caller, isImeProvider ? statsToken : null) // Fake control target cannot change the client visibility, but it should // change the insets with its newly requested visibility. || (caller == provider.getFakeControlTarget()); + } else if (isImeProvider && android.view.inputmethod.Flags.refactorInsetsController()) { + ImeTracker.forLogging().onCancelled(statsToken, + ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY); } } if (changed) { diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java index b3cff9c6cc3d..acc658bf635e 100644 --- a/services/core/java/com/android/server/wm/PresentationController.java +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -16,10 +16,17 @@ package com.android.server.wm; +import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; +import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; + +import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR; import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import android.annotation.NonNull; -import android.util.IntArray; +import android.annotation.Nullable; +import android.hardware.display.DisplayManager; +import android.util.SparseArray; +import android.view.WindowManager.LayoutParams.WindowType; import com.android.internal.protolog.ProtoLog; import com.android.internal.protolog.WmProtoLogGroups; @@ -27,15 +34,125 @@ import com.android.internal.protolog.WmProtoLogGroups; /** * Manages presentation windows. */ -class PresentationController { +class PresentationController implements DisplayManager.DisplayListener { + + private static class Presentation { + @NonNull final WindowState mWin; + @NonNull final WindowContainerListener mPresentationListener; + // This is the task which started this presentation. This shouldn't be null in most cases + // because the intended usage of the Presentation API is that an activity that started a + // presentation should control the UI and lifecycle of the presentation window. + // However, the API doesn't necessarily requires a host activity to exist (e.g. a background + // service can launch a presentation), so this can be null. + @Nullable final Task mHostTask; + @Nullable final WindowContainerListener mHostTaskListener; + + Presentation(@NonNull WindowState win, + @NonNull WindowContainerListener presentationListener, + @Nullable Task hostTask, + @Nullable WindowContainerListener hostTaskListener) { + mWin = win; + mPresentationListener = presentationListener; + mHostTask = hostTask; + mHostTaskListener = hostTaskListener; + } + + @Override + public String toString() { + return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId() + + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}"; + } + } + + private final SparseArray<Presentation> mPresentations = new SparseArray(); + + @Nullable + private Presentation getPresentation(@Nullable WindowState win) { + if (win == null) return null; + for (int i = 0; i < mPresentations.size(); i++) { + final Presentation presentation = mPresentations.valueAt(i); + if (win == presentation.mWin) return presentation; + } + return null; + } - // TODO(b/395475549): Add support for display add/remove, and activity move across displays. - private final IntArray mPresentingDisplayIds = new IntArray(); + private boolean hasPresentationWindow(int displayId) { + return mPresentations.contains(displayId); + } - PresentationController() {} + boolean isPresentationVisible(int displayId) { + final Presentation presentation = mPresentations.get(displayId); + return presentation != null && presentation.mWin.mToken.isVisibleRequested(); + } - private boolean isPresenting(int displayId) { - return mPresentingDisplayIds.contains(displayId); + boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) { + return canPresent(win, displayContent, win.mAttrs.type, win.getUid()); + } + + /** + * Checks if a presentation window can be shown on the given display. + * If the given |win| is empty, a new presentation window is being created. + * If the given |win| is not empty, the window already exists as presentation, and we're + * revalidate if the |win| is still qualified to be shown. + */ + boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent, + @WindowType int type, int uid) { + if (type == TYPE_PRIVATE_PRESENTATION) { + // Private presentations can only be created on private displays. + return displayContent.isPrivate(); + } + + if (type != TYPE_PRESENTATION) { + return false; + } + + if (!enablePresentationForConnectedDisplays()) { + return displayContent.getDisplay().isPublicPresentation(); + } + + boolean allDisplaysArePresenting = true; + for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) { + final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i); + if (displayContent.mDisplayId != dc.mDisplayId + && !mPresentations.contains(dc.mDisplayId)) { + allDisplaysArePresenting = false; + break; + } + } + if (allDisplaysArePresenting) { + // All displays can't present simultaneously. + return false; + } + + final int displayId = displayContent.mDisplayId; + if (hasPresentationWindow(displayId) + && win != null && win != mPresentations.get(displayId).mWin) { + // A display can't have multiple presentations. + return false; + } + + Task hostTask = null; + final Presentation presentation = getPresentation(win); + if (presentation != null) { + hostTask = presentation.mHostTask; + } else if (win == null) { + final Task globallyFocusedTask = + displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask(); + if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) { + hostTask = globallyFocusedTask; + } + } + if (hostTask != null && displayId == hostTask.getDisplayId()) { + // A presentation can't cover its own host task. + return false; + } + if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) { + // A globally focused host task on a different display is needed to show a + // presentation on a non-presenting display. + return false; + } + + return true; } boolean shouldOccludeActivities(int displayId) { @@ -45,32 +162,87 @@ class PresentationController { // be shown on them. // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that // the presentation won't stop its controlling activity. - return enablePresentationForConnectedDisplays() && isPresenting(displayId); + return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId); } - void onPresentationAdded(@NonNull WindowState win) { + void onPresentationAdded(@NonNull WindowState win, int uid) { final int displayId = win.getDisplayId(); - if (isPresenting(displayId)) { - return; - } ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", - win.getDisplayId(), win); - mPresentingDisplayIds.add(win.getDisplayId()); + displayId, win); win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); - } - void onPresentationRemoved(@NonNull WindowState win) { - final int displayId = win.getDisplayId(); - if (!isPresenting(displayId)) { - return; + final WindowContainerListener presentationWindowListener = new WindowContainerListener() { + @Override + public void onRemoved() { + if (!hasPresentationWindow(displayId)) { + ProtoLog.e(WM_ERROR, "Failed to remove presentation on" + + "non-presenting display %d: %s", displayId, win); + return; + } + final Presentation presentation = mPresentations.get(displayId); + win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener); + if (presentation.mHostTask != null) { + presentation.mHostTask.unregisterWindowContainerListener( + presentation.mHostTaskListener); + } + mPresentations.remove(displayId); + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/); + } + }; + win.mToken.registerWindowContainerListener(presentationWindowListener); + + Task hostTask = null; + if (enablePresentationForConnectedDisplays()) { + final Task globallyFocusedTask = + win.mWmService.mRoot.getTopDisplayFocusedRootTask(); + if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) { + hostTask = globallyFocusedTask; + } + } + WindowContainerListener hostTaskListener = null; + if (hostTask != null) { + hostTaskListener = new WindowContainerListener() { + public void onDisplayChanged(DisplayContent dc) { + final Presentation presentation = mPresentations.get(dc.getDisplayId()); + if (presentation != null && !canPresent(presentation.mWin, dc)) { + removePresentation(dc.mDisplayId, "host task moved to display " + + dc.getDisplayId()); + } + } + + public void onRemoved() { + removePresentation(win.getDisplayId(), "host task removed"); + } + }; + hostTask.registerWindowContainerListener(hostTaskListener); } - ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, - "Presentation removed from display %d: %s", win.getDisplayId(), win); - // TODO(b/393945496): Make sure that there's one presentation at most per display. - final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId); - if (displayIdIndex != -1) { - mPresentingDisplayIds.remove(displayIdIndex); + + mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask, + hostTaskListener)); + } + + void removePresentation(int displayId, @NonNull String reason) { + final Presentation presentation = mPresentations.get(displayId); + if (enablePresentationForConnectedDisplays() && presentation != null) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for " + + "reason %s", mPresentations.get(displayId), reason); + final WindowState win = presentation.mWin; + win.mWmService.mAtmService.mH.post(() -> { + synchronized (win.mWmService.mGlobalLock) { + win.removeIfPossible(); + } + }); } - win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) { + removePresentation(displayId, "display removed " + displayId); + } + + @Override + public void onDisplayChanged(int displayId) {} } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 8d198b26f396..3ed16db7e204 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -737,6 +737,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } } + @Override + public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) { + synchronized (mService.mGlobalLock) { + final WindowState win = mService.windowForClientLocked(this, window, + false /* throwOnError */); + if (win != null) { + win.setAnimatingTypes(animatingTypes); + } + } + } + void onWindowAdded(WindowState w) { if (mPackageName == null) { mPackageName = mProcess.mInfo.packageName; @@ -1015,15 +1026,4 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } } } - - @Override - public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) { - synchronized (mService.mGlobalLock) { - final WindowState win = mService.windowForClientLocked(this, window, - false /* throwOnError */); - if (win != null) { - win.notifyInsetsAnimationRunningStateChanged(running); - } - } - } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 821c04011d97..28f2825150c2 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1583,14 +1583,18 @@ public class WindowManagerService extends IWindowManager.Stub return WindowManagerGlobal.ADD_DUPLICATE_ADD; } - if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) { + if (type == TYPE_PRIVATE_PRESENTATION + && !mPresentationController.canPresent(null /*win*/, displayContent, type, + callingUid)) { ProtoLog.w(WM_ERROR, "Attempted to add private presentation window to a non-private display. " + "Aborting."); return WindowManagerGlobal.ADD_PERMISSION_DENIED; } - if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) { + if (type == TYPE_PRESENTATION + && !mPresentationController.canPresent(null /*win*/, displayContent, type, + callingUid)) { ProtoLog.w(WM_ERROR, "Attempted to add presentation window to a non-suitable display. " + "Aborting."); @@ -1830,7 +1834,8 @@ public class WindowManagerService extends IWindowManager.Stub } win.mTransitionController.collect(win.mToken); res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, - outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs, + callingUid); // A presentation hides all activities behind on the same display. win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); @@ -1841,7 +1846,8 @@ public class WindowManagerService extends IWindowManager.Stub } } else { res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, - outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs, + callingUid); } } @@ -1854,7 +1860,7 @@ public class WindowManagerService extends IWindowManager.Stub @NonNull ActivityRecord activity, @NonNull DisplayContent displayContent, @NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame, @NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client, - @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) { + @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) { int res = 0; final int type = attrs.type; boolean imMayMove = true; @@ -1971,7 +1977,7 @@ public class WindowManagerService extends IWindowManager.Stub outSizeCompatScale[0] = win.getCompatScaleForClient(); if (res >= ADD_OKAY && win.isPresentation()) { - mPresentationController.onPresentationAdded(win); + mPresentationController.onPresentationAdded(win, uid); } return res; @@ -4767,6 +4773,26 @@ public class WindowManagerService extends IWindowManager.Stub } } + @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS) + @Override + public void updateDisplayWindowAnimatingTypes(int displayId, @InsetsType int animatingTypes) { + updateDisplayWindowAnimatingTypes_enforcePermission(); + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final DisplayContent dc = mRoot.getDisplayContent(displayId); + if (dc == null || dc.mRemoteInsetsControlTarget == null) { + return; + } + dc.mRemoteInsetsControlTarget.setAnimatingTypes(animatingTypes); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + } + @Override public int watchRotation(IRotationWatcher watcher, int displayId) { final DisplayContent displayContent; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index ec67dd87533a..3b7d31274326 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -736,6 +736,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + private @InsetsType int mAnimatingTypes = 0; + /** * Freeze the insets state in some cases that not necessarily keeps up-to-date to the client. * (e.g app exiting transition) @@ -842,6 +844,27 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } + @Override + public @InsetsType int getAnimatingTypes() { + return mAnimatingTypes; + } + + @Override + public void setAnimatingTypes(@InsetsType int animatingTypes) { + if (mAnimatingTypes != animatingTypes) { + if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { + Trace.instant(TRACE_TAG_WINDOW_MANAGER, + TextUtils.formatSimple("%s: setAnimatingTypes(%s)", + getName(), + animatingTypes)); + } + mInsetsAnimationRunning = animatingTypes != 0; + mWmService.scheduleAnimationLocked(); + + mAnimatingTypes = animatingTypes; + } + } + /** * Set a freeze state for the window to ignore dispatching its insets state to the client. * @@ -2435,7 +2458,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mAnimatingExit = true; mRemoveOnExit = true; mToken.setVisibleRequested(false); - mWmService.mPresentationController.onPresentationRemoved(this); // A presentation hides all activities behind on the same display. mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); @@ -2656,7 +2678,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // The client gave us a touchable region and so first // we calculate the untouchable region, then punch that out of our // expanded modal region. - mTmpRegion.set(0, 0, frame.right, frame.bottom); + mTmpRegion.set(0, 0, frame.width(), frame.height()); mTmpRegion.op(mGivenTouchableRegion, Region.Op.DIFFERENCE); region.op(mTmpRegion, Region.Op.DIFFERENCE); } @@ -6079,17 +6101,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mWmService.scheduleAnimationLocked(); } - void notifyInsetsAnimationRunningStateChanged(boolean running) { - if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { - Trace.instant(TRACE_TAG_WINDOW_MANAGER, - TextUtils.formatSimple("%s: notifyInsetsAnimationRunningStateChanged(%s)", - getName(), - Boolean.toString(running))); - } - mInsetsAnimationRunning = running; - mWmService.scheduleAnimationLocked(); - } - boolean isInsetsAnimationRunning() { return mInsetsAnimationRunning; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index e158310455ac..860b6fb1dcd1 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1814,7 +1814,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } - if (!isWatch && !isTv && !isAutomotive + if (!isWatch && !isTv && !isAutomotive && !isDesktop && android.security.Flags.aapmApi()) { t.traceBegin("StartAdvancedProtectionService"); mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java index 5eb23a24908d..12866481b320 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java @@ -16,29 +16,43 @@ package com.android.server.am; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import android.annotation.NonNull; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.AppOpsManager; +import android.app.BackgroundStartPrivileges; +import android.app.BroadcastOptions; +import android.app.SystemServiceRegistry; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Context; +import android.content.IIntentReceiver; +import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.TestLooperManager; import android.os.UserHandle; +import android.permission.IPermissionManager; +import android.permission.PermissionManager; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; @@ -47,7 +61,6 @@ import android.util.SparseArray; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.internal.util.FrameworkStatsLog; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.AlarmManagerInternal; @@ -55,6 +68,7 @@ import com.android.server.DropBoxManagerInternal; import com.android.server.LocalServices; import com.android.server.appop.AppOpsService; import com.android.server.compat.PlatformCompat; +import com.android.server.firewall.IntentFirewall; import com.android.server.wm.ActivityTaskManagerService; import org.junit.Rule; @@ -63,8 +77,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.File; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; public abstract class BaseBroadcastQueueTest { @@ -97,6 +114,8 @@ public abstract class BaseBroadcastQueueTest { public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) .spyStatic(FrameworkStatsLog.class) .spyStatic(ProcessList.class) + .spyStatic(SystemServiceRegistry.class) + .mockStatic(AppGlobals.class) .build(); @@ -119,6 +138,16 @@ public abstract class BaseBroadcastQueueTest { ProcessList mProcessList; @Mock PlatformCompat mPlatformCompat; + @Mock + IntentFirewall mIntentFirewall; + @Mock + IPackageManager mIPackageManager; + @Mock + AppOpsManager mAppOpsManager; + @Mock + IPermissionManager mIPermissionManager; + @Mock + PermissionManager mPermissionManager; @Mock AppStartInfoTracker mAppStartInfoTracker; @@ -167,22 +196,22 @@ public abstract class BaseBroadcastQueueTest { return getUidForPackage(invocation.getArgument(0)); }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM)); + final Context spyContext = spy(mContext); + doReturn(mPermissionManager).when(spyContext).getSystemService(PermissionManager.class); final ActivityManagerService realAms = new ActivityManagerService( - new TestInjector(mContext), mServiceThreadRule.getThread()); + new TestInjector(spyContext), mServiceThreadRule.getThread()); realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal()); realAms.mOomAdjuster.mCachedAppOptimizer = mock(CachedAppOptimizer.class); realAms.mOomAdjuster = spy(realAms.mOomAdjuster); - ExtendedMockito.doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt())); + doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt())); realAms.mPackageManagerInt = mPackageManagerInt; realAms.mUsageStatsService = mUsageStatsManagerInt; realAms.mProcessesReady = true; mAms = spy(realAms); - mSkipPolicy = spy(new BroadcastSkipPolicy(mAms)); - doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any()); - doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any()); + mSkipPolicy = createBroadcastSkipPolicy(); doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker(); @@ -198,6 +227,14 @@ public abstract class BaseBroadcastQueueTest { } } + public BroadcastSkipPolicy createBroadcastSkipPolicy() { + final BroadcastSkipPolicy skipPolicy = spy(new BroadcastSkipPolicy(mAms)); + doReturn(null).when(skipPolicy).shouldSkipAtEnqueueMessage(any(), any()); + doReturn(null).when(skipPolicy).shouldSkipMessage(any(), any()); + doReturn(false).when(skipPolicy).disallowBackgroundStart(any()); + return skipPolicy; + } + static int getUidForPackage(@NonNull String packageName) { switch (packageName) { case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID; @@ -240,6 +277,11 @@ public abstract class BaseBroadcastQueueTest { public BroadcastQueue getBroadcastQueue(ActivityManagerService service) { return null; } + + @Override + public IntentFirewall getIntentFirewall() { + return mIntentFirewall; + } } abstract String getTag(); @@ -281,24 +323,35 @@ public abstract class BaseBroadcastQueueTest { ri.activityInfo.packageName = packageName; ri.activityInfo.processName = processName; ri.activityInfo.name = name; + ri.activityInfo.exported = true; ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId); return ri; } + // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord() + @SuppressWarnings("GuardedBy") + ProcessRecord makeProcessRecord(ApplicationInfo info) { + final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid)); + r.setPid(mNextPid.incrementAndGet()); + ProcessRecord.updateProcessRecordNodes(r); + return r; + } + BroadcastFilter makeRegisteredReceiver(ProcessRecord app) { return makeRegisteredReceiver(app, 0); } BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) { final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid()); - return makeRegisteredReceiver(receiverList, priority); + return makeRegisteredReceiver(receiverList, priority, null); } - static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) { + static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority, + String requiredPermission) { final IntentFilter filter = new IntentFilter(); filter.setPriority(priority); final BroadcastFilter res = new BroadcastFilter(filter, receiverList, - receiverList.app.info.packageName, null, null, null, receiverList.uid, + receiverList.app.info.packageName, null, null, requiredPermission, receiverList.uid, receiverList.userId, false, false, true, receiverList.app.info, mock(PlatformCompat.class)); receiverList.add(res); @@ -313,4 +366,62 @@ public abstract class BaseBroadcastQueueTest { ArgumentMatcher<ApplicationInfo> appInfoEquals(int uid) { return test -> (test.uid == uid); } + + static final class BroadcastRecordBuilder { + private BroadcastQueue mQueue = mock(BroadcastQueue.class); + private Intent mIntent = mock(Intent.class); + private ProcessRecord mProcessRecord = mock(ProcessRecord.class); + private String mCallerPackage; + private String mCallerFeatureId; + private int mCallingPid; + private int mCallingUid; + private boolean mCallerInstantApp; + private String mResolvedType; + private String[] mRequiredPermissions; + private String[] mExcludedPermissions; + private String[] mExcludedPackages; + private int mAppOp; + private BroadcastOptions mOptions = BroadcastOptions.makeBasic(); + private List mReceivers = Collections.emptyList(); + private ProcessRecord mResultToApp; + private IIntentReceiver mResultTo; + private int mResultCode = Activity.RESULT_OK; + private String mResultData; + private Bundle mResultExtras; + private boolean mSerialized; + private boolean mSticky; + private boolean mInitialSticky; + private int mUserId = UserHandle.USER_SYSTEM; + private BackgroundStartPrivileges mBackgroundStartPrivileges = + BackgroundStartPrivileges.NONE; + private boolean mTimeoutExempt; + private BiFunction<Integer, Bundle, Bundle> mFilterExtrasForReceiver; + private int mCallerAppProcState = ActivityManager.PROCESS_STATE_UNKNOWN; + private PlatformCompat mPlatformCompat = mock(PlatformCompat.class); + + public BroadcastRecordBuilder setIntent(Intent intent) { + mIntent = intent; + return this; + } + + public BroadcastRecordBuilder setRequiredPermissions(String[] requiredPermissions) { + mRequiredPermissions = requiredPermissions; + return this; + } + + public BroadcastRecordBuilder setAppOp(int appOp) { + mAppOp = appOp; + return this; + } + + public BroadcastRecord build() { + return new BroadcastRecord(mQueue, mIntent, mProcessRecord, mCallerPackage, + mCallerFeatureId, mCallingPid, mCallingUid, mCallerInstantApp, mResolvedType, + mRequiredPermissions, mExcludedPermissions, mExcludedPackages, mAppOp, + mOptions, mReceivers, mResultToApp, mResultTo, mResultCode, mResultData, + mResultExtras, mSerialized, mSticky, mInitialSticky, mUserId, + mBackgroundStartPrivileges, mTimeoutExempt, mFilterExtrasForReceiver, + mCallerAppProcState, mPlatformCompat); + } + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java index 409706b14c56..b32ce49d049d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java @@ -1803,8 +1803,10 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked()); } + @SuppressWarnings("GuardedBy") + @DisableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE) @Test - public void testSkipPolicy_atEnqueueTime() throws Exception { + public void testSkipPolicy_atEnqueueTime_flagDisabled() throws Exception { final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT); final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN); final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED); @@ -1839,6 +1841,44 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(redQueue, List.of(userPresent, timeTick)); } + @SuppressWarnings("GuardedBy") + @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE) + @Test + public void testSkipPolicy_atEnqueueTime() throws Exception { + final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT); + final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN); + final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED); + + final BroadcastRecord userPresentRecord = makeBroadcastRecord(userPresent, + List.of(greenReceiver, redReceiver)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, + List.of(greenReceiver, redReceiver)); + + doAnswer(invocation -> { + final BroadcastRecord r = invocation.getArgument(0); + final Object o = invocation.getArgument(1); + if (userPresent.getAction().equals(r.intent.getAction()) + && isReceiverEquals(o, greenReceiver)) { + return "receiver skipped by test"; + } + return null; + }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any()); + + mImpl.enqueueBroadcastLocked(userPresentRecord); + mImpl.enqueueBroadcastLocked(timeTickRecord); + + final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + // There should be only one broadcast for green process as the other would have + // been skipped. + verifyPendingRecords(greenQueue, List.of(timeTick)); + final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED, + getUidForPackage(PACKAGE_RED)); + verifyPendingRecords(redQueue, List.of(userPresent, timeTick)); + } + @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE) @Test public void testDeliveryDeferredForCached_flagDisabled() throws Exception { @@ -2270,19 +2310,11 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { assertFalse(mImpl.isProcessFreezable(greenProcess)); } - // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord() - private ProcessRecord makeProcessRecord(ApplicationInfo info) { - final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid)); - r.setPid(mNextPid.incrementAndGet()); - ProcessRecord.updateProcessRecordNodes(r); - return r; - } - BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) { final IIntentReceiver receiver = mock(IIntentReceiver.class); final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid, UserHandle.getUserId(app.info.uid), receiver); - return makeRegisteredReceiver(receiverList, priority); + return makeRegisteredReceiver(receiverList, priority, null /* requiredPermission */); } private Intent createPackageChangedIntent(int uid, List<String> componentNameList) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index ad35b25a0d74..3a9c99d57d71 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -2301,6 +2301,52 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { } /** + * Verify that we skip broadcasts at enqueue if {@link BroadcastSkipPolicy} decides it + * should be skipped. + */ + @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE) + @Test + public void testSkipPolicy_atEnqueueTime() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + final Object greenReceiver = makeRegisteredReceiver(receiverGreenApp); + final Object blueReceiver = makeRegisteredReceiver(receiverBlueApp); + final Object yellowReceiver = makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW); + final Object orangeReceiver = makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE); + + doAnswer(invocation -> { + final BroadcastRecord r = invocation.getArgument(0); + final Object o = invocation.getArgument(1); + if (airplane.getAction().equals(r.intent.getAction()) + && (isReceiverEquals(o, greenReceiver) + || isReceiverEquals(o, orangeReceiver))) { + return "test skipped receiver"; + } + return null; + }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any()); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(greenReceiver, blueReceiver, yellowReceiver, orangeReceiver))); + + waitForIdle(); + // Verify that only blue and yellow receiver apps received the broadcast. + verifyScheduleRegisteredReceiver(never(), receiverGreenApp, USER_SYSTEM); + verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class), + eq(greenReceiver)); + verifyScheduleRegisteredReceiver(receiverBlueApp, airplane); + final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW, + getUidForPackage(PACKAGE_YELLOW)); + verifyScheduleReceiver(receiverYellowApp, airplane); + final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE, + getUidForPackage(PACKAGE_ORANGE)); + assertNull(receiverOrangeApp); + verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class), + eq(orangeReceiver)); + } + + /** * Verify broadcasts to runtime receivers in cached processes are deferred * until that process leaves the cached state. */ diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java new file mode 100644 index 000000000000..c8aad79edd12 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java @@ -0,0 +1,305 @@ +/* + * 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.am; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.never; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.AppOpsManager; +import android.content.IIntentReceiver; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +@SmallTest +public class BroadcastSkipPolicyTest extends BaseBroadcastQueueTest { + private static final String TAG = "BroadcastSkipPolicyTest"; + + BroadcastSkipPolicy mBroadcastSkipPolicy; + + @Before + public void setUp() throws Exception { + super.setUp(); + mBroadcastSkipPolicy = new BroadcastSkipPolicy(mAms); + + doReturn(true).when(mIntentFirewall).checkBroadcast(any(Intent.class), + anyInt(), anyInt(), nullable(String.class), anyInt()); + + doReturn(mIPackageManager).when(AppGlobals::getPackageManager); + doReturn(true).when(mIPackageManager).isPackageAvailable(anyString(), anyInt()); + + doReturn(ActivityManager.APP_START_MODE_NORMAL).when(mAms).getAppStartModeLOSP(anyInt(), + anyString(), anyInt(), anyInt(), eq(true), eq(false), eq(false)); + + doReturn(mAppOpsManager).when(mAms).getAppOpsManager(); + doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).checkOpNoThrow(anyString(), + anyInt(), anyString(), nullable(String.class)); + doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOpNoThrow(anyString(), + anyInt(), anyString(), nullable(String.class), anyString()); + + doReturn(mIPermissionManager).when(AppGlobals::getPermissionManager); + doReturn(PackageManager.PERMISSION_GRANTED).when(mIPermissionManager).checkUidPermission( + anyInt(), anyString(), anyInt()); + } + + @Override + public String getTag() { + return TAG; + } + + @Override + public BroadcastSkipPolicy createBroadcastSkipPolicy() { + return new BroadcastSkipPolicy(mAms); + } + + @Test + public void testShouldSkipMessage_withManifestRcvr_withCompPerm_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .build(); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, + makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN, + Manifest.permission.PACKAGE_USAGE_STATS)); + assertNull(msg); + verify(mAppOpsManager).noteOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId), + anyString()); + verify(mAppOpsManager, never()).checkOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class)); + } + + @Test + public void testShouldSkipMessage_withRegRcvr_withCompPerm_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, + makeRegisteredReceiver(receiverApp, 0 /* priority */, + Manifest.permission.PACKAGE_USAGE_STATS)); + assertNull(msg); + verify(mAppOpsManager).noteOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId), + anyString()); + verify(mAppOpsManager, never()).checkOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class)); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withCompPerm_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .build(); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, + makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN, + Manifest.permission.PACKAGE_USAGE_STATS)); + assertNull(msg); + verify(mAppOpsManager).checkOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId)); + verify(mAppOpsManager, never()).noteOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class), anyString()); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withRegRcvr_withCompPerm_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, + makeRegisteredReceiver(receiverApp, 0 /* priority */, + Manifest.permission.PACKAGE_USAGE_STATS)); + assertNull(msg); + verify(mAppOpsManager).checkOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId)); + verify(mAppOpsManager, never()).noteOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class), anyString()); + } + + @Test + public void testShouldSkipMessage_withManifestRcvr_withAppOp_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS)) + .build(); + final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, receiver); + assertNull(msg); + verify(mAppOpsManager).noteOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(receiver.activityInfo.applicationInfo.uid), + eq(receiver.activityInfo.packageName), nullable(String.class), anyString()); + verify(mAppOpsManager, never()).checkOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class)); + } + + @Test + public void testShouldSkipMessage_withRegRcvr_withAppOp_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS)) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */, + null /* requiredPermission */); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, filter); + assertNull(msg); + verify(mAppOpsManager).noteOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(filter.receiverList.uid), + eq(filter.packageName), nullable(String.class), anyString()); + verify(mAppOpsManager, never()).checkOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class)); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withAppOp_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS)) + .build(); + final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, receiver); + assertNull(msg); + verify(mAppOpsManager).checkOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(receiver.activityInfo.applicationInfo.uid), + eq(receiver.activityInfo.applicationInfo.packageName), nullable(String.class)); + verify(mAppOpsManager, never()).noteOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class), anyString()); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withRegRcvr_withAppOp_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS)) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */, + null /* requiredPermission */); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, filter); + assertNull(msg); + verify(mAppOpsManager).checkOpNoThrow( + eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)), + eq(filter.receiverList.uid), + eq(filter.packageName), nullable(String.class)); + verify(mAppOpsManager, never()).noteOpNoThrow( + anyString(), anyInt(), anyString(), nullable(String.class), anyString()); + } + + @Test + public void testShouldSkipMessage_withManifestRcvr_withRequiredPerms_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS}) + .build(); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)); + assertNull(msg); + verify(mPermissionManager).checkPermissionForDataDelivery( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString()); + verify(mPermissionManager, never()).checkPermissionForPreflight( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any()); + } + + @Test + public void testShouldSkipMessage_withRegRcvr_withRequiredPerms_invokesNoteOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS}) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, + makeRegisteredReceiver(receiverApp, 0 /* priority */, + null /* requiredPermission */)); + assertNull(msg); + verify(mPermissionManager).checkPermissionForDataDelivery( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString()); + verify(mPermissionManager, never()).checkPermissionForPreflight( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any()); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withRequiredPerms_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS}) + .build(); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, + makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)); + assertNull(msg); + verify(mPermissionManager, never()).checkPermissionForDataDelivery( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString()); + verify(mPermissionManager).checkPermissionForPreflight( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any()); + } + + @Test + public void testShouldSkipAtEnqueueMessage_withRegRcvr_withRequiredPerms_invokesCheckOp() { + final BroadcastRecord record = new BroadcastRecordBuilder() + .setIntent(new Intent(Intent.ACTION_TIME_TICK)) + .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS}) + .build(); + final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); + final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, + makeRegisteredReceiver(receiverApp, 0 /* priority */, + null /* requiredPermission */)); + assertNull(msg); + verify(mPermissionManager, never()).checkPermissionForDataDelivery( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString()); + verify(mPermissionManager).checkPermissionForPreflight( + eq(Manifest.permission.PACKAGE_USAGE_STATS), any()); + } + + private ResolveInfo makeManifestReceiverWithPermission(String packageName, String name, + String permission) { + final ResolveInfo resolveInfo = makeManifestReceiver(packageName, name); + resolveInfo.activityInfo.permission = permission; + return resolveInfo; + } + + private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority, + String requiredPermission) { + final IIntentReceiver receiver = mock(IIntentReceiver.class); + final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid, + UserHandle.getUserId(app.info.uid), receiver); + return makeRegisteredReceiver(receiverList, priority, requiredPermission); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index bada337c7aa6..6b8ef88c556c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -64,7 +64,6 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; -import android.content.res.Resources; import android.graphics.Color; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; @@ -95,6 +94,7 @@ import com.android.internal.R; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; +import com.android.server.wm.DesktopModeHelper; import com.android.server.wm.WindowManagerInternal; import org.hamcrest.CoreMatchers; @@ -155,8 +155,6 @@ public class WallpaperManagerServiceTests { private IPackageManager mIpm = AppGlobals.getPackageManager(); - private Resources mResources = sContext.getResources(); - @Mock private DisplayManager mDisplayManager; @@ -178,6 +176,7 @@ public class WallpaperManagerServiceTests { .spyStatic(WallpaperUtils.class) .spyStatic(LocalServices.class) .spyStatic(WallpaperManager.class) + .spyStatic(DesktopModeHelper.class) .startMocking(); sWindowManagerInternal = mock(WindowManagerInternal.class); @@ -246,6 +245,8 @@ public class WallpaperManagerServiceTests { int userId = (invocation.getArgument(0)); return getWallpaperTestDir(userId); }).when(() -> WallpaperUtils.getWallpaperDir(anyInt())); + ExtendedMockito.doAnswer(invocation -> true).when( + () -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any())); sContext.addMockSystemService(DisplayManager.class, mDisplayManager); @@ -257,10 +258,6 @@ public class WallpaperManagerServiceTests { doReturn(displays).when(mDisplayManager).getDisplays(); spyOn(mIpm); - spyOn(mResources); - doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); - doReturn(true).when(mResources).getBoolean( - eq(R.bool.config_canInternalDisplayHostDesktops)); mService = new TestWallpaperManagerService(sContext); spyOn(mService); mService.systemReady(); diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index a4e77c00d647..1de864cb4eb0 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -17,9 +17,9 @@ package com.android.server.location.contexthub; import static com.google.common.truth.Truth.assertThat; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,15 +33,21 @@ import android.hardware.contexthub.HubMessage; import android.hardware.contexthub.IContextHubEndpoint; import android.hardware.contexthub.IContextHubEndpointCallback; import android.hardware.contexthub.IEndpointCommunication; +import android.hardware.contexthub.Message; import android.hardware.contexthub.MessageDeliveryStatus; import android.hardware.contexthub.Reason; +import android.hardware.location.IContextHubTransactionCallback; +import android.hardware.location.NanoAppState; import android.os.Binder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; - +import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; +import java.util.Collections; +import java.util.List; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -51,11 +57,11 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.Collections; - @RunWith(AndroidJUnit4.class) @Presubmit public class ContextHubEndpointTest { + private static final String TAG = "ContextHubEndpointTest"; + private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE; private static final int MIN_SESSION_ID = 0; private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1; @@ -206,6 +212,68 @@ public class ContextHubEndpointTest { assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); } + @Test + public void testMessageTransaction() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true); + + unregisterExampleEndpoint(endpoint); + } + + @Test + public void testMessageTransactionCleanupOnUnregistration() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ false); + + unregisterExampleEndpoint(endpoint); + assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0); + } + + /** A helper method to create a session and validates reliable message sending. */ + private void testMessageTransactionInternal( + IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException { + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + + final int messageType = 1234; + HubMessage message = + new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5}) + .setResponseRequired(true) + .build(); + IContextHubTransactionCallback callback = + new IContextHubTransactionCallback.Stub() { + @Override + public void onQueryResponse(int result, List<NanoAppState> nanoappList) { + Log.i(TAG, "Received onQueryResponse callback, result=" + result); + } + + @Override + public void onTransactionComplete(int result) { + Log.i(TAG, "Received onTransactionComplete callback, result=" + result); + } + }; + endpoint.sendMessage(sessionId, message, callback); + ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mMockEndpointCommunications, timeout(1000)) + .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture()); + Message halMessage = messageCaptor.getValue(); + assertThat(halMessage.type).isEqualTo(message.getMessageType()); + assertThat(halMessage.content).isEqualTo(message.getMessageBody()); + assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1); + + if (deliverMessageStatus) { + mEndpointManager.onMessageDeliveryStatusReceived( + sessionId, halMessage.sequenceNumber, ErrorCode.OK); + assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0); + } + } + private IContextHubEndpoint registerExampleEndpoint() throws RemoteException { HubEndpointInfo info = new HubEndpointInfo( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java index b33233107766..6b989cb0aaee 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java @@ -38,6 +38,7 @@ import android.os.IInterface; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.Condition; +import com.android.internal.R; import com.android.server.UiServiceTestCase; import org.junit.Before; @@ -46,6 +47,8 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + public class ConditionProvidersTest extends UiServiceTestCase { private ConditionProviders mProviders; @@ -169,4 +172,15 @@ public class ConditionProvidersTest extends UiServiceTestCase { assertTrue(mProviders.getApproved(userId, true).isEmpty()); } + + @Test + public void getDefaultDndAccessPackages_returnsPackages() { + mContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultDndAccessPackages, + "com.example.a:com.example.b::::com.example.c"); + + List<String> packages = ConditionProviders.getDefaultDndAccessPackages(mContext); + + assertThat(packages).containsExactly("com.example.a", "com.example.b", "com.example.c"); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java new file mode 100644 index 000000000000..154a905c776b --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java @@ -0,0 +1,124 @@ +/* + * 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.notification; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.R; +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ZenConfigTrimmerTest extends UiServiceTestCase { + + private static final String TRUSTED_PACKAGE = "com.trust.me"; + private static final int ONE_PERCENT = 1_500; + + private ZenConfigTrimmer mTrimmer; + + @Before + public void setUp() { + mContext.getOrCreateTestableResources().addOverride( + R.string.config_defaultDndAccessPackages, TRUSTED_PACKAGE); + + mTrimmer = new ZenConfigTrimmer(mContext); + } + + @Test + public void trimToMaximumSize_belowMax_untouched() { + ZenModeConfig config = new ZenModeConfig(); + addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT); + addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT); + + mTrimmer.trimToMaximumSize(config); + + assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + public void trimToMaximumSize_exceedsMax_removesAllRulesOfLargestPackages() { + ZenModeConfig config = new ZenModeConfig(); + addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT); + addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT); + addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT); + addZenRule(config, "7", "pkg4", 38 * ONE_PERCENT); + + mTrimmer.trimToMaximumSize(config); + + assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "6"); + assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct()) + .containsExactly("pkg1", "pkg3"); + } + + @Test + public void trimToMaximumSize_keepsRulesFromTrustedPackages() { + ZenModeConfig config = new ZenModeConfig(); + addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT); + addZenRule(config, "4", TRUSTED_PACKAGE, 60 * ONE_PERCENT); + addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT); + addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT); + + mTrimmer.trimToMaximumSize(config); + + assertThat(config.automaticRules.keySet()).containsExactly("4", "5"); + assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct()) + .containsExactly(TRUSTED_PACKAGE, "pkg2"); + } + + /** + * Create a ZenRule that, when serialized to a Parcel, will take <em>approximately</em> + * {@code desiredSize} bytes (within 100 bytes). Try to make the tests not rely on a very tight + * fit. + */ + private static void addZenRule(ZenModeConfig config, String id, String pkg, int desiredSize) { + ZenRule rule = new ZenRule(); + rule.id = id; + rule.pkg = pkg; + config.automaticRules.put(id, rule); + + // Make the ZenRule as large as desired. Not to the exact byte, because otherwise this + // test would have to be adjusted whenever we change the parceling of ZenRule in any way. + // (Still might need adjustment if we change the serialization _significantly_). + int nameLength = desiredSize - id.length() - pkg.length() - 232; + rule.name = "A".repeat(nameLength); + + Parcel verification = Parcel.obtain(); + try { + verification.writeParcelable(rule, 0); + assertThat(verification.dataSize()).isWithin(100).of(desiredSize); + } finally { + verification.recycle(); + } + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index f8387a4c54cc..51891ef71bbd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -90,6 +90,7 @@ import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG; import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; +import static com.android.server.notification.Flags.FLAG_LIMIT_ZEN_CONFIG_SIZE; import static com.android.server.notification.Flags.FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING; import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; @@ -236,6 +237,7 @@ import java.util.stream.Collectors; @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWith(ParameterizedAndroidJunit4.class) +@EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) // Should be parameterization, but off path does nothing. @TestableLooper.RunWithLooper public class ZenModeHelperTest extends UiServiceTestCase { @@ -7480,6 +7482,45 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(getZenRule(ruleId).lastActivation).isNull(); } + @Test + @EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) + public void addAutomaticZenRule_trimsConfiguration() { + mZenModeHelper.mConfig.automaticRules.clear(); + AutomaticZenRule smallRule = new AutomaticZenRule.Builder("Reasonable", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .build(); + AutomaticZenRule systemRule = new AutomaticZenRule.Builder("System", CONDITION_ID) + .setOwner(new ComponentName("android", "ScheduleConditionProvider")) + .build(); + + AutomaticZenRule bigRule = new AutomaticZenRule.Builder("Yuge", CONDITION_ID) + .setConfigurationActivity(new ComponentName("evil.package", "cls")) + .setTriggerDescription("0123456789".repeat(6000)) // ~60k bytes utf16. + .build(); + + String systemRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", + systemRule, ORIGIN_SYSTEM, "add", SYSTEM_UID); + String smallRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, smallRule, + ORIGIN_APP, "add", CUSTOM_PKG_UID); + String bigRuleId1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package", + bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( + systemRuleId, smallRuleId, bigRuleId1); + + String bigRuleId2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package", + bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( + systemRuleId, smallRuleId, bigRuleId1, bigRuleId2); + + // This should go over the threshold + String bigRuleId3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package", + bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID); + + // Rules from evil.package are gone. + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( + systemRuleId, smallRuleId); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); diff --git a/services/tests/wmtests/src/com/android/server/TransitionSubject.java b/services/tests/wmtests/src/com/android/server/TransitionSubject.java new file mode 100644 index 000000000000..07026b98f226 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/TransitionSubject.java @@ -0,0 +1,79 @@ +/* + * 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.wm; + +import android.annotation.Nullable; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.IterableSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.Truth; + +import java.util.ArrayList; +import java.util.List; + +public class TransitionSubject extends Subject { + + @Nullable + private final Transition actual; + + /** + * Internal constructor. + * + * @see TransitionSubject#assertThat(Transition) + */ + private TransitionSubject(FailureMetadata metadata, @Nullable Transition actual) { + super(metadata, actual); + this.actual = actual; + } + + /** + * In a fluent assertion chain, the argument to the "custom" overload of {@link + * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies + * what kind of {@link Subject} to create. + */ + public static Factory<TransitionSubject, Transition> transitions() { + return TransitionSubject::new; + } + + /** + * Typical entry point for making assertions about Transitions. + * + * @see @Truth#assertThat(Object) + */ + public static TransitionSubject assertThat(Transition transition) { + return Truth.assertAbout(transitions()).that(transition); + } + + /** + * Converts to a {@link IterableSubject} containing {@link Transition#getFlags()} separated into + * a list of individual flags for assertions such as {@code flags().contains(TRANSIT_FLAG_XYZ)}. + * + * <p>If the subject is null, this will fail instead of returning a null subject. + */ + public IterableSubject flags() { + isNotNull(); + + final List<Integer> sortedFlags = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + if ((actual.getFlags() & (1 << i)) != 0) { + sortedFlags.add((1 << i)); + } + } + return com.google.common.truth.Truth.assertThat(sortedFlags); + } +} 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 cfd501abbe8b..61ed0b53cdcf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -63,6 +63,9 @@ import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; @@ -80,6 +83,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; +import static com.android.server.wm.TransitionSubject.assertThat; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT; @@ -147,6 +151,7 @@ import com.android.internal.logging.nano.MetricsProto; import com.android.server.LocalServices; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.utils.WmDisplayCutout; +import com.android.window.flags.Flags; import org.junit.Test; import org.junit.runner.RunWith; @@ -2620,6 +2625,7 @@ public class DisplayContentTests extends WindowTestsBase { final KeyguardController keyguard = mAtm.mKeyguardController; final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); final int displayId = mDisplayContent.getDisplayId(); + final TestTransitionPlayer transitions = registerTestTransitionPlayer(); final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); @@ -2629,21 +2635,40 @@ public class DisplayContentTests extends WindowTestsBase { keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); assertFalse(keyguardGoingAway.getAsBoolean()); assertFalse(appVisible.getAsBoolean()); + transitions.flush(); // Start unlocking from AOD. keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); assertTrue(keyguardGoingAway.getAsBoolean()); assertTrue(appVisible.getAsBoolean()); + if (Flags.ensureKeyguardDoesTransitionStarting()) { + assertThat(transitions.mLastTransit).isNull(); + } else { + assertThat(transitions.mLastTransit).flags() + .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY); + } + transitions.flush(); + // Clear AOD. This does *not* clear the going-away status. keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); assertTrue(keyguardGoingAway.getAsBoolean()); assertTrue(appVisible.getAsBoolean()); + if (Flags.aodTransition()) { + assertThat(transitions.mLastTransit).flags() + .containsExactly(TRANSIT_FLAG_AOD_APPEARING); + } else { + assertThat(transitions.mLastTransit).isNull(); + } + transitions.flush(); + // Finish unlock keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */); assertFalse(keyguardGoingAway.getAsBoolean()); assertTrue(appVisible.getAsBoolean()); + + assertThat(transitions.mLastTransit).isNull(); } @Test @@ -2653,6 +2678,7 @@ public class DisplayContentTests extends WindowTestsBase { final KeyguardController keyguard = mAtm.mKeyguardController; final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); final int displayId = mDisplayContent.getDisplayId(); + final TestTransitionPlayer transitions = registerTestTransitionPlayer(); final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); @@ -2662,22 +2688,44 @@ public class DisplayContentTests extends WindowTestsBase { keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); assertFalse(keyguardGoingAway.getAsBoolean()); assertFalse(appVisible.getAsBoolean()); + transitions.flush(); // Start unlocking from AOD. keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); assertTrue(keyguardGoingAway.getAsBoolean()); assertTrue(appVisible.getAsBoolean()); + if (!Flags.ensureKeyguardDoesTransitionStarting()) { + assertThat(transitions.mLastTransit).flags() + .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY); + } + transitions.flush(); + // Clear AOD. This does *not* clear the going-away status. keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); assertTrue(keyguardGoingAway.getAsBoolean()); assertTrue(appVisible.getAsBoolean()); + if (Flags.aodTransition()) { + assertThat(transitions.mLastTransit).flags() + .containsExactly(TRANSIT_FLAG_AOD_APPEARING); + } else { + assertThat(transitions.mLastTransit).isNull(); + } + transitions.flush(); + // 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()); + + if (Flags.ensureKeyguardDoesTransitionStarting()) { + assertThat(transitions.mLastTransit).isNull(); + } else { + assertThat(transitions.mLastTransit).flags() + .containsExactly(TRANSIT_FLAG_KEYGUARD_APPEARING); + } } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java index 2d4101e40615..6e0f7fbbf388 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -16,9 +16,12 @@ package com.android.server.wm; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_PRESENTATION; +import static android.view.Display.FLAG_TRUSTED; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_WAKE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; @@ -30,6 +33,7 @@ import static org.mockito.ArgumentMatchers.eq; import android.annotation.NonNull; import android.graphics.Rect; +import android.os.Binder; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; @@ -118,6 +122,112 @@ public class PresentationControllerTests extends WindowTestsBase { assertFalse(window.isAttached()); } + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationCannotCoverHostTask() { + int uid = Binder.getCallingUid(); + final DisplayContent presentationDisplay = createPresentationDisplay(); + final Task task = createTask(presentationDisplay); + task.effectiveUid = uid; + final ActivityRecord activity = createActivityRecord(task); + assertTrue(activity.isVisible()); + + // Adding a presentation window over its host task must fail. + assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId); + + // Adding a presentation window on the other display must succeed. + final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + completeTransition(addTransition, /*abortSync=*/ true); + assertTrue(window.isVisible()); + + // Moving the host task to the presenting display will remove the presentation. + task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true); + waitHandlerIdle(window.mWmService.mAtmService.mH); + final Transition removeTransition = window.mTransitionController.getCollectingTransition(); + assertEquals(TRANSIT_CLOSE, removeTransition.mType); + completeTransition(removeTransition, /*abortSync=*/ false); + assertFalse(window.isVisible()); + } + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationCannotLaunchOnAllDisplays() { + final int uid = Binder.getCallingUid(); + final DisplayContent presentationDisplay = createPresentationDisplay(); + final Task task = createTask(presentationDisplay); + task.effectiveUid = uid; + final ActivityRecord activity = createActivityRecord(task); + assertTrue(activity.isVisible()); + + // Add a presentation window on the default display. + final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + completeTransition(addTransition, /*abortSync=*/ true); + assertTrue(window.isVisible()); + + // Adding another presentation window over the task even if it's a different UID because + // it would end up showing presentations on all displays. + assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId); + } + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() { + final int uid = Binder.getCallingUid(); + // Adding a presentation window on an internal display requires a host task + // with global focus on another display. + assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY); + + final DisplayContent presentationDisplay = createPresentationDisplay(); + final Task taskWiSameUid = createTask(presentationDisplay); + taskWiSameUid.effectiveUid = uid; + final ActivityRecord activity = createActivityRecord(taskWiSameUid); + assertTrue(activity.isVisible()); + final Task taskWithDifferentUid = createTask(presentationDisplay); + taskWithDifferentUid.effectiveUid = uid + 1; + createActivityRecord(taskWithDifferentUid); + assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask()); + + // The task with the same UID is covered by another task with a different UID, so this must + // also fail. + assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY); + + // Moving the task with the same UID to front and giving it global focus allows a + // presentation to show on the default display. + taskWiSameUid.moveToFront("test"); + final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + completeTransition(addTransition, /*abortSync=*/ true); + assertTrue(window.isVisible()); + } + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testReparentingActivityToSameDisplayClosesPresentation() { + final int uid = Binder.getCallingUid(); + final Task task = createTask(mDefaultDisplay); + task.effectiveUid = uid; + final ActivityRecord activity = createActivityRecord(task); + assertTrue(activity.isVisible()); + + // Add a presentation window on a presentation display. + final DisplayContent presentationDisplay = createPresentationDisplay(); + final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId()); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + completeTransition(addTransition, /*abortSync=*/ true); + assertTrue(window.isVisible()); + + // Reparenting the host task below the presentation must close the presentation. + task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true); + waitHandlerIdle(window.mWmService.mAtmService.mH); + final Transition removeTransition = window.mTransitionController.getCollectingTransition(); + // It's a WAKE transition instead of CLOSE because + assertEquals(TRANSIT_WAKE, removeTransition.mType); + completeTransition(removeTransition, /*abortSync=*/ false); + assertFalse(window.isVisible()); + } + private WindowState addPresentationWindow(int uid, int displayId) { final Session session = createTestSession(mAtm, 1234 /* pid */, uid); final int userId = UserHandle.getUserId(uid); @@ -134,10 +244,29 @@ public class PresentationControllerTests extends WindowTestsBase { return window; } + private void assertAddPresentationWindowFails(int uid, int displayId) { + final Session session = createTestSession(mAtm, 1234 /* pid */, uid); + final IWindow clientWindow = new TestIWindow(); + final int res = addPresentationWindowInner(uid, displayId, session, clientWindow); + assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res); + } + + private int addPresentationWindowInner(int uid, int displayId, Session session, + IWindow clientWindow) { + final int userId = UserHandle.getUserId(uid); + doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_PRESENTATION); + return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId, + WindowInsets.Type.defaultVisible(), null, new InsetsState(), + new InsetsSourceControl.Array(), new Rect(), new float[1]); + } + private DisplayContent createPresentationDisplay() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; + displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED; + displayInfo.displayId = DEFAULT_DISPLAY + 1; final DisplayContent dc = createNewDisplay(displayInfo); final int displayId = dc.getDisplayId(); doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index 45436e47e881..d3f3269392d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -33,6 +33,7 @@ import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.view.Display.Mode; import android.view.Surface; +import android.view.WindowInsets; import android.view.WindowManager.LayoutParams; import androidx.test.filters.SmallTest; @@ -283,7 +284,7 @@ public class RefreshRatePolicyTest extends WindowTestsBase { assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); - overrideWindow.notifyInsetsAnimationRunningStateChanged(true); + overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars()); assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow)); assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote); @@ -303,7 +304,7 @@ public class RefreshRatePolicyTest extends WindowTestsBase { assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); - overrideWindow.notifyInsetsAnimationRunningStateChanged(true); + overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars()); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index c0642f5533eb..57ab13ffee89 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -2151,6 +2151,14 @@ public class WindowTestsBase extends SystemServiceTestsBase { mLastRequest = null; } + void flush() { + if (mLastTransit != null) { + start(); + finish(); + clear(); + } + } + @Override public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo, SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT) diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index ca4a643d7b20..ae7346eb6df4 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -1737,13 +1737,8 @@ public class EuiccManager { private int getCardIdForDefaultEuicc() { int cardId = TelephonyManager.UNINITIALIZED_CARD_ID; - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - PackageManager pm = mContext.getPackageManager(); - if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) { - TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); - cardId = tm.getCardIdForDefaultEuicc(); - } - } else { + PackageManager pm = mContext.getPackageManager(); + if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) { TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); cardId = tm.getCardIdForDefaultEuicc(); } diff --git a/tools/processors/view_inspector/OWNERS b/tools/processors/view_inspector/OWNERS index 0473f54e57ca..38d21e141f43 100644 --- a/tools/processors/view_inspector/OWNERS +++ b/tools/processors/view_inspector/OWNERS @@ -1,3 +1,2 @@ alanv@google.com -ashleyrose@google.com -aurimas@google.com
\ No newline at end of file +aurimas@google.com |