diff options
389 files changed, 9325 insertions, 3014 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 2dd16de3e188..4d9fdf041d58 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -587,6 +587,7 @@ aconfig_declarations { java_aconfig_library { name: "android.view.inputmethod.flags-aconfig-java", aconfig_declarations: "android.view.inputmethod.flags-aconfig", + host_supported: true, defaults: ["framework-minus-apex-aconfig-java-defaults"], } diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java index 26e20f601c7a..6542d08d6384 100644 --- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java +++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java @@ -89,6 +89,11 @@ public class UsbCommand extends Svc.Command { IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService( Context.USB_SERVICE)); + if (usbMgr == null) { + System.err.println("Could not obtain USB service. Try again later."); + return; + } + Executor executor = context.getMainExecutor(); Consumer<Integer> consumer = new Consumer<Integer>(){ public void accept(Integer status){ diff --git a/core/api/test-current.txt b/core/api/test-current.txt index bb023f25094e..12bfccf2172c 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -4507,7 +4507,7 @@ package android.window { method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int); - method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken); + method @Deprecated @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams); method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index 5f57a41675c9..d5b2f980e1a6 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -110,8 +110,7 @@ public class AnimationHandler { } }; - public static final ThreadLocal<AnimationHandler> sAnimatorHandler = - ThreadLocal.withInitial(AnimationHandler::new); + public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>(); private static AnimationHandler sTestHandler = null; private boolean mListDirty = false; @@ -119,6 +118,9 @@ public class AnimationHandler { if (sTestHandler != null) { return sTestHandler; } + if (sAnimatorHandler.get() == null) { + sAnimatorHandler.set(new AnimationHandler()); + } return sAnimatorHandler.get(); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 73de1b67dc66..c74bd1a092ee 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6981,6 +6981,8 @@ public class DevicePolicyManager { * <p>The caller must hold the * {@link android.Manifest.permission#TRIGGER_LOST_MODE} permission. * + * <p>This API accesses the device's location and will only be used when a device is lost. + * * <p>Register a broadcast receiver to receive lost mode location updates. This receiver should * subscribe to the {@link #ACTION_LOST_MODE_LOCATION_UPDATE} action and receive the location * from an intent extra {@link #EXTRA_LOST_MODE_LOCATION}. diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 5c267c9f6475..4afe75f7814c 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -265,6 +265,16 @@ flag { } flag { + name: "expanding_public_view" + namespace: "systemui" + description: "enables user expanding the public view of a notification" + bug: "398853084" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "api_rich_ongoing" is_exported: true namespace: "systemui" diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 55d78f9b8c48..cc288b1f5601 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -744,15 +744,22 @@ public abstract class Context { */ public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L; + /** + * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible. + * + * @hide + */ + public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L; /** * These bind flags reduce the strength of the binding such that we shouldn't * consider it as pulling the process up to the level of the one that is bound to it. * @hide */ - public static final int BIND_REDUCTION_FLAGS = + public static final long BIND_REDUCTION_FLAGS = Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE; + | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE + | Context.BIND_ALLOW_FREEZE; /** @hide */ @IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = { diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index bcb7ebfb286f..6e9dcf5a83a1 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -1714,7 +1714,7 @@ public final class CameraManager { final TaskInfo taskInfo = appTask.getTaskInfo(); final int freeformCameraCompatMode = taskInfo.appCompatTaskInfo .cameraCompatTaskInfo.freeformCameraCompatMode; - if (freeformCameraCompatMode != 0 + if (isInCameraCompatMode(freeformCameraCompatMode) && taskInfo.topActivity != null && taskInfo.topActivity.getPackageName().equals(packageName)) { // WindowManager has requested rotation override. @@ -1741,6 +1741,12 @@ public final class CameraManager { : ICameraService.ROTATION_OVERRIDE_NONE; } + private static boolean isInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int + freeformCameraCompatMode) { + return (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED) + && (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE); + } + private static int getRotationOverrideForCompatFreeform( @CameraCompatTaskInfo.FreeformCameraCompatMode int freeformCameraCompatMode) { // Only rotate-and-crop if the app and device orientations do not match. diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl index 509b9482154e..4da991ee85b1 100644 --- a/core/java/android/hardware/input/IKeyGestureHandler.aidl +++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl @@ -28,15 +28,4 @@ interface IKeyGestureHandler { * to that gesture. */ boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken); - - /** - * Called to know if a particular gesture type is supported by the handler. - * - * TODO(b/358569822): Remove this call to reduce the binder calls to single call for - * handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if - * a key gesture is supported on first relevant key down. - * Also, for now we prioritize handlers in the system server process above external handlers to - * reduce IPC binder calls. - */ - boolean isKeyGestureSupported(int gestureType); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 49db54d81e65..d6419afb2a5a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1758,13 +1758,6 @@ public final class InputManager { */ boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, @Nullable IBinder focusedToken); - - /** - * Called to identify if a particular gesture is of interest to a handler. - * - * NOTE: If no active handler supports certain gestures, the gestures will not be captured. - */ - boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType); } /** @hide */ diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index a9a45ae45ec3..c4b4831ba76e 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -1193,23 +1193,6 @@ public final class InputManagerGlobal { } return false; } - - @Override - public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - synchronized (mKeyGestureEventHandlerLock) { - if (mKeyGestureEventHandlers == null) { - return false; - } - final int numHandlers = mKeyGestureEventHandlers.size(); - for (int i = 0; i < numHandlers; i++) { - KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i); - if (handler.isKeyGestureSupported(gestureType)) { - return true; - } - } - } - return false; - } } /** diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 1a712d2b3f31..9dd1fed4a85a 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -108,7 +108,8 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55; public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56; public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57; - public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58; + @Deprecated + public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58; public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59; public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60; public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61; @@ -200,7 +201,6 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD, KEY_GESTURE_TYPE_GLOBAL_ACTIONS, - KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, @@ -777,8 +777,6 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD"; case KEY_GESTURE_TYPE_GLOBAL_ACTIONS: return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS"; - case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD"; case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT"; case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT: diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index ae0b56e6f009..45a21beabd89 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -44,6 +44,16 @@ flag { } flag { + name: "notify_single_item_change_on_icon_load" + namespace: "intentresolver" + description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)." + bug: "298193161" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "fix_resolver_memory_leak" is_exported: true namespace: "intentresolver" diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 44c3f9a8244e..0152c52a6753 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -75,9 +75,12 @@ public abstract class Layout { // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f; private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f; - private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f; // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8 private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f; + @VisibleForTesting + static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f; + @VisibleForTesting + static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f; /** @hide */ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { @@ -1030,7 +1033,9 @@ public abstract class Layout { var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); - var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP; + var cornerRadius = Math.max( + mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP, + mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR); // We set the alpha on the color itself instead of Paint.setAlpha(), because that function // actually mutates the color in... *ehem* very strange ways. Also the color might get reset diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 7c1e4976b9d3..3a2ec91b3b20 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -967,8 +967,11 @@ public final class Choreographer { DisplayEventReceiver.VsyncEventData vsyncEventData) { final long startNanos; final long frameIntervalNanos = vsyncEventData.frameInterval; - boolean resynced = false; + // Original intended vsync time that is not adjusted by jitter + // or buffer stuffing recovery. Reported for jank tracking. + final long intendedFrameTimeNanos = frameTimeNanos; long offsetFrameTimeNanos = frameTimeNanos; + boolean resynced = false; // Evaluate if buffer stuffing recovery needs to start or end, and // what actions need to be taken for recovery. @@ -1012,7 +1015,6 @@ public final class Choreographer { + ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms"); } - long intendedFrameTimeNanos = offsetFrameTimeNanos; startNanos = System.nanoTime(); // Calculating jitter involves using the original frame time without // adjustments from buffer stuffing diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 6f346bdae70a..394ac8f8c6e9 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1986,7 +1986,11 @@ 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 | (mAnimatingTypes & ime()); + if (Flags.reportAnimatingInsetsTypes()) { + typesToReport = mRequestedVisibleTypes; + } else { + typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime()); + } } else { typesToReport = mRequestedVisibleTypes; } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index eca798d6eb4f..290885593ee6 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -379,7 +379,7 @@ final class IInputMethodManagerGlobalInvoker { @Nullable IRemoteInputConnection remoteInputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { final IInputMethodManager service = getService(); if (service == null) { return InputBindResult.NULL; @@ -388,7 +388,7 @@ final class IInputMethodManagerGlobalInvoker { return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, - imeDispatcher); + imeDispatcher, imeRequestedVisible); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -408,7 +408,8 @@ final class IInputMethodManagerGlobalInvoker { @Nullable IRemoteInputConnection remoteInputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + boolean useAsyncShowHideMethod) { final IInputMethodManager service = getService(); if (service == null) { return -1; @@ -417,7 +418,8 @@ final class IInputMethodManagerGlobalInvoker { service.startInputOrWindowGainedFocusAsync(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, - imeDispatcher, advanceAngGetStartInputSequenceNumber(), useAsyncShowHideMethod); + imeDispatcher, imeRequestedVisible, advanceAngGetStartInputSequenceNumber(), + useAsyncShowHideMethod); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0b34600f4104..a41ab368aed8 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -871,6 +871,19 @@ public final class InputMethodManager { IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible); } + private static boolean hasViewImeRequestedVisible(View view) { + // before the refactor, the requestedVisibleTypes for the IME were not in sync with + // the state that was actually requested. + if (Flags.refactorInsetsController() && view != null) { + final var controller = view.getWindowInsetsController(); + if (controller != null) { + return (view.getWindowInsetsController() + .getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0; + } + } + return false; + } + private final class DelegateImpl implements ImeFocusController.InputMethodManagerDelegate { @@ -941,6 +954,9 @@ public final class InputMethodManager { Log.v(TAG, "Reporting focus gain, without startInput"); } + final boolean imeRequestedVisible = hasViewImeRequestedVisible( + mCurRootView.getView()); + // ignore the result Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus"); IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( @@ -950,7 +966,7 @@ public final class InputMethodManager { null, null, null, mCurRootView.mContext.getApplicationInfo().targetSdkVersion, - UserHandle.myUserId(), mImeDispatcher); + UserHandle.myUserId(), mImeDispatcher, imeRequestedVisible); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } @@ -2441,9 +2457,8 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_NO_ONGOING_USER_ANIMATION); if (resultReceiver != null) { - final boolean imeReqVisible = - (viewRootImpl.getInsetsController().getRequestedVisibleTypes() - & WindowInsets.Type.ime()) != 0; + final boolean imeReqVisible = hasViewImeRequestedVisible( + viewRootImpl.getView()); resultReceiver.send( imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN : InputMethodManager.RESULT_SHOWN, null); @@ -2656,9 +2671,8 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); - final boolean imeReqVisible = - (viewRootImpl.getInsetsController().getRequestedVisibleTypes() - & WindowInsets.Type.ime()) != 0; + final boolean imeReqVisible = hasViewImeRequestedVisible( + viewRootImpl.getView()); if (resultReceiver != null) { resultReceiver.send( !imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_HIDDEN @@ -3412,6 +3426,7 @@ public final class InputMethodManager { final Handler icHandler; InputBindResult res = null; final boolean hasServedView; + final boolean imeRequestedVisible; synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. @@ -3479,10 +3494,13 @@ public final class InputMethodManager { } mServedInputConnection = servedInputConnection; + imeRequestedVisible = hasViewImeRequestedVisible(servedView); + if (DEBUG) { Log.v(TAG, "START INPUT: view=" + InputMethodDebug.dumpViewInfo(view) + " ic=" + ic + " editorInfo=" + editorInfo + " startInputFlags=" - + InputMethodDebug.startInputFlagsToString(startInputFlags)); + + InputMethodDebug.startInputFlagsToString(startInputFlags) + + " imeRequestedVisible=" + imeRequestedVisible); } // When we switch between non-editable views, do not call into the IMMS. @@ -3513,7 +3531,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher, mAsyncShowHideMethodEnabled); + mImeDispatcher, imeRequestedVisible, mAsyncShowHideMethodEnabled); } else { res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, @@ -3521,7 +3539,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher); + mImeDispatcher, imeRequestedVisible); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (Flags.useZeroJankProxy()) { diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 3f611c7efbdd..a328c78f3738 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -72,7 +72,7 @@ import java.util.function.Predicate; /** * <p>Displays a vertically-scrollable collection of views, where each view is positioned - * immediatelybelow the previous view in the list. For a more modern, flexible, and performant + * immediately below the previous view in the list. For a more modern, flexible, and performant * approach to displaying lists, use {@link androidx.recyclerview.widget.RecyclerView}.</p> * * <p>To display a list, you can include a list view in your layout XML file:</p> diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index 4f89c831a05f..866c16cb566d 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -50,6 +50,8 @@ public enum DesktopExperienceFlags { ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, true), ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false), ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false), + ENABLE_CONNECTED_DISPLAYS_WALLPAPER( + android.app.Flags::enableConnectedDisplaysWallpaper, false), ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true), ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT( com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement, diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index ea345a5ef46a..485e7b33f3a7 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -684,7 +684,10 @@ public final class WindowContainerTransaction implements Parcelable { * organizer. * @param root1 the first root. * @param root2 the second root. + * @deprecated replace with {@link #setAdjacentRootSet} */ + @SuppressWarnings("UnflaggedApi") // @TestApi without associated feature. + @Deprecated @NonNull public WindowContainerTransaction setAdjacentRoots( @NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) { @@ -704,7 +707,7 @@ public final class WindowContainerTransaction implements Parcelable { * * @param roots the Tasks that should be adjacent to each other. * @throws IllegalArgumentException if roots have size < 2. - * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS. + * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS in 25Q4. */ @NonNull public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) { @@ -1003,7 +1006,7 @@ public final class WindowContainerTransaction implements Parcelable { /** * Sets to TaskFragments adjacent to each other. Containers below two visible adjacent * TaskFragments will be made invisible. This is similar to - * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with + * {@link #setAdjacentRootSet(WindowContainerToken...)}, but can be used with * fragmentTokens when that TaskFragments haven't been created (but will be created in the same * {@link WindowContainerTransaction}). * @param fragmentToken1 client assigned unique token to create TaskFragment with specified diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index cbeaeda65fca..94e87c7a034b 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -105,17 +105,6 @@ flag { flag { namespace: "windowing_sdk" - name: "allow_multiple_adjacent_task_fragments" - description: "Refactor to allow more than 2 adjacent TaskFragments" - bug: "373709676" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - namespace: "windowing_sdk" name: "track_system_ui_context_before_wms" description: "Keep track of SystemUiContext before WMS is initialized" bug: "384428048" diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index c009fc3b7e63..9bc6671bbc31 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; import static android.content.ContentProvider.getUriWithoutUserId; import static android.content.ContentProvider.getUserIdFromUri; +import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; @@ -163,9 +164,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -3212,6 +3215,8 @@ public class ChooserActivity extends ResolverActivity implements private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20; + private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>(); + ChooserGridAdapter(ChooserListAdapter wrappedAdapter) { super(); mChooserListAdapter = wrappedAdapter; @@ -3232,6 +3237,31 @@ public class ChooserActivity extends ResolverActivity implements notifyDataSetChanged(); } }); + if (notifySingleItemChangeOnIconLoad()) { + wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded); + } + } + + private void onTargetIconLoaded(DisplayResolveInfo info) { + for (ViewHolderBase holder : mBoundViewHolders) { + switch (holder.getViewType()) { + case VIEW_TYPE_NORMAL: + TargetInfo itemInfo = + mChooserListAdapter.getItem( + ((ItemViewHolder) holder).mListPosition); + if (info == itemInfo) { + notifyItemChanged(holder.getAdapterPosition()); + } + break; + case VIEW_TYPE_CALLER_AND_RANK: + ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder; + if (suggestedAppsGroupContainsTarget(groupHolder, info)) { + notifyItemChanged(holder.getAdapterPosition()); + } + break; + } + + } } public void setFooterHeight(int height) { @@ -3382,6 +3412,9 @@ public class ChooserActivity extends ResolverActivity implements @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.add((ViewHolderBase) holder); + } int viewType = ((ViewHolderBase) holder).getViewType(); switch (viewType) { case VIEW_TYPE_DIRECT_SHARE: @@ -3396,6 +3429,22 @@ public class ChooserActivity extends ResolverActivity implements } @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.remove((ViewHolderBase) holder); + } + super.onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.remove((ViewHolderBase) holder); + } + return super.onFailedToRecycleView(holder); + } + + @Override public int getItemViewType(int position) { int count; @@ -3604,6 +3653,33 @@ public class ChooserActivity extends ResolverActivity implements } } + /** + * Checks whether the suggested apps group, {@code holder}, contains the target, + * {@code info}. + */ + private boolean suggestedAppsGroupContainsTarget( + ItemGroupViewHolder holder, DisplayResolveInfo info) { + + int position = holder.getAdapterPosition(); + int start = getListPosition(position); + int startType = getRowType(start); + + int columnCount = holder.getColumnCount(); + int end = start + columnCount - 1; + while (getRowType(end) != startType && end >= start) { + end--; + } + + for (int i = 0; i < columnCount; i++) { + if (start + i <= end) { + if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) { + return true; + } + } + } + return false; + } + int getListPosition(int position) { position -= getSystemRowCount() + getProfileRowCount(); diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index d38689c7505b..1b8c36db3908 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -16,9 +16,12 @@ package com.android.internal.app; +import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad; + import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import android.annotation.Nullable; import android.app.prediction.AppPredictor; import android.content.ComponentName; import android.content.Context; @@ -56,6 +59,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; public class ChooserListAdapter extends ResolverListAdapter { private static final String TAG = "ChooserListAdapter"; @@ -108,6 +112,9 @@ public class ChooserListAdapter extends ResolverListAdapter { // Represents the UserSpace in which the Initial Intents should be resolved. private final UserHandle mInitialIntentsUserSpace; + @Nullable + private Consumer<DisplayResolveInfo> mOnIconLoadedListener; + // For pinned direct share labels, if the text spans multiple lines, the TextView will consume // the full width, even if the characters actually take up less than that. Measure the actual // line widths and constrain the View's width based upon that so that the pin doesn't end up @@ -218,6 +225,10 @@ public class ChooserListAdapter extends ResolverListAdapter { true); } + public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) { + mOnIconLoadedListener = onIconLoadedListener; + } + AppPredictor getAppPredictor() { return mAppPredictor; } @@ -329,6 +340,15 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + @Override + protected void onIconLoaded(DisplayResolveInfo info) { + if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) { + mOnIconLoadedListener.accept(info); + } else { + notifyDataSetChanged(); + } + } + private void loadDirectShareIcon(SelectableTargetInfo info) { LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); if (task == null) { diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 54c0e61fd5cd..4d9ce86096c7 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -680,6 +680,10 @@ public class ResolverListAdapter extends BaseAdapter { } } + protected void onIconLoaded(DisplayResolveInfo info) { + notifyDataSetChanged(); + } + private void loadLabel(DisplayResolveInfo info) { LoadLabelTask task = mLabelLoaders.get(info); if (task == null) { @@ -1004,7 +1008,7 @@ public class ResolverListAdapter extends BaseAdapter { mResolverListCommunicator.updateProfileViewButton(); } else if (!mDisplayResolveInfo.hasDisplayIcon()) { mDisplayResolveInfo.setDisplayIcon(d); - notifyDataSetChanged(); + onIconLoaded(mDisplayResolveInfo); } } } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9380d99b7de3..0791612fa0e8 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -105,7 +105,7 @@ interface IInputMethodManager { in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection, in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, int userId, - in ImeOnBackInvokedDispatcher imeDispatcher); + in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible); // If windowToken is null, this just does startInput(). Otherwise this reports that a window // has gained focus, and if 'editorInfo' is non-null then also does startInput. @@ -120,8 +120,8 @@ interface IInputMethodManager { in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection, in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, int userId, - in ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod); + in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod); void showInputMethodPickerFromClient(in IInputMethodClient client, int auxiliarySubtypeMode); @@ -224,7 +224,7 @@ interface IInputMethodManager { * async **/ oneway void acceptStylusHandwritingDelegationAsync(in IInputMethodClient client, in int userId, in String delegatePackageName, in String delegatorPackageName, int flags, - in IBooleanListener callback); + in IBooleanListener callback); /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 8c5b20da73ef..7a38dce296de 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4184,6 +4184,11 @@ <!-- Whether device supports double tap to wake --> <bool name="config_supportDoubleTapWake">false</bool> + <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable + double tap gestures in non-action areas in the lock screen and launcher workspace to go to + sleep. --> + <bool name="config_supportDoubleTapSleep">false</bool> + <!-- The RadioAccessFamilies supported by the device. Empty is viewed as "all". Only used on devices which don't support RIL_REQUEST_GET_RADIO_CAPABILITY diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3de30f7f25a8..46d18e3d3302 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3142,6 +3142,7 @@ <java-symbol type="color" name="chooser_row_divider" /> <java-symbol type="layout" name="chooser_row_direct_share" /> <java-symbol type="bool" name="config_supportDoubleTapWake" /> + <java-symbol type="bool" name="config_supportDoubleTapSleep" /> <java-symbol type="drawable" name="ic_perm_device_info" /> <java-symbol type="string" name="config_radio_access_family" /> <java-symbol type="string" name="notification_inbox_ellipsis" /> diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index 9e78af57b470..11ec9f8e1912 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -16,6 +16,9 @@ package android.text; +import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR; +import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP; + import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT; import static org.junit.Assert.assertArrayEquals; @@ -1073,6 +1076,68 @@ public class LayoutTest { } } + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() { + mTextPaint.setColor(Color.BLACK); + mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP + Layout layout = new StaticLayout("Test text", mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + + final float expectedRoundedRectSize = + mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP; + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + if (drawCommand.rect != null) { + expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize); + expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize); + } + } + } + + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() { + mTextPaint.setColor(Color.BLACK); + mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP + Layout layout = new StaticLayout("Test text", mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + + final float expectedRoundedRectSize = + mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR; + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + if (drawCommand.rect != null) { + expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize); + expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize); + } + } + } + private int removeAlpha(int color) { return Color.rgb( Color.red(color), @@ -1087,6 +1152,8 @@ public class LayoutTest { public final String text; public final float x; public final float y; + public final float rX; + public final float rY; public final Path path; public final RectF rect; public final Paint paint; @@ -1098,6 +1165,8 @@ public class LayoutTest { this.paint = new Paint(paint); path = null; rect = null; + this.rX = 0; + this.rY = 0; } DrawCommand(Path path, Paint paint) { @@ -1107,15 +1176,19 @@ public class LayoutTest { x = 0; text = null; rect = null; + this.rX = 0; + this.rY = 0; } - DrawCommand(RectF rect, Paint paint) { + DrawCommand(RectF rect, Paint paint, float rX, float rY) { this.rect = new RectF(rect); this.paint = new Paint(paint); path = null; y = 0; x = 0; text = null; + this.rX = rX; + this.rY = rY; } @Override @@ -1189,12 +1262,12 @@ public class LayoutTest { @Override public void drawRect(RectF rect, Paint p) { - mDrawCommands.add(new DrawCommand(rect, p)); + mDrawCommands.add(new DrawCommand(rect, p, 0, 0)); } @Override public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) { - mDrawCommands.add(new DrawCommand(rect, paint)); + mDrawCommands.add(new DrawCommand(rect, paint, rx, ry)); } List<DrawCommand> getDrawCommands() { diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java index db69cf2397fc..02a296892c53 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java @@ -72,7 +72,8 @@ public class ChooserActivityWorkProfileTest { private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry .getInstrumentation().getTargetContext().getUser(); - private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + private static final UserHandle WORK_USER_HANDLE = + UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1); @Rule public ActivityTestRule<ChooserWrapperActivity> mActivityRule = diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 03076c0940a4..50666911e313 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -51,6 +51,7 @@ android_robolectric_test { "androidx.test.ext.junit", "mockito-robolectric-prebuilt", "mockito-kotlin2", + "platform-parametric-runner-lib", "truth", "flag-junit-base", "flag-junit", @@ -74,6 +75,7 @@ android_test { "frameworks-base-testutils", "mockito-kotlin2", "mockito-target-extended-minus-junit4", + "platform-parametric-runner-lib", "truth", "platform-test-annotations", "platform-test-rules", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt new file mode 100644 index 000000000000..bdfaef2c6960 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt @@ -0,0 +1,73 @@ +/* + * 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.bubbles + +import android.content.ComponentName +import android.content.Context +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.wm.shell.Flags +import com.android.wm.shell.taskview.TaskView +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +/** Tests for [BubbleExpandedView] */ +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleExpandedViewTest(flags: FlagsParameterization) { + + @get:Rule + val setFlagsRule = SetFlagsRule(flags) + + private val context = ApplicationProvider.getApplicationContext<Context>() + private val componentName = ComponentName(context, "TestClass") + + @Test + fun getTaskId_onTaskCreated_returnsCorrectTaskId() { + val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor()) + val expandedView = BubbleExpandedView(context).apply { + initialize( + mock<BubbleExpandedViewManager>(), + mock<BubbleStackView>(), + mock<BubblePositioner>(), + false /* isOverflow */, + bubbleTaskView, + ) + setAnimating(true) // Skips setContentVisibility for testing. + } + + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(expandedView.getTaskId()).isEqualTo(123) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams() = FlagsParameterization.allCombinationsOf( + Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER, + ) + } +} 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 9087da34d259..636ff669d6b4 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 @@ -266,8 +266,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() @@ -295,8 +293,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() @@ -324,8 +320,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() 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 c2aa146d6437..3ebe87dcf505 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 @@ -38,11 +38,11 @@ android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp"> + android:gravity="center_horizontal" + android:layout_marginHorizontal="4dp"> <Button - android:layout_width="94dp" + android:layout_width="108dp" android:layout_height="60dp" android:id="@+id/maximize_menu_immersive_toggle_button" style="?android:attr/buttonBarButtonStyle" @@ -75,8 +75,7 @@ android:layout_weight="1" android:orientation="vertical" android:gravity="center_horizontal" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp"> + android:layout_marginHorizontal="4dp"> <Button android:layout_width="108dp" @@ -112,8 +111,7 @@ android:layout_weight="1" android:orientation="vertical" android:gravity="center_horizontal" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp"> + android:layout_marginHorizontal="4dp"> <LinearLayout android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 2179128df300..5ef83826840b 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -226,7 +226,7 @@ <string name="windowing_app_handle_education_tooltip">The app menu can be found here</string> <!-- App handle education tooltip text for tooltip pointing to windowing image button --> - <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop view to open multiple apps together</string> + <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop windowing to open multiple apps together</string> <!-- App handle education tooltip text for tooltip pointing to app chip --> <string name="windowing_desktop_mode_exit_education_tooltip">Return to full screen anytime from the app menu</string> @@ -293,7 +293,7 @@ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> <string name="fullscreen_text">Fullscreen</string> <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> - <string name="desktop_text">Desktop View</string> + <string name="desktop_text">Desktop windowing</string> <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> <string name="split_screen_text">Split Screen</string> <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> @@ -319,7 +319,7 @@ <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> - <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string> + <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop windowing)</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> @@ -348,7 +348,7 @@ <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] --> <string name="app_handle_chip_accessibility_announce">Open Menu</string> <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string> + <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop windowing">%1$s</xliff:g></string> <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt new file mode 100644 index 000000000000..9bf56b075112 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt @@ -0,0 +1,86 @@ +/* + * 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.shared + +import android.graphics.Typeface +import android.widget.TextView +import com.android.wm.shell.Flags + +/** + * Utility class to apply a specified typeface to a [TextView]. + * + * This class provides a method, [setTypeface], + * to easily set a pre-defined font family and style to a given [TextView]. + */ +class TypefaceUtils { + + enum class FontFamily(val value: String) { + GSF_DISPLAY_LARGE("variable-display-large"), + GSF_DISPLAY_MEDIUM("variable-display-medium"), + GSF_DISPLAY_SMALL("variable-display-small"), + GSF_HEADLINE_LARGE("variable-headline-large"), + GSF_HEADLINE_MEDIUM("variable-headline-medium"), + GSF_HEADLINE_SMALL("variable-headline-small"), + GSF_TITLE_LARGE("variable-title-large"), + GSF_TITLE_MEDIUM("variable-title-medium"), + GSF_TITLE_SMALL("variable-title-small"), + GSF_LABEL_LARGE("variable-label-large"), + GSF_LABEL_MEDIUM("variable-label-medium"), + GSF_LABEL_SMALL("variable-label-small"), + GSF_BODY_LARGE("variable-body-large"), + GSF_BODY_MEDIUM("variable-body-medium"), + GSF_BODY_SMALL("variable-body-small"), + GSF_DISPLAY_LARGE_EMPHASIZED("variable-display-large-emphasized"), + GSF_DISPLAY_MEDIUM_EMPHASIZED("variable-display-medium-emphasized"), + GSF_DISPLAY_SMALL_EMPHASIZED("variable-display-small-emphasized"), + GSF_HEADLINE_LARGE_EMPHASIZED("variable-headline-large-emphasized"), + GSF_HEADLINE_MEDIUM_EMPHASIZED("variable-headline-medium-emphasized"), + GSF_HEADLINE_SMALL_EMPHASIZED("variable-headline-small-emphasized"), + GSF_TITLE_LARGE_EMPHASIZED("variable-title-large-emphasized"), + GSF_TITLE_MEDIUM_EMPHASIZED("variable-title-medium-emphasized"), + GSF_TITLE_SMALL_EMPHASIZED("variable-title-small-emphasized"), + GSF_LABEL_LARGE_EMPHASIZED("variable-label-large-emphasized"), + GSF_LABEL_MEDIUM_EMPHASIZED("variable-label-medium-emphasized"), + GSF_LABEL_SMALL_EMPHASIZED("variable-label-small-emphasized"), + GSF_BODY_LARGE_EMPHASIZED("variable-body-large-emphasized"), + GSF_BODY_MEDIUM_EMPHASIZED("variable-body-medium-emphasized"), + GSF_BODY_SMALL_EMPHASIZED("variable-body-small-emphasized"), + } + + companion object { + /** + * Sets the typeface of the provided [textView] to the specified [fontFamily] and [fontStyle]. + * + * The typeface is only applied to the [TextView] when [Flags.enableGsf] is `true`. + * If [Flags.enableGsf] is `false`, this method has no effect. + * + * @param textView The [TextView] to which the typeface should be applied. If `null`, this method does nothing. + * @param fontFamily The desired [FontFamily] for the [TextView]. + * @param fontStyle The desired font style (e.g., [Typeface.NORMAL], [Typeface.BOLD], [Typeface.ITALIC]). Defaults to [Typeface.NORMAL]. + */ + @JvmStatic + @JvmOverloads + fun setTypeface( + textView: TextView?, + fontFamily: FontFamily, + fontStyle: Int = Typeface.NORMAL, + ) { + if (!Flags.enableGsf()) return + textView?.typeface = Typeface.create(fontFamily.name, fontStyle) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 2c2451cab999..8ac9230c36c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -238,7 +238,6 @@ public class BubbleExpandedView extends LinearLayout { mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Intent fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, @@ -467,6 +466,11 @@ public class BubbleExpandedView extends LinearLayout { new BubbleTaskViewListener.Callback() { @Override public void onTaskCreated() { + // The taskId is saved to use for removeTask, + // preventing appearance in recent tasks. + mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener) + .getTaskId(); + setContentVisibility(true); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java index 63d713495177..9c20e3af9ab4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java @@ -130,7 +130,6 @@ public class BubbleTaskViewListener implements TaskView.Listener { mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Intent fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); // First try get pending intent from the bubble PendingIntent pi = mBubble.getPendingIntent(); if (pi == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index d5f2dbdbf5f5..51a5b12edb84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -639,7 +639,7 @@ public class BubbleTransitions { @Override public void continueCollapse() { mBubble.cleanupTaskView(); - if (mTaskLeash == null) return; + if (mTaskLeash == null || !mTaskLeash.isValid()) return; SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.reparent(mTaskLeash, mRootLeash); t.apply(); 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 6c840f020f90..bdb21f246359 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 @@ -447,9 +447,9 @@ public class BubbleBarLayerView extends FrameLayout bubble.cleanupViews(!inTransition); endAction.run(); }; - if (mBubbleData.getBubbles().isEmpty()) { - // we're removing the last bubble. collapse the expanded view and cleanup bubble views - // at the end. + if (mBubbleData.getBubbles().isEmpty() || inTransition) { + // If we are removing the last bubble or removing the current bubble via transition, + // collapse the expanded view and clean up bubbles at the end. collapse(cleanUp); } else { cleanUp.run(); 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 dd2050a5fd5d..f73788486d04 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 @@ -440,11 +440,18 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged statsToken); } - // In case of a hide, the statsToken should not been send yet (as the animation - // is still ongoing). It will be sent at the end of the animation - boolean hideAnimOngoing = !mImeRequestedVisible && mAnimation != null; - setVisibleDirectly(mImeRequestedVisible || mAnimation != null, - hideAnimOngoing ? null : statsToken); + boolean hideAnimOngoing; + boolean reportVisible; + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + hideAnimOngoing = false; + reportVisible = mImeRequestedVisible; + } else { + // In case of a hide, the statsToken should not been send yet (as the animation + // is still ongoing). It will be sent at the end of the animation. + hideAnimOngoing = !mImeRequestedVisible && mAnimation != null; + reportVisible = mImeRequestedVisible || mAnimation != null; + } + setVisibleDirectly(reportVisible, hideAnimOngoing ? null : statsToken); } } @@ -628,7 +635,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); } if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { - setAnimating(true); + setAnimating(true /* imeAnimationOngoing */); } int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY), imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW, @@ -678,7 +685,7 @@ 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); + setAnimating(false /* imeAnimationOngoing */); } 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 478fd6d83661..bc2ed3f35b45 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 @@ -759,6 +759,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopUserRepositories desktopUserRepositories, + DesktopRepositoryInitializer desktopRepositoryInitializer, Optional<DesktopImmersiveController> desktopImmersiveController, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, @@ -805,6 +806,7 @@ public abstract class WMShellModule { dragToDesktopTransitionHandler, desktopImmersiveController.get(), desktopUserRepositories, + desktopRepositoryInitializer, recentsTransitionHandler, multiInstanceHelper, mainExecutor, @@ -1313,10 +1315,12 @@ public abstract class WMShellModule { static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler( Context context, ShellInit shellInit, + @ShellMainThread CoroutineScope mainScope, DisplayController displayController, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, - Optional<DesktopDisplayModeController> desktopDisplayModeController + Optional<DesktopDisplayModeController> desktopDisplayModeController, + DesktopRepositoryInitializer desktopRepositoryInitializer ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1325,7 +1329,9 @@ public abstract class WMShellModule { new DesktopDisplayEventHandler( context, shellInit, + mainScope, displayController, + desktopRepositoryInitializer, desktopUserRepositories.get(), desktopTasksController.get(), desktopDisplayModeController.get())); @@ -1466,7 +1472,9 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, IWindowManager windowManager, ShellTaskOrganizer shellTaskOrganizer, - DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + InputManager inputManager, + @ShellMainThread Handler mainHandler ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1478,7 +1486,9 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, windowManager, shellTaskOrganizer, - desktopWallpaperActivityTokenProvider)); + desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler)); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index afc48acad4f5..683b74392fa6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -23,15 +23,21 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch /** Handles display events in desktop mode */ class DesktopDisplayEventHandler( private val context: Context, shellInit: ShellInit, + private val mainScope: CoroutineScope, private val displayController: DisplayController, + private val desktopRepositoryInitializer: DesktopRepositoryInitializer, private val desktopUserRepositories: DesktopUserRepositories, private val desktopTasksController: DesktopTasksController, private val desktopDisplayModeController: DesktopDisplayModeController, @@ -61,15 +67,19 @@ class DesktopDisplayEventHandler( logV("Display #$displayId does not support desks") return } - logV("Creating new desk in new display#$displayId") - // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the - // task is recreated and received in [FreeformTaskListener] before this display callback - // is invoked, which results in the repository trying to add the task to a desk before the - // desk has been recreated here, which may result in a crash-loop if the repository is - // checking that the desk exists before adding a task to it. See b/391984373. - desktopTasksController.createDesk(displayId) - // TODO: b/393978539 - consider activating the desk on creation when applicable, such as - // for connected displays. + + mainScope.launch { + desktopRepositoryInitializer.isInitialized.collect { initialized -> + if (!initialized) return@collect + if (desktopRepository.getNumberOfDesks(displayId) == 0) { + logV("Creating new desk in new display#$displayId") + // TODO: b/393978539 - consider activating the desk on creation when + // applicable, such as for connected displays. + desktopTasksController.createDesk(displayId) + } + cancel() + } + } } override fun onDisplayRemoved(displayId: Int) { 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 e89aafe267ed..904d86282c39 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 @@ -22,6 +22,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.windowingModeToString import android.content.Context +import android.hardware.input.InputManager +import android.os.Handler import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -29,11 +31,13 @@ import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction +import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions /** Controls the display windowing mode in desktop mode */ @@ -44,8 +48,26 @@ class DesktopDisplayModeController( private val windowManager: IWindowManager, private val shellTaskOrganizer: ShellTaskOrganizer, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val inputManager: InputManager, + @ShellMainThread private val mainHandler: Handler, ) { + private val onTabletModeChangedListener = + object : InputManager.OnTabletModeChangedListener { + override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) { + refreshDisplayWindowingMode() + } + } + + init { + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + inputManager.registerOnTabletModeChangedListener( + onTabletModeChangedListener, + mainHandler, + ) + } + } + fun refreshDisplayWindowingMode() { if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return @@ -89,10 +111,20 @@ class DesktopDisplayModeController( transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } - private fun getTargetWindowingModeForDefaultDisplay(): Int { + @VisibleForTesting + fun getTargetWindowingModeForDefaultDisplay(): Int { if (isExtendedDisplayEnabled() && hasExternalDisplay()) { return WINDOWING_MODE_FREEFORM } + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + if (isInClamshellMode()) { + return WINDOWING_MODE_FREEFORM + } + return WINDOWING_MODE_FULLSCREEN + } + + // If form factor-based desktop first switch is disabled, use the default display windowing + // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC). return windowManager.getWindowingMode(DEFAULT_DISPLAY) } @@ -108,6 +140,8 @@ class DesktopDisplayModeController( private fun hasExternalDisplay() = rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } + private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF + 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/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt index 5269318943d9..1ea545f3ab67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt @@ -56,7 +56,6 @@ class DesktopModeKeyGestureHandler( override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean { if ( - !isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent || !desktopModeWindowDecorViewModel.isPresent ) { @@ -136,19 +135,6 @@ class DesktopModeKeyGestureHandler( } } - override fun isKeyGestureSupported(gestureType: Int): Boolean = - when (gestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> - enableMoveToNextDisplayShortcut() - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> - DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue && - manageKeyGestures() - else -> false - } - // TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it // will pick a wrong task when a user quickly perform other actions with keyboard shortcuts // after moveToNextDisplay, and move this to FocusTransitionObserver class. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 70539902f651..b3b4d59090e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -32,6 +32,7 @@ import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; +import android.view.Display; import android.view.SurfaceControl; import android.window.DesktopModeFlags; @@ -114,6 +115,7 @@ public class DesktopModeVisualIndicator { private final Context mContext; private final DisplayController mDisplayController; private final ActivityManager.RunningTaskInfo mTaskInfo; + private final Display mDisplay; private IndicatorType mCurrentType; private final DragStartState mDragStartState; @@ -145,9 +147,10 @@ public class DesktopModeVisualIndicator { mCurrentType = NO_INDICATOR; mDragStartState = dragStartState; mSnapEventHandler = snapEventHandler; + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); mVisualIndicatorViewContainer.createView( mContext, - mDisplayController.getDisplay(mTaskInfo.displayId), + mDisplay, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mTaskInfo, taskSurface @@ -175,6 +178,7 @@ public class DesktopModeVisualIndicator { /** Start the fade-in animation. */ void fadeInIndicator() { + if (mCurrentType == NO_INDICATOR) return; mVisualIndicatorViewContainer.fadeInIndicator( mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, mTaskInfo.displayId); @@ -193,7 +197,7 @@ public class DesktopModeVisualIndicator { if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR; IndicatorType result; if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() - && !DesktopModeStatus.canEnterDesktopMode(mContext)) { + && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(mContext, mDisplay)) { // If desktop is not available, default to "no indicator" result = NO_INDICATOR; } else { 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 f64bd757de3b..8636bc1f56c2 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 @@ -716,12 +716,15 @@ class DesktopRepository( } /** - * Returns the top transparent fullscreen task id for a given display's active desk, or null. + * Returns the top transparent fullscreen task id for a given display, or null. * * TODO: b/389960283 - add explicit [deskId] argument. */ fun getTopTransparentFullscreenTaskId(displayId: Int): Int? = - desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId + desktopData + .desksSequence(displayId) + .mapNotNull { it.topTransparentFullscreenTaskId } + .firstOrNull() /** * Clears the top transparent fullscreen task id info for a given display's active desk. @@ -818,7 +821,6 @@ class DesktopRepository( } /** Minimizes the task in its desk. */ - @VisibleForTesting fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) @@ -933,6 +935,12 @@ class DesktopRepository( listener.onDeskRemoved(displayId = desk.displayId, deskId = desk.deskId) } } + if ( + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue && + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + ) { + removeDeskFromPersistentRepository(desk) + } return activeTasks } @@ -1031,6 +1039,24 @@ class DesktopRepository( } } + private fun removeDeskFromPersistentRepository(desk: Desk) { + mainCoroutineScope.launch { + try { + logD( + "updatePersistentRepositoryForRemovedDesk user=%d desk=%d", + userId, + desk.deskId, + ) + persistentRepository.removeDesktop(userId = userId, desktopId = desk.deskId) + } catch (throwable: Throwable) { + logE( + "An exception occurred while updating the persistent repository \n%s", + throwable.stackTrace, + ) + } + } + } + internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopRepository") @@ -1049,6 +1075,7 @@ class DesktopRepository( } .forEach { (displayId, activeDeskId, desks) -> pw.println("${prefix}Display #$displayId:") + pw.println("${innerPrefix}numOfDesks=${desks.size}") pw.println("${innerPrefix}activeDesk=$activeDeskId") pw.println("${innerPrefix}desks:") val desksPrefix = "$innerPrefix " 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 dbc599be57af..d0356d55035d 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 @@ -115,6 +115,9 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.desktopmode.multidesks.createDesk +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -194,6 +197,7 @@ class DesktopTasksController( private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val desktopImmersiveController: DesktopImmersiveController, private val userRepositories: DesktopUserRepositories, + desktopRepositoryInitializer: DesktopRepositoryInitializer, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, @@ -276,6 +280,19 @@ class DesktopTasksController( } userId = ActivityManager.getCurrentUser() taskRepository = userRepositories.getProfile(userId) + + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desktopRepositoryInitializer.deskRecreationFactory = + DeskRecreationFactory { deskUserId, destinationDisplayId, deskId -> + if (deskUserId != userId) { + // TODO: b/400984250 - add multi-user support for multi-desk restoration. + logW("Tried to recreated desk of another user.") + deskId + } else { + desksOrganizer.createDesk(destinationDisplayId) + } + } + } } private fun onInit() { @@ -1718,15 +1735,7 @@ class DesktopTasksController( wct.reorder(runningTaskInfo.token, /* onTop= */ true) } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { // Task is not running, start it - wct.startTask( - taskId, - ActivityOptions.makeBasic() - .apply { - launchWindowingMode = WINDOWING_MODE_FREEFORM - splashScreenStyle = SPLASH_SCREEN_STYLE_ICON - } - .toBundle(), - ) + wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) } } @@ -2826,9 +2835,6 @@ class DesktopTasksController( } prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? - } taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId) ) @@ -2846,6 +2852,19 @@ class DesktopTasksController( desksOrganizer.minimizeTask(wct, deskId, taskToMinimize) } } + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + expandedTasksOrderedFrontToBack + .filter { taskId -> taskId != taskIdToMinimize } + .reversed() + .forEach { taskId -> + val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (runningTaskInfo == null) { + wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) + } else { + desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo) + } + } + } return { transition -> val activateDeskTransition = if (newTaskIdInFront != null) { @@ -3497,6 +3516,13 @@ class DesktopTasksController( } } + private fun createActivityOptionsForStartTask(): ActivityOptions { + return ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + splashScreenStyle = SPLASH_SCREEN_STYLE_ICON + } + } + private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopTasksController") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 8c4bc2598dff..5a988fcd1b77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.desktopmode.multidesks import android.app.ActivityManager import android.window.TransitionInfo import android.window.WindowContainerTransaction +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback +import kotlin.coroutines.suspendCoroutine /** An organizer of desk containers in which to host child desktop windows. */ interface DesksOrganizer { @@ -82,3 +84,9 @@ interface DesksOrganizer { fun onCreated(deskId: Int) } } + +/** Creates a new desk container in the given display. */ +suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont -> + val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) } + createDesk(displayId, onCreateCallback) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 1566544f5303..f71eacab518d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -132,10 +132,7 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis .toBuilder() .putDesktopRepoByUser( userId, - currentRepository - .toBuilder() - .putDesktop(desktopId, desktop.build()) - .build(), + currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(), ) .build() } @@ -149,6 +146,33 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis } } + /** Removes the desktop from the persistent repository. */ + suspend fun removeDesktop(userId: Int, desktopId: Int) { + try { + dataStore.updateData { persistentRepositories: DesktopPersistentRepositories -> + val currentRepository = + persistentRepositories.getDesktopRepoByUserOrDefault( + userId, + DesktopRepositoryState.getDefaultInstance(), + ) + persistentRepositories + .toBuilder() + .putDesktopRepoByUser( + userId, + currentRepository.toBuilder().removeDesktop(desktopId).build(), + ) + .build() + } + } catch (throwable: Throwable) { + Log.e( + TAG, + "Error in removing desktop related data, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + throwable, + ) + } + } + suspend fun removeUsers(uids: List<Int>) { try { dataStore.updateData { persistentRepositories: DesktopPersistentRepositories -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt index a26ebbf4c99a..8191181cac11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt @@ -17,8 +17,22 @@ package com.android.wm.shell.desktopmode.persistence import com.android.wm.shell.desktopmode.DesktopUserRepositories +import kotlinx.coroutines.flow.StateFlow /** Interface for initializing the [DesktopUserRepositories]. */ -fun interface DesktopRepositoryInitializer { +interface DesktopRepositoryInitializer { + /** A factory used to recreate a desk from persistence. */ + var deskRecreationFactory: DeskRecreationFactory + + /** A flow that emits true when the repository has been initialized. */ + val isInitialized: StateFlow<Boolean> + + /** Initialize the user repositories from a persistent data store. */ fun initialize(userRepositories: DesktopUserRepositories) + + /** A factory for recreating desks. */ + fun interface DeskRecreationFactory { + /** Recreates a restored desk and returns the new desk id. */ + suspend fun recreateDesk(userId: Int, destinationDisplayId: Int, deskId: Int): Int + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 0507e59c06e1..49cb7391fe97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -17,13 +17,19 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context +import android.view.Display import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** @@ -37,62 +43,136 @@ class DesktopRepositoryInitializerImpl( private val persistentRepository: DesktopPersistentRepository, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ) : DesktopRepositoryInitializer { + + override var deskRecreationFactory: DeskRecreationFactory = DefaultDeskRecreationFactory() + + private val _isInitialized = MutableStateFlow(false) + override val isInitialized: StateFlow<Boolean> = _isInitialized + override fun initialize(userRepositories: DesktopUserRepositories) { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + _isInitialized.value = true + return + } // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized mainCoroutineScope.launch { - val desktopUserPersistentRepositoryMap = - persistentRepository.getUserDesktopRepositoryMap() ?: return@launch - for (userId in desktopUserPersistentRepositoryMap.keys) { - val repository = userRepositories.getProfile(userId) - val desktopRepositoryState = - persistentRepository.getDesktopRepositoryState(userId) ?: continue - val desktopByDesktopIdMap = desktopRepositoryState.desktopMap - for (desktopId in desktopByDesktopIdMap.keys) { - val persistentDesktop = - persistentRepository.readDesktop(userId, desktopId) ?: continue - val maxTasks = - DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } - ?: persistentDesktop.zOrderedTasksCount - var visibleTasksCount = 0 - repository.addDesk( - displayId = persistentDesktop.displayId, - deskId = - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - persistentDesktop.desktopId - } else { - // When disabled, desk ids are always the display id. - persistentDesktop.displayId - }, + try { + val desktopUserPersistentRepositoryMap = + persistentRepository.getUserDesktopRepositoryMap() ?: return@launch + for (userId in desktopUserPersistentRepositoryMap.keys) { + val repository = userRepositories.getProfile(userId) + val desktopRepositoryState = + persistentRepository.getDesktopRepositoryState(userId) ?: continue + val desksToRestore = getDesksToRestore(desktopRepositoryState, userId) + logV( + "initialize() will restore desks=%s user=%d", + desksToRestore.map { it.desktopId }, + userId, ) - persistentDesktop.zOrderedTasksList - // Reverse it so we initialize the repo from bottom to top. - .reversed() - .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } - // TODO: b/362720497 - add tasks to their respective desk when multi-desk - // persistence is implemented. - .forEach { task -> - if ( - task.desktopTaskState == DesktopTaskState.VISIBLE && - visibleTasksCount < maxTasks - ) { - visibleTasksCount++ - repository.addTask( - persistentDesktop.displayId, - task.taskId, - isVisible = false, - ) - } else { - repository.addTask( - persistentDesktop.displayId, - task.taskId, + desksToRestore.forEach { persistentDesktop -> + val maxTasks = getTaskLimit(persistentDesktop) + val displayId = persistentDesktop.displayId + val deskId = persistentDesktop.desktopId + // TODO: b/401107440 - Implement desk restoration to other displays. + val newDisplayId = Display.DEFAULT_DISPLAY + val newDeskId = + deskRecreationFactory.recreateDesk( + userId = userId, + destinationDisplayId = newDisplayId, + deskId = deskId, + ) + logV( + "Recreated desk=%d in display=%d using new deskId=%d and displayId=%d", + deskId, + displayId, + newDeskId, + newDisplayId, + ) + if (newDeskId != deskId || newDisplayId != displayId) { + logV("Removing obsolete desk from persistence under deskId=%d", deskId) + persistentRepository.removeDesktop(userId, deskId) + } + + // TODO: b/393961770 - [DesktopRepository] doesn't save desks to the + // persistent repository until a task is added to them. Update it so that + // empty desks can be restored too. + repository.addDesk(displayId = displayId, deskId = newDeskId) + var visibleTasksCount = 0 + persistentDesktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } + .forEach { task -> + // Visible here means non-minimized a.k.a. expanded, it does not + // mean + // it is visible in WM (and |DesktopRepository|) terms. + val isVisible = + task.desktopTaskState == DesktopTaskState.VISIBLE && + visibleTasksCount < maxTasks + + repository.addTaskToDesk( + displayId = displayId, + deskId = newDeskId, + taskId = task.taskId, isVisible = false, ) - repository.minimizeTask(persistentDesktop.displayId, task.taskId) + + if (isVisible) { + visibleTasksCount++ + } else { + repository.minimizeTaskInDesk( + displayId = displayId, + deskId = newDeskId, + taskId = task.taskId, + ) + } } - } + } } + } finally { + _isInitialized.value = true } } } + + private suspend fun getDesksToRestore( + state: DesktopRepositoryState, + userId: Int, + ): Set<Desktop> { + // TODO: b/365873835 - what about desks that won't be restored? + // - invalid desk ids from multi-desk -> single-desk switching can be ignored / deleted. + val limitToSingleDeskPerDisplay = + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + return state.desktopMap.keys + .mapNotNull { deskId -> + persistentRepository.readDesktop(userId, deskId)?.takeIf { desk -> + // Do not restore invalid desks when multi-desks is disabled. This is + // possible if the feature is disabled after having created multiple desks. + val isValidSingleDesk = desk.desktopId == desk.displayId + (!limitToSingleDeskPerDisplay || isValidSingleDesk) + } + } + .toSet() + } + + private fun getTaskLimit(persistedDesk: Desktop): Int = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: persistedDesk.zOrderedTasksCount + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + /** A default implementation of [DeskRecreationFactory] that reuses the desk id. */ + private class DefaultDeskRecreationFactory : DeskRecreationFactory { + override suspend fun recreateDesk( + userId: Int, + destinationDisplayId: Int, + deskId: Int, + ): Int = deskId + } + + companion object { + private const val TAG = "DesktopRepositoryInitializerImpl" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 383afcf6f821..f81f330e50c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -20,6 +20,7 @@ import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; +import android.os.Bundle; import android.os.SystemProperties; import android.view.SurfaceControl; import android.window.WindowContainerToken; @@ -47,7 +48,7 @@ import java.util.function.Supplier; /** * Scheduler for Shell initiated PiP transitions and animations. */ -public class PipScheduler { +public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = PipScheduler.class.getSimpleName(); /** @@ -71,6 +72,7 @@ public class PipScheduler { private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; @Nullable private Runnable mUpdateMovementBoundsRunnable; + @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator; private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier; private Supplier<PictureInPictureParams> mPipParamsSupplier; @@ -85,6 +87,7 @@ public class PipScheduler { mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDesktopState = pipDesktopState; mSplitScreenControllerOptional = splitScreenControllerOptional; @@ -238,12 +241,16 @@ public class PipScheduler { void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash, boolean withStartDelay, @NonNull Runnable onAnimationEnd) { - PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash, + mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash, null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT); - animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS); - animator.setStartDelay(withStartDelay ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); - animator.setAnimationEndCallback(onAnimationEnd); - animator.start(); + mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS); + mOverlayFadeoutAnimator.setStartDelay(withStartDelay + ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); + mOverlayFadeoutAnimator.setAnimationEndCallback(() -> { + onAnimationEnd.run(); + mOverlayFadeoutAnimator = null; + }); + mOverlayFadeoutAnimator.start(); } void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) { @@ -289,6 +296,21 @@ public class PipScheduler { mSurfaceControlTransactionFactory = factory; } + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, + @android.annotation.Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.EXITING_PIP: + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) { + mOverlayFadeoutAnimator.end(); + mOverlayFadeoutAnimator = null; + } + break; + } + } + @VisibleForTesting interface PipAlphaAnimatorSupplier { PipAlphaAnimator get(@NonNull Context context, @@ -303,6 +325,17 @@ public class PipScheduler { mPipAlphaAnimatorSupplier = supplier; } + @VisibleForTesting + void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) { + mOverlayFadeoutAnimator = animator; + } + + @VisibleForTesting + @Nullable + PipAlphaAnimator getOverlayFadeoutAnimator() { + return mOverlayFadeoutAnimator; + } + void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) { mPipParamsSupplier = pipParamsSupplier; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index c370c0cb0930..75c09829e551 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -93,7 +93,6 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.IActivityTaskManager; import android.app.PendingIntent; -import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.content.ActivityNotFoundException; import android.content.Context; @@ -2249,10 +2248,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setRootForceTranslucent(true, wct); if (!enableFlexibleSplit()) { - //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split + // TODO: consider support 3 splits // Make the stages adjacent to each other so they occlude what's behind them. - wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + wct.setAdjacentRootSet(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); mSplitLayout.getInvisibleBounds(mTempRect1); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); } @@ -2263,7 +2262,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }); mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token); } else { - // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split + // TODO: consider support 3 splits } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 2ebe67b22c18..3f0d931af7c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -29,6 +29,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.isDesktopModeSupportedOnDisplay; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; @@ -1095,8 +1096,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Resources.ID_NULL; } - private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) { - final PointF position = new PointF(); + private Point calculateMaximizeMenuPosition(int menuWidth, int menuHeight) { + final Point position = new Point(); final Resources resources = mContext.getResources(); final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); @@ -1111,11 +1112,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int[] maximizeButtonLocation = new int[2]; maximizeWindowButton.getLocationInWindow(maximizeButtonLocation); - float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth - - maximizeWindowButton.getWidth()) / 2)); - float menuTop = (mPositionInParent.y + captionHeight); - final float menuRight = menuLeft + menuWidth; - final float menuBottom = menuTop + menuHeight; + int menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - (menuWidth + - maximizeWindowButton.getWidth()) / 2); + int menuTop = (mPositionInParent.y + captionHeight); + final int menuRight = menuLeft + menuWidth; + final int menuBottom = menuTop + menuHeight; // If the menu is out of screen bounds, shift it as needed if (menuLeft < 0) { @@ -1127,7 +1128,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin menuTop = (displayHeight - menuHeight); } - return new PointF(menuLeft, menuTop); + return new Point(menuLeft, menuTop); } boolean isHandleMenuActive() { @@ -1410,7 +1411,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin supportsMultiInstance, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - canEnterDesktopMode(mContext), + isDesktopModeSupportedOnDisplay(mContext, mDisplay), isBrowserApp, isBrowserApp ? getAppLink() : getBrowserLink(), mResult.mCaptionWidth, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index ad3525af3f94..5d1a7a0cc3a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -26,7 +26,7 @@ import android.content.res.ColorStateList import android.content.res.Resources import android.graphics.Paint import android.graphics.PixelFormat -import android.graphics.PointF +import android.graphics.Point import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable @@ -90,7 +90,7 @@ class MaximizeMenu( private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, private val decorWindowContext: Context, - private val positionSupplier: (Int, Int) -> PointF, + private val positionSupplier: (Int, Int) -> Point, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { private var maximizeMenu: AdditionalViewHostViewContainer? = null @@ -100,14 +100,14 @@ class MaximizeMenu( private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() - private lateinit var menuPosition: PointF + private lateinit var menuPosition: Point private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) /** Position the menu relative to the caption's position. */ fun positionMenu(t: Transaction) { menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0, maximizeMenuView?.measureHeight() ?: 0) - t.setPosition(leash, menuPosition.x, menuPosition.y) + t.setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat()) } /** Creates and shows the maximize window. */ @@ -208,8 +208,8 @@ class MaximizeMenu( val menuHeight = menuView.measureHeight() menuPosition = positionSupplier(menuWidth, menuHeight) val lp = WindowManager.LayoutParams( - menuWidth.toInt(), - menuHeight.toInt(), + menuWidth, + menuHeight, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, @@ -222,7 +222,7 @@ class MaximizeMenu( // Bring menu to front when open t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU) - .setPosition(leash, menuPosition.x, menuPosition.y) + .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat()) .setCornerRadius(leash, cornerRadius) .show(leash) maximizeMenu = @@ -1047,7 +1047,7 @@ interface MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - positionSupplier: (Int, Int) -> PointF, + positionSupplier: (Int, Int) -> Point, transactionSupplier: Supplier<Transaction> ): MaximizeMenu } @@ -1060,7 +1060,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - positionSupplier: (Int, Int) -> PointF, + positionSupplier: (Int, Int) -> Point, transactionSupplier: Supplier<Transaction> ): MaximizeMenu { return MaximizeMenu( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index c3d15df6eae5..9c55f0ecda93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor.tiling import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.Context import android.content.res.Configuration import android.content.res.Resources @@ -28,9 +29,11 @@ import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.internal.annotations.VisibleForTesting @@ -422,6 +425,8 @@ class DesktopTilingWindowDecoration( change.taskInfo?.let { if (it.isFullscreen || isMinimized(change.mode, info.type)) { removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen) + } else if (isEnteringPip(change, info.type)) { + removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen) } } } @@ -434,6 +439,27 @@ class DesktopTilingWindowDecoration( infoType == TRANSIT_OPEN)) } + private fun isEnteringPip(change: Change, transitType: Int): Boolean { + if (change.taskInfo != null && change.taskInfo?.windowingMode == WINDOWING_MODE_PINNED) { + // - TRANSIT_PIP: type (from RootWindowContainer) + // - TRANSIT_OPEN (from apps that enter PiP instantly on opening, mostly from + // CTS/Flicker tests). + // - TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also + // be allowed to animate if the task in question is pinned already - see b/308054074. + // - TRANSIT_CHANGE: This can happen if the request to enter PIP happens when we are + // collecting for another transition, such as TRANSIT_CHANGE (display rotation). + if ( + transitType == TRANSIT_PIP || + transitType == TRANSIT_OPEN || + transitType == TRANSIT_TO_FRONT || + transitType == TRANSIT_CHANGE + ) { + return true + } + } + return false + } + class AppResizingHelper( val taskInfo: RunningTaskInfo, val desktopModeWindowDecoration: DesktopModeWindowDecoration, @@ -550,12 +576,16 @@ class DesktopTilingWindowDecoration( taskVanished: Boolean = false, shouldDelayUpdate: Boolean = false, ) { + val taskRepository = desktopUserRepositories.current if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate) leftTaskResizingHelper = null - rightTaskResizingHelper - ?.desktopModeWindowDecoration - ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + val taskId = rightTaskResizingHelper?.taskInfo?.taskId + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + rightTaskResizingHelper + ?.desktopModeWindowDecoration + ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + } tearDownTiling() return } @@ -563,9 +593,12 @@ class DesktopTilingWindowDecoration( if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate) rightTaskResizingHelper = null - leftTaskResizingHelper - ?.desktopModeWindowDecoration - ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + val taskId = leftTaskResizingHelper?.taskInfo?.taskId + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + leftTaskResizingHelper + ?.desktopModeWindowDecoration + ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + } tearDownTiling() } } @@ -600,7 +633,6 @@ class DesktopTilingWindowDecoration( fun onOverviewAnimationStateChange(isRunning: Boolean) { if (!isTilingManagerInitialised) return - if (isRunning) { desktopTilingDividerWindowManager?.hideDividerBar() } else if (allTiledTasksVisible()) { diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml index 1de47df78853..e51447f5cfcb 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 34d001c858f6..7659ec903480 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml index 34d001c858f6..7659ec903480 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index 9c1a8f17aeee..a4ecac9dfeb0 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index ae73dae99d6f..75ffdc69c73b 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index a136936c0838..8003cbaada50 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 8ad54f5a0bb4..275d7b73a112 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -28,14 +28,22 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @@ -46,15 +54,18 @@ import org.mockito.quality.Strictness * * Usage: atest WMShellUnitTests:DesktopDisplayEventHandlerTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var displayController: DisplayController @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories + @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController + private val testScope = TestScope() private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit @@ -77,7 +88,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { DesktopDisplayEventHandler( context, shellInit, + testScope.backgroundScope, displayController, + mockDesktopRepositoryInitializer, mockDesktopUserRepositories, mockDesktopTasksController, desktopDisplayModeController, @@ -89,17 +102,66 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @After fun tearDown() { + testScope.cancel() mockitoSession.finishMocking() } @Test - fun testDisplayAdded_supportsDesks_createsDesk() { - whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) - onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + runCurrent() - verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) - } + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + runCurrent() + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + stateFlow.emit(true) + runCurrent() + + verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + runCurrent() + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } @Test fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() { 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 cc37c440f650..450989dd334d 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 @@ -21,9 +21,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver +import android.hardware.input.InputManager import android.os.Binder +import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -44,6 +47,7 @@ import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -64,13 +68,18 @@ import org.mockito.kotlin.whenever */ @SmallTest @RunWith(TestParameterInjector::class) -class DesktopDisplayModeControllerTest : ShellTestCase() { +class DesktopDisplayModeControllerTest( + @TestParameter(valuesProvider = FlagsParameterizationProvider::class) + flags: FlagsParameterization +) : ShellTestCase() { private val transitions = mock<Transitions>() private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val mockWindowManager = mock<IWindowManager>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val inputManager = mock<InputManager>() + private val mainHandler = mock<Handler>() private lateinit var controller: DesktopDisplayModeController @@ -82,6 +91,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) private val wallpaperToken = MockToken().token() + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) @@ -95,27 +108,20 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { mockWindowManager, shellTaskOrganizer, desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler, ) runningTasks.add(freeformTask) runningTasks.add(fullscreenTask) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks)) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + setTabletModeStatus(SwitchState.UNKNOWN) } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectToSwitch: Boolean, - ) { - defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) - val settingsSession = - ExtendedDisplaySettingsSession( - context.contentResolver, - if (extendedDisplayEnabled) 1 else 0, - ) - - settingsSession.use { + private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN) + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { connectExternalDisplay() if (expectToSwitch) { // Assumes [connectExternalDisplay] properly triggered the switching transition. @@ -133,7 +139,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) } else { @@ -144,25 +150,64 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled( - @TestParameter param: ModeSwitchTestCase - ) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - // When the flag is disabled, never switch. - expectToSwitch = false, - ) + fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() { + // When the flag is disabled, never switch. + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false) } @Test @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - param.expectToSwitchByDefault, - ) + fun displayWindowingModeSwitchOnDisplayConnected() { + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) + @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH) + fun testTargetWindowingMode_formfactorDisabled( + @TestParameter param: ExternalDisplayBasedTargetModeTestCase, + @TestParameter tabletModeStatus: SwitchState, + ) { + whenever(mockWindowManager.getWindowingMode(anyInt())) + .thenReturn(param.defaultWindowingMode) + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + ) + fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) { + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(param.tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } } @Test @@ -217,6 +262,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { controller.refreshDisplayWindowingMode() } + private fun setTabletModeStatus(status: SwitchState) { + whenever(inputManager.isInTabletMode()).thenReturn(status.value) + } + private class ExtendedDisplaySettingsSession( private val contentResolver: ContentResolver, private val overrideValue: Int, @@ -233,33 +282,158 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { } } + private class FlagsParameterizationProvider : TestParameterValuesProvider() { + override fun provideValues( + context: TestParameterValuesProvider.Context + ): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH + ) + } + } + companion object { const val EXTERNAL_DISPLAY_ID = 100 - enum class ModeSwitchTestCase( + enum class SwitchState(val value: Int) { + UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN), + ON(InputManager.SWITCH_STATE_ON), + OFF(InputManager.SWITCH_STATE_OFF), + } + + enum class ExternalDisplayBasedTargetModeTestCase( val defaultWindowingMode: Int, + val hasExternalDisplay: Boolean, val extendedDisplayEnabled: Boolean, - val expectToSwitchByDefault: Boolean, + val expectedWindowingMode: Int, ) { - FULLSCREEN_DISPLAY( + FREEFORM_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_EXTERNAL_EXTENDED( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FREEFORM_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, extendedDisplayEnabled = true, - expectToSwitchByDefault = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + FREEFORM_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FULLSCREEN_DISPLAY_MIRRORING( + FULLSCREEN_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - FREEFORM_DISPLAY( + FREEFORM_NO_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + } + + enum class FormFactorBasedTargetModeTestCase( + val hasExternalDisplay: Boolean, + val extendedDisplayEnabled: Boolean, + val tabletModeStatus: SwitchState, + val expectedWindowingMode: Int, + ) { + EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = true, extendedDisplayEnabled = true, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FREEFORM_DISPLAY_MIRRORING( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, + NO_EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = false, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index 006c3cae121c..4c18ee1500b7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -114,6 +114,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transactionSupplier = transactionSupplier, ) desktopRepository = userRepositories.current + desktopRepository.addDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID) + desktopRepository.setActiveDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID) } @Test @@ -835,5 +837,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() { companion object { private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900) private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000) + private const val DEFAULT_DESK_ID = 0 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index fe1dc29181b9..b859a00c6df4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -24,7 +24,6 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper -import android.view.Display import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils @@ -67,7 +66,6 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private lateinit var taskInfo: RunningTaskInfo @Mock private lateinit var syncQueue: SyncTransactionQueue @Mock private lateinit var displayController: DisplayController - @Mock private lateinit var display: Display @Mock private lateinit var taskSurface: SurfaceControl @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var displayLayout: DisplayLayout @@ -83,12 +81,20 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) - whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any())) .thenReturn(Rect()) taskInfo = DesktopTestHelpers.createFullscreenTask() + + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_isDesktopModeSupported, + true, + ) + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_canInternalDisplayHostDesktops, + true, + ) } @Test @@ -260,14 +266,9 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { ) fun testDefaultIndicatorWithNoDesktop() { mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_isDesktopModeSupported, + com.android.internal.R.bool.config_canInternalDisplayHostDesktops, false, ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_isDesktopModeDevOptionSupported, - false, - ) - // Fullscreen to center, no desktop indicator createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index de92d391645a..f84a1a38bdfc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -1203,6 +1203,17 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeDesk_removesFromPersistence() = + runTest(StandardTestDispatcher()) { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + + repo.removeDesk(deskId = 2) + + verify(persistentRepository).removeDesktop(DEFAULT_USER_ID, 2) + } + + @Test fun getTaskInFullImmersiveState_byDisplay() { repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) 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 75308442d76a..34c5ebd6d94d 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 @@ -22,6 +22,7 @@ import android.app.ActivityOptions import android.app.KeyguardManager import android.app.PendingIntent import android.app.PictureInPictureParams +import android.app.WindowConfiguration import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -297,6 +298,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private val wallpaperToken = MockToken().token() private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) @@ -433,6 +438,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() dragToDesktopTransitionHandler, mMockDesktopImmersiveController, userRepositories, + repositoryInitializer, recentsTransitionHandler, multiInstanceHelper, shellExecutor, @@ -627,11 +633,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_noTasks_returnsFalse() { assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_noTasksVisible_returnsFalse() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -642,6 +650,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isDesktopModeShowing_noActiveDesk_returnsFalse() { + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -656,6 +673,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, ) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_topTransparentFullscreenTask_returnsTrue() { val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) @@ -665,6 +683,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun isDesktopModeShowing_deskInactive_topTransparentFullscreenTask_returnsTrue() { + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test + @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) @@ -1053,11 +1085,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_noTasks_returnsFalse() { assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isAnyDeskActive_noActiveDesk_returnsFalse() { + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isAnyDeskActive_withActiveDesk_returnsTrue() { + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + + assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_twoTasks_bothVisible_returnsTrue() { setUpHomeTask() @@ -1068,6 +1118,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isInDesktop_twoTasks_oneVisible_returnsTrue() { setUpHomeTask() @@ -1078,6 +1129,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_twoTasksVisibleOnDifferentDisplays_returnsTrue() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -1097,7 +1149,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1108,7 +1160,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1119,7 +1171,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1130,7 +1182,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1862,6 +1914,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wallpaperToken = MockToken().token() whenever(desktopWallpaperActivityTokenProvider.getToken(SECOND_DISPLAY)) .thenReturn(wallpaperToken) + taskRepository.addDesk(SECOND_DISPLAY, deskId = 2) val task = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2, background = true) controller.moveTaskToDefaultDeskAndActivate( @@ -2666,7 +2719,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksDisabled() { // Set up two display ids taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) @@ -2692,7 +2746,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksEnabled() { + // Set up two display ids + val targetDeskId = 2 + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksDisabled() { // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -2718,6 +2792,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksEnabled() { + // Set up two display ids + val targetDeskId = 0 + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task)) + } + + @Test @EnableFlags( FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, @@ -2781,6 +2874,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_sizeInDpPreserved() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2822,6 +2916,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2863,6 +2958,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) // Create a mock for the target display area: second display val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) @@ -2971,7 +3067,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_toDesktopInOtherDisplay_bringsExistingTasksToFront() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksDisabled_bringsExistingTasksToFront() { val transition = Binder() val sourceDeskId = 0 val targetDeskId = 2 @@ -2997,6 +3094,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksEnabled_bringsExistingTasksToFront() { + val transition = Binder() + val sourceDeskId = 0 + val targetDeskId = 2 + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) + taskRepository.setDeskInactive(deskId = targetDeskId) + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull())) + .thenReturn(transition) + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = sourceDeskId) + val task2 = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = targetDeskId) + + controller.moveToNextDisplay(task1.taskId) + + // Existing desktop task in the target display is moved to front. + val wct = getLatestTransition() + assertNotNull(wct) + verify(desksOrganizer).reorderTaskToFront(wct, targetDeskId, task2) + } + + @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, @@ -3110,7 +3235,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun moveToNextDisplay_movingToDesktop_sendsTaskbarRoundingUpdate() { val transition = Binder() - val sourceDeskId = 1 + val sourceDeskId = 0 val targetDeskId = 2 taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) taskRepository.setDeskInactive(deskId = targetDeskId) @@ -3627,7 +3752,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -3643,7 +3769,22 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_deskActive_multiDesksEnabled_movesToDesk() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = deskId) + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -3668,6 +3809,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_activeDesk_multiDesksEnabled_movesToDesk() { + val deskId = 0 + setUpHomeTask() + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksDisabled_dontMinimize() { val freeformTask = setUpFreeformTask() @@ -3899,6 +4055,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -3921,7 +4078,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_freeformDisplay_movesToDesk() { + val deskId = 0 + taskRepository.setDeskInactive(deskId) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() { + taskRepository.setDeskInactive(deskId = 0) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -3933,6 +4111,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { val freeformTask = setUpFreeformTask() markTaskHidden(freeformTask) @@ -3941,12 +4120,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { val fullscreenTask = createFullscreenTask() assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_notInDesk_returnNull() { + taskRepository.setDeskInactive(deskId = 0) + val fullscreenTask = createFullscreenTask() + + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) createFreeformTask(displayId = SECOND_DISPLAY) @@ -3957,6 +4147,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_deskInOtherDisplayActive_returnNull() { + taskRepository.setDeskInactive(deskId = 0) + val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2) + + val result = + controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) + + assertThat(result).isNull() + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksDisabled_minimize() { val deskId = 0 @@ -4008,7 +4212,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { - val freeformTask = setUpFreeformTask() + taskRepository.setDeskInactive(deskId = 0) + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) markTaskHidden(freeformTask) val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) @@ -4038,9 +4243,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - val freeformTask = setUpFreeformTask() + taskRepository.setDeskInactive(deskId = 0) + val freeformTask = setUpFreeformTask(DEFAULT_DISPLAY, deskId = 0) markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) assertNotNull(wct, "should handle request") @@ -4049,7 +4255,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -4068,7 +4277,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_multiDesksDisabled_reorderedToTop() { whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -4091,34 +4301,47 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_freeformTask_desktopWallpaperEnabled_notInDesk_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + val deskId = 0 + taskRepository.setDeskInactive(deskId) + val freeformTask1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTask2 = createFreeformTask() - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(1) - result.assertReorderAt(0, task, toTop = true) + val wct = + controller.handleRequest( + Binder(), + createTransition(freeformTask2, type = TRANSIT_TO_FRONT), + ) + + assertNotNull(wct, "Should handle request") + verify(desksOrganizer).reorderTaskToFront(wct, deskId, freeformTask1) + wct.assertReorder(freeformTask2, toTop = true) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { - whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(2) - // Add desktop wallpaper activity - result.assertPendingIntentAt(0, desktopWallpaperIntent) - // Bring new task to front - result.assertReorderAt(1, task, toTop = true) + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task @@ -4134,10 +4357,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + taskRepository.setDeskInactive(deskId = 0) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task - createFreeformTask(displayId = SECOND_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2) + setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2) val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) @@ -4273,7 +4499,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -4290,6 +4517,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksEnabled_returnSwitchToFreeformWCT() { + val deskId = 0 + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + markTaskVisible(freeformTask) + + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + val wct = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() { @@ -4348,7 +4598,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, ) - fun handleRequest_onlyTopTransparentFullscreenTask_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksDisabled_returnSwitchToFreeformWCT() { val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) @@ -4360,8 +4611,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksEnabled_movesToDesktop() { + val deskId = 0 + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + taskRepository.setDeskInactive(deskId = deskId) + + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + val wct = controller.handleRequest(Binder(), createTransition(task)) + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_returnNull() { + taskRepository.setDeskInactive(deskId = 0) val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() @@ -4415,7 +4686,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_systemUIActivityWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -4434,6 +4706,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .isEqualTo(WINDOWING_MODE_FREEFORM) } + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_systemUIActivityWithoutDisplay_multiDesksEnabled_movesTaskToDesk() { + val deskId = 0 + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + markTaskVisible(freeformTask) + + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "") + val task = + setUpFullscreenTask(displayId = DEFAULT_DISPLAY).apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + val wct = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() { val freeformTask = setUpFreeformTask() @@ -4476,6 +4774,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() { + taskRepository.setDeskInactive(deskId = 0) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -5109,6 +5408,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun activateDesk_hasNonRunningTask_startsTask() { + val deskId = 0 + val nonRunningTask = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0, background = true) + + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(TRANSIT_TO_FRONT, OneShotRemoteHandler::class.java) + assertNotNull(wct) + wct.assertLaunchTask(nonRunningTask.taskId, WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun activateDesk_hasRunningTask_reordersTask() { + val deskId = 0 + val runningTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + verify(desksOrganizer).reorderTaskToFront(any(), eq(deskId), eq(runningTask)) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveTaskToDesk_multipleDesks_addsPendingTransition() { val transition = Binder() whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) @@ -6833,6 +7181,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { + taskRepository.setDeskInactive(deskId = 0) val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, @@ -6909,6 +7258,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setDeskInactive(deskId = 0) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -6944,6 +7294,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) + taskRepository.setDeskInactive(deskId = 0) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -6973,6 +7324,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun startLaunchTransition_desktopNotShowing_movesWallpaperToFront() { + taskRepository.setDeskInactive(deskId = 0) val launchingTask = createFreeformTask(displayId = DEFAULT_DISPLAY) val wct = WindowContainerTransaction() wct.reorder(launchingTask.token, /* onTop= */ true) @@ -7442,7 +7794,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = - wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds + wct.changes.entries + .find { (token, change) -> + token == task.token.asBinder() && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 + } + ?.value + ?.configuration + ?.windowConfiguration + ?.bounds private fun verifyWCTNotExecuted() { verify(transitions, never()).startTransition(anyInt(), any(), isNull()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index c40a04c47b9b..b511fc34fa89 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.ComponentName import android.graphics.Rect import android.view.Display.DEFAULT_DISPLAY @@ -45,6 +46,17 @@ object DesktopTestHelpers { .apply { bounds?.let { setBounds(it) } } .build() + fun createPinnedTask(displayId: Int = DEFAULT_DISPLAY, bounds: Rect? = null): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setDisplayId(displayId) + .setParentTaskId(displayId) + .setToken(MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_PINNED) + .setLastActiveTime(100) + .apply { bounds?.let { setBounds(it) } } + .build() + fun createFullscreenTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder = TestRunningTaskInfoBuilder() .setDisplayId(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 6b2f90fc0a59..9af504797182 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner import android.view.Display @@ -29,6 +30,7 @@ import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_R import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTaskOrganizer.TaskListener import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.LaunchAdjacentController @@ -39,14 +41,17 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.kotlin.argThat import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [RootTaskDesksOrganizer]. @@ -76,48 +81,17 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { ) } - @Test - fun testCreateDesk_callsBack() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - assertThat(callback.created).isTrue() - assertEquals(freeformRoot.taskId, callback.deskId) - } - - @Test - fun testCreateDesk_createsMinimizationRoot() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) - - val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId] - assertNotNull(minimizationRoot) - assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId) - assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) - } + @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() } @Test - fun testCreateMinimizationRoot_marksHidden() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + fun testCreateMinimizationRoot_marksHidden() = runTest { + val desk = createDesk() verify(mockShellTaskOrganizer) .applyTransaction( argThat { wct -> wct.changes.any { change -> - change.key == minimizationRootTask.token.asBinder() && + change.key == desk.minimizationRoot.token.asBinder() && (change.value.changeMask and Change.CHANGE_HIDDEN != 0) && change.value.hidden } @@ -126,7 +100,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskAppeared_withoutRequest_throws() { + fun testOnTaskAppeared_withoutRequest_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } assertThrows(Exception::class.java) { @@ -135,41 +109,25 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() { - organizer.createDesk(displayId = 2, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 } - - assertThrows(Exception::class.java) { - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - } - } - - @Test - fun testOnTaskAppeared_duplicateRoot_throws() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + fun testOnTaskAppeared_duplicateRoot_throws() = runTest { + val desk = createDesk() assertThrows(Exception::class.java) { - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl()) } } @Test - fun testOnTaskAppeared_duplicateMinimizedRoot_throws() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest { + val desk = createDesk() assertThrows(Exception::class.java) { - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl()) } } @Test - fun testOnTaskVanished_removesRoot() { + fun testOnTaskVanished_removesRoot() = runTest { val desk = createDesk() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -178,7 +136,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskVanished_removesMinimizedRoot() { + fun testOnTaskVanished_removesMinimizedRoot() = runTest { val desk = createDesk() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -188,7 +146,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowAppearsInDesk() { + fun testDesktopWindowAppearsInDesk() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -198,7 +156,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowAppearsInDeskMinimizationRoot() { + fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -208,7 +166,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowMovesToMinimizationRoot() { + fun testDesktopWindowMovesToMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -221,7 +179,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowDisappearsFromDesk() { + fun testDesktopWindowDisappearsFromDesk() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -232,7 +190,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowDisappearsFromDeskMinimizationRoot() { + fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -243,7 +201,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testRemoveDesk_removesDeskRoot() { + fun testRemoveDesk_removesDeskRoot() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -259,7 +217,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testRemoveDesk_removesMinimizationRoot() { + fun testRemoveDesk_removesMinimizationRoot() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -275,7 +233,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testActivateDesk() { + fun testActivateDesk() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -299,7 +257,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testActivateDesk_didNotExist_throws() { + fun testActivateDesk_didNotExist_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() @@ -307,7 +265,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testMoveTaskToDesk() { + fun testMoveTaskToDesk() = runTest { val desk = createDesk() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } @@ -333,7 +291,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testMoveTaskToDesk_didNotExist_throws() { + fun testMoveTaskToDesk_didNotExist_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } val desktopTask = createFreeformTask().apply { parentTaskId = -1 } @@ -344,7 +302,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testGetDeskAtEnd() { + fun testGetDeskAtEnd() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -357,7 +315,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testGetDeskAtEnd_inMinimizationRoot() { + fun testGetDeskAtEnd_inMinimizationRoot() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -370,27 +328,24 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testIsDeskActiveAtEnd() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - freeformRoot.isVisibleRequested = true - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + fun testIsDeskActiveAtEnd() = runTest { + val desk = createDesk() val isActive = organizer.isDeskActiveAtEnd( change = - TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply { - taskInfo = freeformRoot + TransitionInfo.Change(desk.deskRoot.token, SurfaceControl()).apply { + taskInfo = desk.deskRoot.taskInfo mode = TRANSIT_TO_FRONT }, - deskId = freeformRoot.taskId, + deskId = desk.deskRoot.deskId, ) assertThat(isActive).isTrue() } @Test - fun deactivateDesk_clearsLaunchRoot() { + fun deactivateDesk_clearsLaunchRoot() = runTest { val wct = WindowContainerTransaction() val desk = createDesk() organizer.activateDesk(wct, desk.deskRoot.deskId) @@ -409,7 +364,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_forDeskId() { + fun isDeskChange_forDeskId() = runTest { val desk = createDesk() assertThat( @@ -424,7 +379,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_forDeskId_inMinimizationRoot() { + fun isDeskChange_forDeskId_inMinimizationRoot() = runTest { val desk = createDesk() assertThat( @@ -442,7 +397,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_anyDesk() { + fun isDeskChange_anyDesk() = runTest { val desk = createDesk() assertThat( @@ -456,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_anyDesk_inMinimizationRoot() { + fun isDeskChange_anyDesk_inMinimizationRoot() = runTest { val desk = createDesk() assertThat( @@ -473,7 +428,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun minimizeTask() { + fun minimizeTask() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -486,7 +441,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun minimizeTask_alreadyMinimized_noOp() { + fun minimizeTask_alreadyMinimized_noOp() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val wct = WindowContainerTransaction() @@ -498,7 +453,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun minimizeTask_inDifferentDesk_noOp() { + fun minimizeTask_inDifferentDesk_noOp() = runTest { val desk = createDesk() val otherDesk = createDesk() val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } @@ -511,7 +466,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun unminimizeTask() { + fun unminimizeTask() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -528,7 +483,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun unminimizeTask_alreadyUnminimized_noOp() { + fun unminimizeTask_alreadyUnminimized_noOp() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -542,7 +497,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun unminimizeTask_notInDesk_noOp() { + fun unminimizeTask_notInDesk_noOp() = runTest { val desk = createDesk() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -553,7 +508,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun reorderTaskToFront() { + fun reorderTaskToFront() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -573,7 +528,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun reorderTaskToFront_notInDesk_noOp() { + fun reorderTaskToFront_notInDesk_noOp() = runTest { val desk = createDesk() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -592,7 +547,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun reorderTaskToFront_minimized_unminimizesAndReorders() { + fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -615,7 +570,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() { + fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true createDesk(visible = true) @@ -624,7 +579,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() { + fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false createDesk(visible = false) @@ -633,7 +588,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() { + fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true createDesk(visible = true) @@ -643,7 +598,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() { + fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true val desk = createDesk(visible = false) @@ -654,7 +609,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() { + fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false val desk = createDesk(visible = true) @@ -665,7 +620,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() { + fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true createDesk(visible = true) @@ -677,7 +632,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() { + fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false val desk = createDesk(visible = true) @@ -687,7 +642,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() { + fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true createDesk(visible = true) @@ -702,20 +657,39 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val minimizationRoot: DeskMinimizationRoot, ) - private fun createDesk(visible: Boolean = true): DeskRoots { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = + private suspend fun createDesk(visible: Boolean = true): DeskRoots { + val freeformRootTask = createFreeformTask().apply { parentTaskId = -1 isVisible = visible + isVisibleRequested = visible } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRoot, SurfaceControl()) - return DeskRoots( - organizer.deskRootsByDeskId[freeformRoot.taskId], - checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]), - ) + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + Mockito.reset(mockShellTaskOrganizer) + whenever( + mockShellTaskOrganizer.createRootTask( + Display.DEFAULT_DISPLAY, + WINDOWING_MODE_FREEFORM, + organizer, + true, + ) + ) + .thenAnswer { invocation -> + val listener = (invocation.arguments[2] as TaskListener) + listener.onTaskAppeared(freeformRootTask, SurfaceControl()) + } + .thenAnswer { invocation -> + val listener = (invocation.arguments[2] as TaskListener) + listener.onTaskAppeared(minimizationRootTask, SurfaceControl()) + } + val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY) + assertEquals(freeformRootTask.taskId, deskId) + val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId)) + val minimizationRoot = + assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId]) + assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId) + assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + return DeskRoots(deskRoot, minimizationRoot) } private fun WindowContainerTransaction.hasMinimizationHops( @@ -738,14 +712,4 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { hop.newParent == desk.deskRoot.token.asBinder() && hop.toTop } - - private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { - var deskId: Int? = null - val created: Boolean - get() = deskId != null - - override fun onCreated(deskId: Int) { - this.deskId = deskId - } - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index dd9e6ca0deae..4440d4e801fe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY @@ -82,10 +81,27 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) - /** TODO: b/362720497 - add multi-desk version when implemented. */ - @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() = + fun init_updatesFlow() = + runTest(StandardTestDispatcher()) { + whenever(persistentRepository.getUserDesktopRepositoryMap()) + .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) + whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1)) + .thenReturn(desktopRepositoryState1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2) + + repositoryInitializer.initialize(desktopUserRepositories) + + assertThat(repositoryInitializer.isInitialized.value).isTrue() + } + + @Test + @EnableFlags( + FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, + FLAG_ENABLE_DESKTOP_WINDOWING_HSUM, + FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun initWithPersistence_multipleUsers_addedCorrectly() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn( @@ -104,50 +120,74 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { repositoryInitializer.initialize(desktopUserRepositories) - // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be - // updated - // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(DESKTOP_ID_1) ) - .containsExactly(1, 3, 4, 5) + .containsExactly(1, 3) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getActiveTaskIdsInDesk(DESKTOP_ID_2) ) - .containsExactly(5, 1) + .containsExactly(4, 5) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_2) + .getActiveTaskIdsInDesk(DESKTOP_ID_3) ) - .containsExactly(3, 4) + .containsExactly(7, 8) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1) + ) + .containsExactly(1) .inOrder() - assertThat( - desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2) ) - .containsExactly(7, 8) + .containsExactly(5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_2) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_3) + ) + .containsExactly(7) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_1) + ) + .containsExactly(3) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_2) ) - .contains(7) + .containsExactly(4) + .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_2) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_3) ) .containsExactly(8) + .inOrder() } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - /** TODO: b/362720497 - add multi-desk version when implemented. */ - @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() = + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_singleUser_addedCorrectly() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) @@ -161,23 +201,44 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY) + .getActiveTaskIdsInDesk(DESKTOP_ID_1) + ) + .containsExactly(1, 3) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(DESKTOP_ID_2) + ) + .containsExactly(4, 5) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1) + ) + .containsExactly(1) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2) ) - .containsExactly(1, 3, 4, 5) + .containsExactly(5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_1) ) - .containsExactly(5, 1) + .containsExactly(3) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_2) ) - .containsExactly(3, 4) + .containsExactly(4) .inOrder() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index 275e4882a79d..42f65dd71f16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.phone; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -86,6 +87,7 @@ public class PipSchedulerTest { @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; @Mock private SplitScreenController mMockSplitScreenController; + @Mock private SurfaceControl mMockLeash; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; @@ -315,6 +317,30 @@ public class PipSchedulerTest { verify(mMockAlphaAnimator, never()).start(); } + @Test + public void onPipTransitionStateChanged_exiting_endAnimation() { + mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator); + when(mMockAlphaAnimator.isStarted()).thenReturn(true); + mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP, + PipTransitionState.EXITING_PIP, null); + + verify(mMockAlphaAnimator, times(1)).end(); + assertNull("mOverlayFadeoutAnimator should be reset to null", + mPipScheduler.getOverlayFadeoutAnimator()); + } + + @Test + public void onPipTransitionStateChanged_scheduledBoundsChange_endAnimation() { + mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator); + when(mMockAlphaAnimator.isStarted()).thenReturn(true); + mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP, + PipTransitionState.SCHEDULED_BOUNDS_CHANGE, null); + + verify(mMockAlphaAnimator, times(1)).end(); + assertNull("mOverlayFadeoutAnimator should be reset to null", + mPipScheduler.getOverlayFadeoutAnimator()); + } + private void setNullPipTaskToken() { when(mMockPipTransitionState.getPipTaskToken()).thenReturn(null); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt new file mode 100644 index 000000000000..048981d634ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt @@ -0,0 +1,115 @@ +/* + * 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.transition + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.WindowManager +import android.window.RemoteTransition +import android.window.TransitionFilter +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestSyncExecutor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Test class for [RemoteTransitionHandler]. + * + * atest WMShellUnitTests:RemoteTransitionHandlerTest + */ +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class RemoteTransitionHandlerTest : ShellTestCase() { + + private val testExecutor: TestSyncExecutor = TestSyncExecutor() + + private val testRemoteTransition = RemoteTransition(TestRemoteTransition()) + private lateinit var handler: RemoteTransitionHandler + + @Before + fun setUp() { + handler = RemoteTransitionHandler(testExecutor) + } + + @Test + fun handleRequest_noRemoteTransition_returnsNull() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null) + + assertNull(handler.handleRequest(mock(), request)) + } + + @Test + fun handleRequest_testRemoteTransition_returnsWindowContainerTransaction() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition) + + assertTrue(handler.handleRequest(mock(), request) is WindowContainerTransaction) + } + + @Test + fun startAnimation_noRemoteTransition_returnsFalse() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null) + handler.handleRequest(mock(), request) + + val isHandled = handler.startAnimation( + /* transition= */ mock(), + /* info= */ createTransitionInfo(), + /* startTransaction= */ mock(), + /* finishTransaction= */ mock(), + /* finishCallback= */ {}, + ) + + assertFalse(isHandled) + } + + @Test + fun startAnimation_remoteTransition_returnsTrue() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition) + handler.addFiltered(TransitionFilter(), testRemoteTransition) + handler.handleRequest(mock(), request) + + val isHandled = handler.startAnimation( + /* transition= */ testRemoteTransition.remoteTransition.asBinder(), + /* info= */ createTransitionInfo(), + /* startTransaction= */ mock(), + /* finishTransaction= */ mock(), + /* finishCallback= */ {}, + ) + + assertTrue(isHandled) + } + + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_OPEN, + changeMode: Int = WindowManager.TRANSIT_CLOSE, + ): TransitionInfo = + TransitionInfo(type, /* flags= */ 0).apply { + addChange( + TransitionInfo.Change(mock(), mock()).apply { + mode = changeMode + parent = null + } + ) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 1371b38a579f..878324937f1a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -60,7 +60,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.TypedArray; -import android.graphics.PointF; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.net.Uri; @@ -1805,7 +1805,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @NonNull DisplayController displayController, @NonNull ActivityManager.RunningTaskInfo taskInfo, @NonNull Context decorWindowContext, - @NonNull Function2<? super Integer,? super Integer,? extends PointF> + @NonNull Function2<? super Integer,? super Integer,? extends Point> positionSupplier, @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { return mMaximizeMenu; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index bc8faedd77a9..e4424f3c57f2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.windowdecor.tiling import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.Resources import android.graphics.Rect @@ -24,8 +25,10 @@ import android.testing.AndroidTestingRunner import android.view.MotionEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -40,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeT import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createPinnedTask import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler @@ -552,6 +556,37 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { } @Test + fun taskTiled_shouldBeRemoved_whenEnteringPip() { + val task1 = createPipTask() + val stableBounds = STABLE_BOUNDS_MOCK + whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } + whenever(context.resources).thenReturn(resources) + whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width) + whenever(tiledTaskHelper.taskInfo).thenReturn(task1) + whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration) + tilingDecoration.onAppTiled( + task1, + desktopWindowDecoration, + DesktopTasksController.SnapPosition.LEFT, + BOUNDS, + ) + tilingDecoration.leftTaskResizingHelper = tiledTaskHelper + val changeInfo = createPipChangeTransition(task1) + tilingDecoration.onTransitionReady( + transition = mock(), + info = changeInfo, + startTransaction = mock(), + finishTransaction = mock(), + ) + + assertThat(tilingDecoration.leftTaskResizingHelper).isNull() + verify(tiledTaskHelper, times(1)).dispose() + } + + @Test fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() { val task1 = createVisibleTask() val task2 = createVisibleTask() @@ -652,6 +687,23 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true) } + private fun createPipTask() = + createPinnedTask().also { + whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true) + } + + private fun createPipChangeTransition(task: RunningTaskInfo?, type: Int = TRANSIT_PIP) = + TransitionInfo(type, /* flags= */ 0).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_PIP + parent = null + taskInfo = task + flags = flags + } + ) + } + companion object { private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100) private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100) diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a18c5f5f92f6..8ecd6ba9b253 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -6520,41 +6520,79 @@ base::expected<StringPiece16, NullOrIOError> StringPoolRef::string16() const { } bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const { - if (mError != NO_ERROR) { - return false; - } + if (mError != NO_ERROR) { + return false; + } - const ssize_t p = getResourcePackageIndex(resID); - const int t = Res_GETTYPE(resID); - const int e = Res_GETENTRY(resID); + const ssize_t p = getResourcePackageIndex(resID); + const int t = Res_GETTYPE(resID); + const int e = Res_GETENTRY(resID); - if (p < 0) { - if (Res_GETPACKAGE(resID)+1 == 0) { - ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); - } else { - ALOGW("No known package when getting flags for resource number 0x%08x", resID); - } - return false; - } - if (t < 0) { - ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); - return false; + if (p < 0) { + if (Res_GETPACKAGE(resID)+1 == 0) { + ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); + } else { + ALOGW("No known package when getting flags for resource number 0x%08x", resID); } + return false; + } + if (t < 0) { + ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); + return false; + } - const PackageGroup* const grp = mPackageGroups[p]; - if (grp == NULL) { - ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); - return false; - } + const PackageGroup* const grp = mPackageGroups[p]; + if (grp == NULL) { + ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); + return false; + } - Entry entry; - status_t err = getEntry(grp, t, e, NULL, &entry); - if (err != NO_ERROR) { - return false; + Entry entry; + status_t err = getEntry(grp, t, e, NULL, &entry); + if (err != NO_ERROR) { + return false; + } + + *outFlags = entry.specFlags; + return true; +} + +bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const { + if (mError != NO_ERROR) { + return false; + } + + const ssize_t p = getResourcePackageIndex(resID); + const int t = Res_GETTYPE(resID); + const int e = Res_GETENTRY(resID); + + if (p < 0) { + if (Res_GETPACKAGE(resID)+1 == 0) { + ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); + } else { + ALOGW("No known package when getting flags for resource number 0x%08x", resID); } + return false; + } + if (t < 0) { + ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); + return false; + } - *outFlags = entry.specFlags; - return true; + const PackageGroup* const grp = mPackageGroups[p]; + if (grp == NULL) { + ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); + return false; + } + + Entry entry; + status_t err = getEntry(grp, t, e, NULL, &entry); + if (err != NO_ERROR) { + return false; + } + + *outFlags = entry.entry->flags(); + return true; } bool ResTable::isPackageDynamic(uint8_t packageID) const { diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 0d45149267cf..63b28da075cd 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1593,6 +1593,8 @@ union ResTable_entry // If set, this is a compact entry with data type and value directly // encoded in the this entry, see ResTable_entry::compact FLAG_COMPACT = 0x0008, + // If set, this entry relies on read write android feature flags + FLAG_USES_FEATURE_FLAGS = 0x0010, }; struct Full { @@ -1622,6 +1624,7 @@ union ResTable_entry uint16_t flags() const { return dtohs(full.flags); }; bool is_compact() const { return flags() & FLAG_COMPACT; } bool is_complex() const { return flags() & FLAG_COMPLEX; } + bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; } size_t size() const { return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size); @@ -2039,6 +2042,8 @@ public: bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const; + bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const; + /** * Returns whether or not the package for the given resource has been dynamically assigned. * If the resource can't be found, returns 'false'. diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index 85bc8efe2750..e9590d50d719 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -18,6 +18,7 @@ package android.media; import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; @@ -37,4 +38,6 @@ oneway interface IMediaRouter2 { */ void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession, in MediaRoute2Info route); + void notifyDeviceSuggestionsUpdated(String suggestingPackageName, + in List<SuggestedDeviceInfo> suggestions); } diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 21908b2ca2e0..1c399d6958bb 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -21,6 +21,7 @@ import android.media.MediaRoute2Info; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; /** * {@hide} @@ -33,6 +34,8 @@ oneway interface IMediaRouter2Manager { in RouteDiscoveryPreference discoveryPreference); void notifyRouteListingPreferenceChange(String packageName, in @nullable RouteListingPreference routeListingPreference); + void notifyDeviceSuggestionsUpdated(String packageName, String suggestingPackageName, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifyRequestFailed(int requestId, int reason); void invalidateInstance(); diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 961962f6a010..60881f4bfc30 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -25,6 +25,7 @@ import android.media.MediaRouterClientState; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; /** @@ -72,6 +73,10 @@ interface IMediaRouterService { in MediaRoute2Info route); void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume); void releaseSessionWithRouter2(IMediaRouter2 router, String sessionId); + void setDeviceSuggestionsWithRouter2(IMediaRouter2 router, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router); // Methods for MediaRouter2Manager List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); @@ -98,4 +103,8 @@ interface IMediaRouterService { String sessionId, int volume); void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId); boolean showMediaOutputSwitcherWithProxyRouter(IMediaRouter2Manager manager); + void setDeviceSuggestionsWithManager(IMediaRouter2Manager manager, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3af36a404c30..db305effac3f 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -22,6 +22,7 @@ import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; +import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; import android.Manifest; import android.annotation.CallbackExecutor; @@ -159,6 +160,8 @@ public final class MediaRouter2 { new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord> + mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests = new CopyOnWriteArrayList<>(); @@ -198,6 +201,10 @@ public final class MediaRouter2 { @Nullable private RouteListingPreference mRouteListingPreference; + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>(); + /** * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback * dispatch. This is only used to determine what callback a route should be assigned to (added, @@ -760,6 +767,27 @@ public final class MediaRouter2 { } /** + * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target + * router changes. + * + * <p>Calls using a previously registered callback will overwrite the previous executor. + * + * @hide + */ + public void registerDeviceSuggestionsCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null"); + + DeviceSuggestionsCallbackRecord record = + new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback); + + mDeviceSuggestionsCallbackRecords.remove(record); + mDeviceSuggestionsCallbackRecords.add(record); + } + + /** * Unregisters the given callback to not receive {@link RouteListingPreference} change events. * * @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer) @@ -779,6 +807,21 @@ public final class MediaRouter2 { } /** + * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events. + * + * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback) + * @hide + */ + public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + + if (!mDeviceSuggestionsCallbackRecords.remove( + new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) { + Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback"); + } + } + + /** * Shows the system output switcher dialog. * * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on @@ -832,6 +875,36 @@ public final class MediaRouter2 { } /** + * Sets the suggested devices. + * + * <p>Use this method to inform the system UI that this device is suggested in the Output + * Switcher and media controls. + * + * <p>You should pass null to this method to clear a previously set suggestion without setting a + * new one. + * + * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be + * provided to the user. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mImpl.setDeviceSuggestions(suggestedDeviceInfo); + } + + /** + * Gets the current suggested devices. + * + * @return the suggested devices, keyed by the package name providing each suggestion list. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + return mImpl.getDeviceSuggestions(); + } + + /** * Returns the current {@link RouteListingPreference} of the target router. * * <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns @@ -1518,6 +1591,17 @@ public final class MediaRouter2 { } } + private void notifyDeviceSuggestionsUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) { + record.mExecutor.execute( + () -> + record.mDeviceSuggestionsCallback.onSuggestionUpdated( + suggestingPackageName, deviceSuggestions)); + } + } + private void notifyTransfer(RoutingController oldController, RoutingController newController) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute( @@ -1568,6 +1652,25 @@ public final class MediaRouter2 { .build(); } + /** + * Callback for receiving events about device suggestions + * + * @hide + */ + public interface DeviceSuggestionsCallback { + + /** + * Called when suggestions are updated. Whenever you register a callback, this will be + * invoked with the current suggestions. + * + * @param suggestingPackageName the package that provided the suggestions. + * @param suggestedDeviceInfo the suggestions provided by the package. + */ + void onSuggestionUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + } + /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** @@ -2326,6 +2429,35 @@ public final class MediaRouter2 { } } + private static final class DeviceSuggestionsCallbackRecord { + public final Executor mExecutor; + public final DeviceSuggestionsCallback mDeviceSuggestionsCallback; + + /* package */ DeviceSuggestionsCallbackRecord( + @NonNull Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + mExecutor = executor; + mDeviceSuggestionsCallback = deviceSuggestionsCallback; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeviceSuggestionsCallbackRecord)) { + return false; + } + return mDeviceSuggestionsCallback + == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback; + } + + @Override + public int hashCode() { + return mDeviceSuggestionsCallback.hashCode(); + } + } + static final class TransferCallbackRecord { public final Executor mExecutor; public final TransferCallback mTransferCallback; @@ -2446,6 +2578,17 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2::notifyDeviceSuggestionsUpdated, + MediaRouter2.this, + suggestingPackageName, + suggestions)); + } + + @Override public void requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { mHandler.sendMessage( @@ -2487,6 +2630,11 @@ public final class MediaRouter2 { void setRouteListingPreference(@Nullable RouteListingPreference preference); + void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + + @Nullable + Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions(); + boolean showSystemOutputSwitcher(); List<MediaRoute2Info> getAllRoutes(); @@ -2687,6 +2835,29 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + synchronized (mLock) { + try { + mMediaRouterService.setDeviceSuggestionsWithManager( + mClient, suggestedDeviceInfo); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithManager(mClient); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { try { return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient); @@ -3296,6 +3467,23 @@ public final class MediaRouter2 { notifyRouteListingPreferenceUpdated(routeListingPreference); } + private void onDeviceSuggestionsChangeHandler( + @NonNull String packageName, + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + if (!TextUtils.equals(getClientPackageName(), packageName)) { + return; + } + synchronized (mLock) { + if (Objects.equals( + mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) { + return; + } + mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo); + } + notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } + private void onRequestFailedOnHandler(int requestId, int reason) { MediaRouter2Manager.TransferRequest matchingRequest = null; for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { @@ -3390,6 +3578,20 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + mHandler.sendMessage( + obtainMessage( + ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler, + ProxyMediaRouter2Impl.this, + packageName, + suggestingPackageName, + deviceSuggestions)); + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( @@ -3553,6 +3755,30 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + synchronized (mLock) { + try { + registerRouterStubIfNeededLocked(); + mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { synchronized (mLock) { try { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3f18eef2f9aa..bf88709eec33 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -1138,6 +1138,14 @@ public final class MediaRouter2Manager { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + // MediaRouter2Manager doesn't support device suggestions + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( diff --git a/media/java/android/media/SuggestedDeviceInfo.aidl b/media/java/android/media/SuggestedDeviceInfo.aidl new file mode 100644 index 000000000000..eab642572ed2 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable SuggestedDeviceInfo; diff --git a/media/java/android/media/SuggestedDeviceInfo.java b/media/java/android/media/SuggestedDeviceInfo.java new file mode 100644 index 000000000000..2aa139fcca17 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.java @@ -0,0 +1,235 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Allows applications to suggest routes to the system UI (for example, in the System UI Output + * Switcher). + * + * @see MediaRouter2#setSuggestedDevice + * @hide + */ +@FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) +public final class SuggestedDeviceInfo implements Parcelable { + @NonNull + public static final Creator<SuggestedDeviceInfo> CREATOR = + new Creator<>() { + @Override + public SuggestedDeviceInfo createFromParcel(Parcel in) { + return new SuggestedDeviceInfo(in); + } + + @Override + public SuggestedDeviceInfo[] newArray(int size) { + return new SuggestedDeviceInfo[size]; + } + }; + + @NonNull private final String mDeviceDisplayName; + + @NonNull private final String mRouteId; + + private final int mType; + + @NonNull private final Bundle mExtras; + + private SuggestedDeviceInfo(Builder builder) { + mDeviceDisplayName = builder.mDeviceDisplayName; + mRouteId = builder.mRouteId; + mType = builder.mType; + mExtras = builder.mExtras; + } + + private SuggestedDeviceInfo(Parcel in) { + mDeviceDisplayName = in.readString(); + mRouteId = in.readString(); + mType = in.readInt(); + mExtras = in.readBundle(); + } + + /** + * Returns the name to be displayed to the user. + * + * @return The device display name. + */ + @NonNull + public String getDeviceDisplayName() { + return mDeviceDisplayName; + } + + /** + * Returns the route ID associated with the suggestion. + * + * @return The route ID. + */ + @NonNull + public String getRouteId() { + return mRouteId; + } + + /** + * Returns the device type associated with the suggestion. + * + * @return The device type. + */ + public int getType() { + return mType; + } + + /** + * Returns the extras associated with the suggestion. + * + * @return The extras. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + // SuggestedDeviceInfo Parcelable implementation. + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mDeviceDisplayName); + dest.writeString(mRouteId); + dest.writeInt(mType); + dest.writeBundle(mExtras); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SuggestedDeviceInfo)) { + return false; + } + return Objects.equals(mDeviceDisplayName, ((SuggestedDeviceInfo) obj).mDeviceDisplayName) + && Objects.equals(mRouteId, ((SuggestedDeviceInfo) obj).mRouteId) + && mType == ((SuggestedDeviceInfo) obj).mType; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceDisplayName, mRouteId, mType); + } + + @Override + public String toString() { + return mDeviceDisplayName + " | " + mRouteId + " | " + mType; + } + + /** Builder for {@link SuggestedDeviceInfo}. */ + public static final class Builder { + @NonNull private String mDeviceDisplayName; + + @NonNull private String mRouteId; + + @NonNull private Integer mType; + + private Bundle mExtras = Bundle.EMPTY; + + /** + * Creates a new SuggestedDeviceInfo. The device display name, route ID, and type must be + * set. The extras cannot be null, but default to an empty {@link Bundle}. + * + * @throws IllegalArgumentException if the builder has a mandatory unset field. + */ + public SuggestedDeviceInfo build() { + if (mDeviceDisplayName == null) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + + if (mRouteId == null) { + throw new IllegalArgumentException("Route ID cannot be null."); + } + + if (mType == null) { + throw new IllegalArgumentException("Device type cannot be null."); + } + + if (mExtras == null) { + throw new IllegalArgumentException("Extras cannot be null."); + } + + return new SuggestedDeviceInfo(this); + } + + /** + * Sets the {@link #getDeviceDisplayName() device display name}. + * + * @throws IllegalArgumentException if the name is null or empty. + */ + public Builder setDeviceDisplayName(@NonNull String deviceDisplayName) { + if (TextUtils.isEmpty(deviceDisplayName)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mDeviceDisplayName = deviceDisplayName; + return this; + } + + /** + * Sets the {@link #getRouteId() route id}. + * + * @throws IllegalArgumentException if the route id is null or empty. + */ + public Builder setRouteId(@NonNull String routeId) { + if (TextUtils.isEmpty(routeId)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mRouteId = routeId; + return this; + } + + /** Sets the {@link #getType() device type}. */ + public Builder setType(int type) { + mType = type; + return this; + } + + /** + * Sets the {@link #getExtras() extras}. + * + * <p>The default value is an empty {@link Bundle}. + * + * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can use + * {@link Bundle#deepCopy()} to keep a mutable copy. + * + * @throws NullPointerException if the extras are null. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras, "extras must not be null"); + return this; + } + } +} diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 0deed3982d9b..e39a0aa8717e 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -62,6 +62,14 @@ flag { } flag { + name: "enable_suggested_device_api" + is_exported: true + namespace: "media_better_together" + description: "Enables the API allowing proxy routers to suggest routes." + bug: "393216553" +} + +flag { name: "enable_full_scan_with_media_content_control" namespace: "media_better_together" description: "Allows holders of the MEDIA_CONTENT_CONTROL permission to scan for routes while not in the foreground." diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java index e4de3e4420fe..fccdba8e727f 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -82,7 +82,7 @@ public class MediaQualityContract { String PARAMETER_NAME = "_name"; String PARAMETER_PACKAGE = "_package"; String PARAMETER_INPUT_ID = "_input_id"; - + String VENDOR_PARAMETERS = "_vendor_parameters"; } /** diff --git a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl index cdf6e23f4b47..40848fe6f875 100644 --- a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl +++ b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl @@ -21,5 +21,5 @@ package android.media.tv.extension.scan; */ interface IHDPlusInfo { // Specifying a HDPlusInfo and start a network scan. - int setHDPlusInfo(String isBlindScanContinue, String isHDMode); + int setHDPlusInfo(boolean isBlindScanContinue, boolean isHDMode); } diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index d55bbb3bbdbf..bf739620bc99 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -187,7 +187,9 @@ public class IllustrationPreference extends Preference implements GroupSectionDi if (mLottieDynamicColor) { LottieColorUtils.applyDynamicColors(getContext(), illustrationView); } - LottieColorUtils.applyMaterialColor(getContext(), illustrationView); + if (SettingsThemeHelper.isExpressiveTheme(getContext())) { + LottieColorUtils.applyMaterialColor(getContext(), illustrationView); + } if (mOnBindListener != null) { mOnBindListener.onBind(illustrationView); diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java index 4421424c0e39..e59cc81d3ba8 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java @@ -157,10 +157,6 @@ public class LottieColorUtils { /** Applies material colors. */ public static void applyMaterialColor(@NonNull Context context, @NonNull LottieAnimationView lottieAnimationView) { - if (!SettingsThemeHelper.isExpressiveTheme(context)) { - return; - } - for (String key : MATERIAL_COLOR_MAP.keySet()) { final int color = context.getColor(MATERIAL_COLOR_MAP.get(key)); lottieAnimationView.addValueCallback( diff --git a/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt new file mode 100644 index 000000000000..92455c05e3d7 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt @@ -0,0 +1,22 @@ +/* + * 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.settingslib.supervision + +/** Constants used in supervision logs. */ +object SupervisionLog { + const val TAG = "SupervisionSettings" +} diff --git a/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt new file mode 100644 index 000000000000..1be8a17915a1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt @@ -0,0 +1,78 @@ +/* + * 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.settingslib.supervision + +import android.app.admin.DeviceAdminReceiver +import android.app.supervision.SupervisionManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.UserHandle +import android.util.Log +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin + +/** Helper class for supervision-enforced restrictions. */ +object SupervisionRestrictionsHelper { + + /** + * Creates an instance of [EnforcedAdmin] that uses the correct supervision component or returns + * null if supervision is not enabled. + */ + @JvmStatic + fun createEnforcedAdmin( + context: Context, + restriction: String, + user: UserHandle, + ): EnforcedAdmin? { + val supervisionManager = context.getSystemService(SupervisionManager::class.java) + val supervisionAppPackage = supervisionManager?.activeSupervisionAppPackage ?: return null + var supervisionComponent: ComponentName? = null + + // Try to find the service whose package matches the active supervision app. + val resolveSupervisionApps = + context.packageManager.queryIntentServicesAsUser( + Intent("android.app.action.BIND_SUPERVISION_APP_SERVICE"), + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, + user.identifier, + ) + resolveSupervisionApps + .mapNotNull { it.serviceInfo?.componentName } + .find { it.packageName == supervisionAppPackage } + ?.let { supervisionComponent = it } + + if (supervisionComponent == null) { + // Try to find the PO receiver whose package matches the active supervision app, for + // backwards compatibility. + val resolveDeviceAdmins = + context.packageManager.queryBroadcastReceiversAsUser( + Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, + user.identifier, + ) + resolveDeviceAdmins + .mapNotNull { it.activityInfo?.componentName } + .find { it.packageName == supervisionAppPackage } + ?.let { supervisionComponent = it } + } + + if (supervisionComponent == null) { + Log.d(SupervisionLog.TAG, "Could not find the supervision component.") + } + return EnforcedAdmin(supervisionComponent, restriction, user) + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java index c5e6f60e3fa6..2b95fd1cdd9d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java @@ -93,18 +93,10 @@ public class CreateUserActivity extends Activity { @Override public boolean onTouchEvent(@Nullable MotionEvent event) { - onBackInvoked(); + cancel(); return super.onTouchEvent(event); } - private void onBackInvoked() { - if (mSetupUserDialog != null) { - mSetupUserDialog.dismiss(); - } - setResult(RESULT_CANCELED); - finish(); - } - @VisibleForTesting void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) { Intent intent = new Intent(this, CreateUserActivity.class); @@ -112,14 +104,12 @@ public class CreateUserActivity extends Activity { intent.putExtra(EXTRA_IS_ADMIN, isAdmin); intent.putExtra(EXTRA_USER_ICON_PATH, path); - mSetupUserDialog.dismiss(); setResult(RESULT_OK, intent); finish(); } @VisibleForTesting void cancel() { - mSetupUserDialog.dismiss(); setResult(RESULT_CANCELED); finish(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index d9f1b632323c..bce229f5a13d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -282,7 +282,7 @@ public class CreateUserDialogController { mCustomDialogHelper.getDialog().dismiss(); break; case EXIT_DIALOG: - finish(); + mUserCreationDialog.dismiss(); break; default: if (mCurrentState < EXIT_DIALOG) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS new file mode 100644 index 000000000000..04e7058b4384 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS @@ -0,0 +1 @@ +file:platform/frameworks/base:/core/java/android/app/supervision/OWNERS diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt index 2ceed2875cb4..83ffa9399dc0 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt @@ -19,6 +19,7 @@ package com.android.settingslib.supervision import android.app.supervision.SupervisionManager import android.content.Context import android.content.ContextWrapper +import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ResolveInfo import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -77,8 +78,8 @@ class SupervisionIntentProviderTest { fun getSettingsIntent_unresolvedIntent() { `when`(mockSupervisionManager.activeSupervisionAppPackage) .thenReturn(SUPERVISION_APP_PACKAGE) - `when`(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) - .thenReturn(emptyList()) + `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) val intent = SupervisionIntentProvider.getSettingsIntent(context) @@ -89,7 +90,7 @@ class SupervisionIntentProviderTest { fun getSettingsIntent_resolvedIntent() { `when`(mockSupervisionManager.activeSupervisionAppPackage) .thenReturn(SUPERVISION_APP_PACKAGE) - `when`(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) + `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt())) .thenReturn(listOf(ResolveInfo())) val intent = SupervisionIntentProvider.getSettingsIntent(context) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt new file mode 100644 index 000000000000..872fc2a44b3d --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt @@ -0,0 +1,178 @@ +/* + * 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.settingslib.supervision + +import android.app.admin.DeviceAdminReceiver +import android.app.supervision.SupervisionManager +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +/** + * Unit tests for [SupervisionRestrictionsHelper]. + * + * Run with `atest SupervisionRestrictionsHelperTest`. + */ +@RunWith(AndroidJUnit4::class) +class SupervisionRestrictionsHelperTest { + @get:Rule val mocks: MockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var mockPackageManager: PackageManager + + @Mock private lateinit var mockSupervisionManager: SupervisionManager + + private lateinit var context: Context + + @Before + fun setUp() { + context = + object : ContextWrapper(InstrumentationRegistry.getInstrumentation().context) { + override fun getPackageManager() = mockPackageManager + + override fun getSystemService(name: String) = + when (name) { + Context.SUPERVISION_SERVICE -> mockSupervisionManager + else -> super.getSystemService(name) + } + } + } + + @Test + fun createEnforcedAdmin_nullSupervisionPackage() { + `when`(mockSupervisionManager.activeSupervisionAppPackage).thenReturn(null) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNull() + } + + @Test + fun createEnforcedAdmin_supervisionAppService() { + val resolveInfo = + ResolveInfo().apply { + serviceInfo = + ServiceInfo().apply { + packageName = SUPERVISION_APP_PACKAGE + name = "service.class" + } + } + + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`( + mockPackageManager.queryIntentServicesAsUser( + argThat(hasAction("android.app.action.BIND_SUPERVISION_APP_SERVICE")), + anyInt(), + eq(USER_ID), + ) + ) + .thenReturn(listOf(resolveInfo)) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isEqualTo(resolveInfo.serviceInfo.componentName) + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + @Test + fun createEnforcedAdmin_profileOwnerReceiver() { + val resolveInfo = + ResolveInfo().apply { + activityInfo = + ActivityInfo().apply { + packageName = SUPERVISION_APP_PACKAGE + name = "service.class" + } + } + + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`(mockPackageManager.queryIntentServicesAsUser(any<Intent>(), anyInt(), eq(USER_ID))) + .thenReturn(emptyList<ResolveInfo>()) + `when`( + mockPackageManager.queryBroadcastReceiversAsUser( + argThat(hasAction(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED)), + anyInt(), + eq(USER_ID), + ) + ) + .thenReturn(listOf(resolveInfo)) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isEqualTo(resolveInfo.activityInfo.componentName) + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + @Test + fun createEnforcedAdmin_noSupervisionComponent() { + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`(mockPackageManager.queryIntentServicesAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) + `when`(mockPackageManager.queryBroadcastReceiversAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isNull() + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + private fun hasAction(action: String) = + object : ArgumentMatcher<Intent> { + override fun matches(intent: Intent?) = intent?.action == action + } + + private companion object { + const val SUPERVISION_APP_PACKAGE = "app.supervision" + const val RESTRICTION = "restriction" + val USER_HANDLE = UserHandle.CURRENT + val USER_ID = USER_HANDLE.identifier + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java index f58eb7cc2e31..220c03e53be2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java @@ -65,23 +65,23 @@ public class CreateUserActivityTest { } @Test - public void onTouchEvent_dismissesDialogAndCancelsResult() { + public void onTouchEvent_finishesActivityAndCancelsResult() { mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0)); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()) .isEqualTo(Activity.RESULT_CANCELED); } @Test - public void setSuccessResult_dismissesDialogAndSetsSuccessResult() { + public void setSuccessResult_finishesActivityAndSetsSuccessResult() { Drawable mockDrawable = mock(Drawable.class); mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH, TEST_IS_ADMIN); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK); Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent(); @@ -92,10 +92,10 @@ public class CreateUserActivityTest { } @Test - public void cancel_dismissesDialogAndSetsCancelResult() { + public void cancel_finishesActivityAndSetsCancelResult() { mCreateUserActivity.cancel(); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()) .isEqualTo(Activity.RESULT_CANCELED); } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 72ae76a45cac..f628a420d8fa 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -312,6 +312,9 @@ <!-- Permission necessary to change car audio volume through CarAudioManager --> <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" /> + <!-- To detect when projecting to Android Auto --> + <uses-permission android:name="android.permission.READ_PROJECTION_STATE" /> + <!-- Permission to control Android Debug Bridge (ADB) --> <uses-permission android:name="android.permission.MANAGE_DEBUGGING" /> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 42f576a9a151..9dd49d6b1015 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1401,6 +1401,16 @@ flag { } flag { + name: "media_controls_device_manager_background_execution" + namespace: "systemui" + description: "Sends some instances creation to background thread" + bug: "400200474" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "output_switcher_redesign" namespace: "systemui" description: "Enables visual update for Media Output Switcher" @@ -1843,6 +1853,16 @@ flag { } flag { + name: "disable_shade_visible_with_blur" + namespace: "systemui" + description: "Removes the check for a blur radius when determining shade window visibility" + bug: "356804470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_row_transparency" namespace: "systemui" description: "Enables transparency on the Notification Shade." @@ -2121,3 +2141,10 @@ flag { description: "Enables moving the launching window on top of the origin window in the Animation library." bug: "390422470" } + +flag { + name: "status_bar_chips_return_animations" + namespace: "systemui" + description: "Enables return animations for status bar chips" + bug: "202516970" +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index e43b8a0b9297..7ee6a6e5ebf4 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -107,6 +107,16 @@ constructor( */ // TODO(b/301385865): Remove this flag. private val disableWmTimeout: Boolean = false, + + /** + * Whether we should disable the reparent transaction that puts the opening/closing window above + * the view's window. This should be set to true in tests only, where we can't currently use a + * valid leash. + * + * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper anymore + * and we can just inject a fake transaction. + */ + private val skipReparentTransaction: Boolean = false, ) { @JvmOverloads constructor( @@ -1140,6 +1150,7 @@ constructor( DelegatingAnimationCompletionListener(listener, this::dispose), transitionAnimator, disableWmTimeout, + skipReparentTransaction, ) } @@ -1173,6 +1184,16 @@ constructor( */ // TODO(b/301385865): Remove this flag. disableWmTimeout: Boolean = false, + + /** + * Whether we should disable the reparent transaction that puts the opening/closing window + * above the view's window. This should be set to true in tests only, where we can't + * currently use a valid leash. + * + * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper + * anymore and we can just inject a fake transaction. + */ + private val skipReparentTransaction: Boolean = false, ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { private val transitionContainer = controller.transitionContainer private val context = transitionContainer.context @@ -1515,7 +1536,7 @@ constructor( ) } - if (moveTransitionAnimationLayer()) { + if (moveTransitionAnimationLayer() && !skipReparentTransaction) { // Ensure that the launching window is rendered above the view's window, // so it is not obstructed. // TODO(b/397180418): re-use the start transaction once the diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt index 9a746870c6ff..e07d7b337ba2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt @@ -16,17 +16,18 @@ package com.android.systemui.animation +import kotlin.text.buildString + class FontVariationUtils { private var mWeight = -1 private var mWidth = -1 private var mOpticalSize = -1 private var mRoundness = -1 - private var isUpdated = false + private var mCurrentFVar = "" /* * generate fontVariationSettings string, used for key in typefaceCache in TextAnimator * the order of axes should align to the order of parameters - * if every axis remains unchanged, return "" */ fun updateFontVariation( weight: Int = -1, @@ -34,15 +35,17 @@ class FontVariationUtils { opticalSize: Int = -1, roundness: Int = -1, ): String { - isUpdated = false + var isUpdated = false if (weight >= 0 && mWeight != weight) { isUpdated = true mWeight = weight } + if (width >= 0 && mWidth != width) { isUpdated = true mWidth = width } + if (opticalSize >= 0 && mOpticalSize != opticalSize) { isUpdated = true mOpticalSize = opticalSize @@ -52,23 +55,32 @@ class FontVariationUtils { isUpdated = true mRoundness = roundness } - var resultString = "" - if (mWeight >= 0) { - resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight" - } - if (mWidth >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth" - } - if (mOpticalSize >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + - "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize" - } - if (mRoundness >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness" + + if (!isUpdated) { + return mCurrentFVar } - return if (isUpdated) resultString else "" + + return buildString { + if (mWeight >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.WEIGHT.tag}' $mWeight") + } + + if (mWidth >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.WIDTH.tag}' $mWidth") + } + + if (mOpticalSize >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize") + } + + if (mRoundness >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.ROUND.tag}' $mRoundness") + } + } + .also { mCurrentFVar = it } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index 5d9c441db003..a4a96d19e8bb 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -1006,13 +1006,32 @@ class TransitionAnimator( Log.d(TAG, "Animation ended") } - // TODO(b/330672236): Post this to the main thread instead so that it does not - // flicker with Flexiglass enabled. - controller.onTransitionAnimationEnd(isExpandingFullyAbove) - transitionContainerOverlay.remove(windowBackgroundLayer) + val onEnd = { + controller.onTransitionAnimationEnd(isExpandingFullyAbove) + transitionContainerOverlay.remove(windowBackgroundLayer) - if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { - openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) + if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { + openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) + } + } + // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not + // flicker with Flexiglass enabled. + if (controller.isLaunching) { + onEnd() + } else { + // onAnimationEnd is called at the end of the animation, on a Choreographer animation + // tick. During dialog launches, the following calls will move the animated content from + // the dialog overlay back to its original position, and this change must be reflected + // in the next frame given that we then sync the next frame of both the content and + // dialog ViewRoots. During SysUI activity launches, we will instantly collapse the + // shade at the end of the transition. However, if those are rendered by Compose, whose + // compositions are also scheduled on a Choreographer frame, any state change made + // *right now* won't be reflected in the next frame given that a Choreographer frame + // can't schedule another and have it happen in the same frame. So we post the forwarded + // calls to [Controller.onLaunchAnimationEnd] in the main executor, leaving this + // Choreographer frame, ensuring that any state change applied by + // onTransitionAnimationEnd() will be reflected in the same frame. + mainExecutor.execute { onEnd() } } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt new file mode 100644 index 000000000000..fce536a60b84 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt @@ -0,0 +1,84 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UFile +import org.jetbrains.uast.UImportStatement + +/** Detects whether [runBlocking] is being imported. */ +class RunBlockingDetector : Detector(), SourceCodeScanner { + + override fun getApplicableUastTypes(): List<Class<out UElement>> { + return listOf(UFile::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitFile(node: UFile) { + for (importStatement in node.imports) { + visitImportStatement(context, importStatement) + } + } + } + } + + private fun visitImportStatement(context: JavaContext, importStatement: UImportStatement) { + val importName = importStatement.importReference?.asSourceString() + if (FORBIDDEN_IMPORTS.contains(importName)) { + context.report( + ISSUE, + importStatement as UElement, + context.getLocation(importStatement), + "Importing $importName is not allowed.", + ) + } + } + + companion object { + @JvmField + val ISSUE = + Issue.create( + id = "RunBlockingUsage", + briefDescription = "Discouraged runBlocking call", + explanation = + """ + Using `runBlocking` is generally discouraged in Android + development as it can lead to UI freezes and ANRs. + Consider using `launch` or `async` with coroutine scope + instead. If needed from java, consider introducing a method + with a callback instead from kotlin. + """, + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation(RunBlockingDetector::class.java, Scope.JAVA_FILE_SCOPE), + ) + + val FORBIDDEN_IMPORTS = listOf("kotlinx.coroutines.runBlocking") + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index adb311610587..b455c0021517 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -50,6 +50,7 @@ class SystemUIIssueRegistry : IssueRegistry() { ShadeDisplayAwareDialogDetector.ISSUE, RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING, RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR, + RunBlockingDetector.ISSUE, ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt new file mode 100644 index 000000000000..4ae429d204aa --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt @@ -0,0 +1,104 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +class RunBlockingDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = RunBlockingDetector() + + override fun getIssues(): List<Issue> = listOf(RunBlockingDetector.ISSUE) + + @Test + fun testViolation() { + lint() + .files( + kotlin( + """ + package com.example + + import kotlinx.coroutines.runBlocking + + class MyClass { + fun myMethod() { + runBlocking { + // Some code here + } + } + } + """ + ), + RUN_BLOCKING_DEFINITION, + ) + .issues(RunBlockingDetector.ISSUE) + .run() + .expect( + """ +src/com/example/MyClass.kt:4: Warning: Importing kotlinx.coroutines.runBlocking is not allowed. [RunBlockingUsage] + import kotlinx.coroutines.runBlocking + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings +""" + .trimIndent() + ) + } + + // Verifies that the lint check does *not* flag calls to other methods. + @Test + fun testNotViolation() { + lint() + .detector(RunBlockingDetector()) + .issues(RunBlockingDetector.ISSUE) + .files( + kotlin( + """ + package com.example + + class MyClass { + fun myMethod() { + myOtherMethod { + } + } + + fun myOtherMethod(block: () -> Unit) { + block() + } + } + """ + ) + ) + .run() + .expectClean() + } + + private companion object { + val RUN_BLOCKING_DEFINITION = + kotlin( + """ + package kotlinx.coroutines + + fun runBlocking(block: suspend () -> Unit) { + // Implementation details don't matter for this test. + } + """ + ) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 4bf0ceb51784..6e29e6932629 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -38,8 +38,9 @@ import com.android.systemui.animation.TypefaceVariantCacheImpl import com.android.systemui.customization.R import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogcatOnlyMessageBuffer -import com.android.systemui.log.core.Logger import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime import java.io.PrintWriter import java.util.Calendar import java.util.Locale @@ -67,7 +68,7 @@ constructor( var messageBuffer: MessageBuffer get() = logger.buffer set(value) { - logger = Logger(value, TAG) + logger = ClockLogger(this, value, TAG) } var hasCustomPositionUpdatedAnimation: Boolean = false @@ -185,7 +186,9 @@ constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) - logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() } + logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) { + str1 = formattedText?.toString() + } // Setting text actually triggers a layout pass in TextView (because the text view is set to // wrap_content width and TextView always relayouts for this). This avoids needless relayout @@ -195,7 +198,7 @@ constructor( } text = formattedText - logger.d({ "refreshTime: done setting new time text to: $str1" }) { + logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) { str1 = formattedText?.toString() } @@ -225,7 +228,7 @@ constructor( @SuppressLint("DrawAllocation") override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - logger.d("onMeasure") + logger.onMeasure(widthMeasureSpec, heightMeasureSpec) if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize @@ -263,14 +266,14 @@ constructor( canvas.translate(parentWidth / 4f, 0f) } - logger.d({ "onDraw($str1)" }) { str1 = text.toString() } + logger.onDraw("$text") // intentionally doesn't call super.onDraw here or else the text will be rendered twice textAnimator?.draw(canvas) canvas.restore() } override fun invalidate() { - logger.d("invalidate") + logger.invalidate() super.invalidate() } @@ -280,7 +283,7 @@ constructor( lengthBefore: Int, lengthAfter: Int, ) { - logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() } + logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" } super.onTextChanged(text, start, lengthBefore, lengthAfter) } @@ -370,7 +373,7 @@ constructor( return } - logger.d("animateCharge") + logger.animateCharge() val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -394,7 +397,7 @@ constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - logger.d("animateDoze") + logger.animateDoze(isDozing, animate) setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, color = if (isDozing) dozingColor else lockScreenColor, @@ -484,7 +487,7 @@ constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } - logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() } + logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() } descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12 refreshTime() @@ -634,7 +637,7 @@ constructor( companion object { private val TAG = AnimatableClockView::class.simpleName!! - private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG) + private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG) const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600 private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt index dd1599e5259d..9857d7f3d69d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt @@ -17,6 +17,8 @@ package com.android.systemui.shared.clocks import android.graphics.Canvas +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF object CanvasUtil { fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt index f5ccc52c8c6b..941cebfb4014 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt @@ -20,7 +20,8 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.TimeInterpolator import android.animation.ValueAnimator -import com.android.systemui.shared.clocks.VPointF.Companion.times +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.times class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) { var currentTranslation = VPointF.ZERO diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt index 336c66eed889..9ac9e60f05fd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt @@ -16,13 +16,13 @@ package com.android.systemui.shared.clocks -import android.graphics.RectF import android.view.View import androidx.annotation.VisibleForTesting import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.VRectF interface SimpleClockLayerController { val view: View @@ -32,5 +32,5 @@ interface SimpleClockLayerController { val config: ClockFaceConfig @VisibleForTesting var fakeTimeMills: Long? - var onViewBoundsChanged: ((RectF) -> Unit)? + var onViewBoundsChanged: ((VRectF) -> Unit)? } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt index 1e90a2370786..0740b0e504cb 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt @@ -18,8 +18,9 @@ package com.android.systemui.shared.clocks import android.graphics.Rect import android.view.View -import com.android.systemui.shared.clocks.VPoint.Companion.center -import com.android.systemui.shared.clocks.VPointF.Companion.center +import com.android.systemui.plugins.clocks.VPoint.Companion.center +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.center object ViewUtils { fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 2dc3e2b7af73..ba32ab083063 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -17,7 +17,6 @@ package com.android.systemui.shared.clocks.view import android.graphics.Canvas -import android.graphics.RectF import android.icu.text.NumberFormat import android.util.MathUtils.constrainedMap import android.view.View @@ -29,14 +28,15 @@ import com.android.app.animation.Interpolators import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.max +import com.android.systemui.plugins.clocks.VPointF.Companion.times +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.shared.clocks.CanvasUtil.translate import com.android.systemui.shared.clocks.CanvasUtil.use import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.DigitTranslateAnimator -import com.android.systemui.shared.clocks.VPoint -import com.android.systemui.shared.clocks.VPointF -import com.android.systemui.shared.clocks.VPointF.Companion.max -import com.android.systemui.shared.clocks.VPointF.Companion.times import com.android.systemui.shared.clocks.ViewUtils.measuredSize import java.util.Locale import kotlin.collections.filterNotNull @@ -101,7 +101,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { updateLocale(Locale.getDefault()) } - var onViewBoundsChanged: ((RectF) -> Unit)? = null + var onViewBoundsChanged: ((VRectF) -> Unit)? = null private val digitOffsets = mutableMapOf<Int, Float>() protected fun calculateSize( @@ -189,13 +189,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { fun updateLocation() { val layoutBounds = this.layoutBounds ?: return - val bounds = - RectF( - layoutBounds.centerX() - measuredWidth / 2f, - layoutBounds.centerY() - measuredHeight / 2f, - layoutBounds.centerX() + measuredWidth / 2f, - layoutBounds.centerY() + measuredHeight / 2f, - ) + val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize) setFrame( bounds.left.roundToInt(), bounds.top.roundToInt(), @@ -215,16 +209,11 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { onAnimateDoze = null } - private val layoutBounds = RectF() + private var layoutBounds = VRectF.ZERO override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { logger.onLayout(changed, left, top, right, bottom) - - layoutBounds.left = left.toFloat() - layoutBounds.top = top.toFloat() - layoutBounds.right = right.toFloat() - layoutBounds.bottom = bottom.toFloat() - + layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) updateChildFrames(isLayout = true) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 0ec2d188833a..2af25fe339a2 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -23,7 +23,6 @@ import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect -import android.graphics.RectF import android.os.VibrationEffect import android.text.Layout import android.text.TextPaint @@ -45,6 +44,10 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.size +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.shared.clocks.CanvasUtil.translate import com.android.systemui.shared.clocks.CanvasUtil.use @@ -53,9 +56,6 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle -import com.android.systemui.shared.clocks.VPoint -import com.android.systemui.shared.clocks.VPointF -import com.android.systemui.shared.clocks.VPointF.Companion.size import com.android.systemui.shared.clocks.ViewUtils.measuredSize import com.android.systemui.shared.clocks.ViewUtils.size import com.android.systemui.shared.clocks.toClockAxisSetting @@ -66,11 +66,11 @@ import kotlin.math.roundToInt private val TAG = SimpleDigitalClockTextView::class.simpleName!! -private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF { - val rect = Rect() - this.getTextBounds(text, 0, text.length, rect) - result.set(rect) - return result +private val tempRect = Rect() + +private fun Paint.getTextBounds(text: CharSequence): VRectF { + this.getTextBounds(text, 0, text.length, tempRect) + return VRectF(tempRect) } enum class VerticalAlignment { @@ -143,7 +143,7 @@ open class SimpleDigitalClockTextView( fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar() } - var onViewBoundsChanged: ((RectF) -> Unit)? = null + var onViewBoundsChanged: ((VRectF) -> Unit)? = null private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1f var maxSingleDigitWidth = -1f @@ -159,13 +159,13 @@ open class SimpleDigitalClockTextView( private val initThread = Thread.currentThread() // textBounds is the size of text in LS, which only measures current text in lockscreen style - var textBounds = RectF() + var textBounds = VRectF.ZERO // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD // especially for the textView which has different bounds during the animation // prevTextBounds holds the state we are transitioning from - private val prevTextBounds = RectF() + private var prevTextBounds = VRectF.ZERO // targetTextBounds holds the state we are interpolating to - private val targetTextBounds = RectF() + private var targetTextBounds = VRectF.ZERO protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: ClockLogger.INIT_LOGGER @@ -215,8 +215,8 @@ open class SimpleDigitalClockTextView( lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface - lockScreenPaint.getTextBounds(text, textBounds) - targetTextBounds.set(textBounds) + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = textBounds textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation)) measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) @@ -287,7 +287,7 @@ open class SimpleDigitalClockTextView( canvas.use { digitTranslateAnimator?.apply { canvas.translate(currentTranslation) } canvas.translate(getDrawTranslation(interpBounds)) - if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f) + if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f) textAnimator.draw(canvas) } } @@ -302,16 +302,12 @@ open class SimpleDigitalClockTextView( super.setAlpha(alpha) } - private val layoutBounds = RectF() + private var layoutBounds = VRectF.ZERO override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) logger.onLayout(changed, left, top, right, bottom) - - layoutBounds.left = left.toFloat() - layoutBounds.top = top.toFloat() - layoutBounds.right = right.toFloat() - layoutBounds.bottom = bottom.toFloat() + layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) } override fun invalidate() { @@ -327,7 +323,7 @@ open class SimpleDigitalClockTextView( fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { if (!this::textAnimator.isInitialized) return - logger.animateDoze() + logger.animateDoze(isDozing, isAnimated) textAnimator.setTextStyle( TextAnimator.Style( fVar = if (isDozing) aodFontVariation else lsFontVariation, @@ -341,6 +337,11 @@ open class SimpleDigitalClockTextView( ), ) updateTextBoundsForTextAnimator() + + if (!isAnimated) { + requestLayout() + (parent as? FlexClockView)?.requestLayout() + } } fun animateCharge() { @@ -408,10 +409,10 @@ open class SimpleDigitalClockTextView( } fun refreshText() { - lockScreenPaint.getTextBounds(text, textBounds) - if (this::textAnimator.isInitialized) { - textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds) - } + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = + if (!this::textAnimator.isInitialized) textBounds + else textAnimator.textInterpolator.targetPaint.getTextBounds(text) if (layout == null) { requestLayout() @@ -432,23 +433,23 @@ open class SimpleDigitalClockTextView( } /** Returns the interpolated text bounding rect based on interpolation progress */ - private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF { + private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF { if (progress <= 0f) { return prevTextBounds } else if (!textAnimator.isRunning || progress >= 1f) { return targetTextBounds } - return RectF().apply { - left = lerp(prevTextBounds.left, targetTextBounds.left, progress) - right = lerp(prevTextBounds.right, targetTextBounds.right, progress) - top = lerp(prevTextBounds.top, targetTextBounds.top, progress) - bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress) - } + return VRectF( + left = lerp(prevTextBounds.left, targetTextBounds.left, progress), + right = lerp(prevTextBounds.right, targetTextBounds.right, progress), + top = lerp(prevTextBounds.top, targetTextBounds.top, progress), + bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress), + ) } private fun computeMeasuredSize( - interpBounds: RectF, + interpBounds: VRectF, widthMeasureSpec: Int = measuredWidthAndState, heightMeasureSpec: Int = measuredHeightAndState, ): VPointF { @@ -461,11 +462,11 @@ open class SimpleDigitalClockTextView( return VPointF( when { mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat() - else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth + else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth }, when { mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat() - else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth + else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth }, ) } @@ -489,44 +490,23 @@ open class SimpleDigitalClockTextView( } /** Set the location of the view to match the interpolated text bounds */ - private fun setInterpolatedLocation(measureSize: VPointF): RectF { - val targetRect = RectF() - targetRect.apply { - when (xAlignment) { - XAlignment.LEFT -> { - left = layoutBounds.left - right = layoutBounds.left + measureSize.x - } - XAlignment.CENTER -> { - left = layoutBounds.centerX() - measureSize.x / 2f - right = layoutBounds.centerX() + measureSize.x / 2f - } - XAlignment.RIGHT -> { - left = layoutBounds.right - measureSize.x - right = layoutBounds.right - } - } - - when (verticalAlignment) { - VerticalAlignment.TOP -> { - top = layoutBounds.top - bottom = layoutBounds.top + measureSize.y - } - VerticalAlignment.CENTER -> { - top = layoutBounds.centerY() - measureSize.y / 2f - bottom = layoutBounds.centerY() + measureSize.y / 2f - } - VerticalAlignment.BOTTOM -> { - top = layoutBounds.bottom - measureSize.y - bottom = layoutBounds.bottom - } - VerticalAlignment.BASELINE -> { - top = layoutBounds.centerY() - measureSize.y / 2f - bottom = layoutBounds.centerY() + measureSize.y / 2f - } - } - } + private fun setInterpolatedLocation(measureSize: VPointF): VRectF { + val pos = + VPointF( + when (xAlignment) { + XAlignment.LEFT -> layoutBounds.left + XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f + XAlignment.RIGHT -> layoutBounds.right - measureSize.x + }, + when (verticalAlignment) { + VerticalAlignment.TOP -> layoutBounds.top + VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f + VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y + VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f + }, + ) + val targetRect = VRectF.fromTopLeft(pos, measureSize) setFrame( targetRect.left.roundToInt(), targetRect.top.roundToInt(), @@ -537,7 +517,7 @@ open class SimpleDigitalClockTextView( return targetRect } - private fun getDrawTranslation(interpBounds: RectF): VPointF { + private fun getDrawTranslation(interpBounds: VRectF): VPointF { val sizeDiff = this.measuredSize - interpBounds.size val alignment = VPointF( @@ -586,11 +566,11 @@ open class SimpleDigitalClockTextView( if (fontSizePx > 0) { setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) lockScreenPaint.textSize = textSize - lockScreenPaint.getTextBounds(text, textBounds) - targetTextBounds.set(textBounds) + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = textBounds } if (!constrainedByHeight) { - val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2 + val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2 fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize } @@ -608,8 +588,8 @@ open class SimpleDigitalClockTextView( for (i in 0..9) { val rectForCalculate = lockScreenPaint.getTextBounds("$i") - maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height()) - maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width()) + maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height) + maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width) } maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth @@ -637,8 +617,8 @@ open class SimpleDigitalClockTextView( * and targetPaint will store the state we transition to */ private fun updateTextBoundsForTextAnimator() { - textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds) - textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds) + prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text) + targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index e26e19d27417..29647cd082b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -16,12 +16,18 @@ package com.android.keyguard; +import static com.android.internal.widget.flags.Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import android.hardware.input.InputManager; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.ViewGroup; @@ -90,6 +96,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { @Mock private UserActivityNotifier mUserActivityNotifier; private NumPadKey[] mButtons = new NumPadKey[]{}; + @Mock + private InputManager mInputManager; private KeyguardPinBasedInputViewController mKeyguardPinViewController; @@ -118,12 +126,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { new KeyguardKeyboardInteractor(new FakeKeyboardRepository()); FakeFeatureFlags featureFlags = new FakeFeatureFlags(); mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES); + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mEmergencyButtonController, mFalsingCollector, featureFlags, mSelectedUserInteractor, keyguardKeyboardInteractor, mBouncerHapticPlayer, - mUserActivityNotifier) { + mUserActivityNotifier, mInputManager) { @Override public void onResume(int reason) { super.onResume(reason); @@ -148,4 +157,112 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.resetState(); verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin); } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(2)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(2)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 142a2868ec14..7fea06ec7f41 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.testing.TestableLooper import android.view.View import android.view.ViewGroup @@ -104,6 +105,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Mock lateinit var enterButton: View @Mock lateinit var uiEventLogger: UiEventLogger @Mock lateinit var mUserActivityNotifier: UserActivityNotifier + @Mock lateinit var inputManager: InputManager @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @@ -154,6 +156,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index c751a7db51dc..003669da498e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.telephony.TelephonyManager import android.testing.TestableLooper import android.view.LayoutInflater @@ -73,6 +74,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier private val updateMonitorCallbackArgumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + @Mock private lateinit var inputManager: InputManager private val kosmos = testKosmos() @@ -107,6 +109,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index c34682551eda..85cb388ace5c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.telephony.PinResult import android.telephony.TelephonyManager import android.testing.TestableLooper @@ -65,6 +66,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier + @Mock private lateinit var inputManager: InputManager private val kosmos = testKosmos() @@ -102,6 +104,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) underTest.init() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt index 8d3640d8d809..53b364c13063 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt @@ -41,21 +41,9 @@ class FontVariationUtilsTest : SysuiTestCase() { @Test fun testStyleValueUnchange_getBlankStr() { val fontVariationUtils = FontVariationUtils() - fontVariationUtils.updateFontVariation( - weight = 100, - width = 100, - opticalSize = 0, - roundness = 100, - ) - val updatedFvar1 = - fontVariationUtils.updateFontVariation( - weight = 100, - width = 100, - opticalSize = 0, - roundness = 100, - ) - Assert.assertEquals("", updatedFvar1) - val updatedFvar2 = fontVariationUtils.updateFontVariation() - Assert.assertEquals("", updatedFvar2) + Assert.assertEquals("", fontVariationUtils.updateFontVariation()) + val fVar = fontVariationUtils.updateFontVariation(weight = 100) + Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation()) + Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt index 25a287c4cfff..15a6de896e92 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt @@ -247,20 +247,20 @@ class PinInputViewModelTest : SysuiTestCase() { } private class PinInputSubject -private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) : +private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel?) : Subject(metadata, actual) { fun matches(mnemonics: String) { val actualMnemonics = - actual.input - .map { entry -> + actual?.input + ?.map { entry -> when (entry) { is Digit -> entry.input.digitToChar() is ClearAll -> 'C' else -> throw IllegalArgumentException() } } - .joinToString(separator = "") + ?.joinToString(separator = "") if (mnemonics != actualMnemonics) { failWithActual( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt index 0f400892f988..56b06de0a9ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt @@ -17,14 +17,12 @@ package com.android.systemui.common.ui.view +import android.testing.TestableLooper +import android.view.MotionEvent import android.view.ViewConfiguration import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -33,18 +31,22 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { @Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle @Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit @Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit + @Mock private lateinit var onDoubleTapDetected: () -> Unit private lateinit var underTest: TouchHandlingViewInteractionHandler @@ -61,14 +63,17 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { underTest = TouchHandlingViewInteractionHandler( + context = context, postDelayed = postDelayed, isAttachedToWindow = { isAttachedToWindow }, onLongPressDetected = onLongPressDetected, onSingleTapDetected = onSingleTapDetected, + onDoubleTapDetected = onDoubleTapDetected, longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() }, allowedTouchSlop = ViewConfiguration.getTouchSlop(), ) underTest.isLongPressHandlingEnabled = true + underTest.isDoubleTapHandlingEnabled = true } @Test @@ -76,63 +81,250 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { val downX = 123 val downY = 456 dispatchTouchEvents( - Down(x = downX, y = downY), - Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_MOVE, + 123f + ViewConfiguration.getTouchSlop() - 0.1f, + 456f, + 0, + ), ) delayedRunnable?.run() verify(onLongPressDetected).invoke(downX, downY) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun longPressButFeatureNotEnabled() = runTest { underTest.isLongPressHandlingEnabled = false - dispatchTouchEvents(Down(x = 123, y = 456)) + dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0)) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun longPressButViewNotAttached() = runTest { isAttachedToWindow = false - dispatchTouchEvents(Down(x = 123, y = 456)) + dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0)) delayedRunnable?.run() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun draggedTooFarToBeConsideredAlongPress() = runTest { dispatchTouchEvents( - Down(x = 123, y = 456), - Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0), + // Drag action within touch slop + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply { + addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0) + }, ) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest { dispatchTouchEvents( - Down(x = 123, y = 456), - Up( - distanceMoved = ViewConfiguration.getTouchSlop().toFloat(), - gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L, + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain( + 0L, + ViewConfiguration.getLongPressTimeout() - 1L, + MotionEvent.ACTION_UP, + 123f, + 456F, + 0, ), ) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) verify(onSingleTapDetected).invoke(123, 456) } - private fun dispatchTouchEvents(vararg models: MotionEventModel) { - models.forEach { model -> underTest.onTouchEvent(model) } + @Test + fun doubleTap() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0), + ) + + verify(onDoubleTapDetected).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun doubleTapButFeatureNotEnabled() = runTest { + underTest.isDoubleTapHandlingEnabled = false + + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0), + ) + + verify(onDoubleTapDetected, never()).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoLongPress() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain( + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + MotionEvent.ACTION_MOVE, + 123f + ViewConfiguration.getTouchSlop() - 0.1f, + 456f, + 0, + ), + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onSingleTapDetected).invoke(anyInt(), anyInt()) + verify(onLongPressDetected).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain( + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + MotionEvent.ACTION_UP, + 123f, + 456f, + 0, + ), + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoDrag() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + // Drag event within touch slop + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0) + .apply { + addBatch( + secondTapTime, + 123f + ViewConfiguration.getTouchSlop() + 0.1f, + 456f, + 0f, + 0f, + 0, + ) + }, + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected).invoke(anyInt(), anyInt()) + } + + @Test + fun doubleTapOutOfAllowableSlop() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f + scaledDoubleTapSlop + 0.1f, + 456f + scaledDoubleTapSlop + 0.1f, + 0, + ), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_UP, + 123f + scaledDoubleTapSlop + 0.1f, + 456f + scaledDoubleTapSlop + 0.1f, + 0, + ), + ) + + verify(onDoubleTapDetected, never()).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + private fun dispatchTouchEvents(vararg events: MotionEvent) { + events.forEach { event -> underTest.onTouchEvent(event) } } } 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 ed73d89db2c7..6a25069a4e5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt @@ -73,12 +73,12 @@ class CommunalOngoingContentStartableTest : SysuiTestCase() { assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() - kosmos.setCommunalEnabled(true) + setCommunalEnabled(true) assertThat(fakeCommunalMediaRepository.isListening()).isTrue() assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() - kosmos.setCommunalEnabled(false) + setCommunalEnabled(false) assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() @@ -93,13 +93,13 @@ class CommunalOngoingContentStartableTest : SysuiTestCase() { assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() - kosmos.setCommunalEnabled(true) + setCommunalEnabled(true) // Media listening does not start when UMO is disabled. assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() - kosmos.setCommunalEnabled(false) + setCommunalEnabled(false) assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt new file mode 100644 index 000000000000..f9b29e9bc5b5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.UiModeManager +import android.app.UiModeManager.OnProjectionStateChangedListener +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CarProjectionRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val capturedListeners = mutableListOf<OnProjectionStateChangedListener>() + + private val Kosmos.uiModeManager by + Kosmos.Fixture<UiModeManager> { + mock { + on { + addOnProjectionStateChangedListener( + eq(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE), + any(), + any(), + ) + } doAnswer + { + val listener = it.getArgument<OnProjectionStateChangedListener>(2) + capturedListeners.add(listener) + Unit + } + + on { removeOnProjectionStateChangedListener(any()) } doAnswer + { + val listener = it.getArgument<OnProjectionStateChangedListener>(0) + capturedListeners.remove(listener) + Unit + } + + on { activeProjectionTypes } doReturn UiModeManager.PROJECTION_TYPE_NONE + } + } + + private val Kosmos.underTest by + Kosmos.Fixture { + CarProjectionRepositoryImpl( + uiModeManager = uiModeManager, + bgDispatcher = testDispatcher, + ) + } + + @Test + fun testProjectionActiveUpdatesAfterCallback() = + kosmos.runTest { + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isFalse() + + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) + assertThat(projectionActive).isTrue() + + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_NONE) + assertThat(projectionActive).isFalse() + } + + @Test + fun testProjectionInitialValueTrue() = + kosmos.runTest { + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) + + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isTrue() + } + + @Test + fun testUnsubscribeWhenCancelled() = + kosmos.runTest { + val job = underTest.projectionActive.launchIn(backgroundScope) + assertThat(capturedListeners).hasSize(1) + + job.cancel() + assertThat(capturedListeners).isEmpty() + } + + private fun Kosmos.setActiveProjectionType(@UiModeManager.ProjectionType projectionType: Int) { + uiModeManager.stub { on { activeProjectionTypes } doReturn projectionType } + capturedListeners.forEach { it.onProjectionStateChanged(projectionType, emptySet()) } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt index 5c983656225e..09d44a5e18d9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt @@ -34,9 +34,7 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.communal.data.model.DisabledReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING -import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED @@ -202,63 +200,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test - fun secondaryUserIsInvalid() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER)) - - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER) - } - - @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - @Test - fun classicFlagIsDisabled() = - kosmos.runTest { - setCommunalV2Enabled(false) - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) - } - - @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - @Test - fun communalHubFlagIsDisabled() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByUser() = - kosmos.runTest { - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING) - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id) - assertThat(enabledState?.enabled).isFalse() - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id) - assertThat(enabledState?.enabled).isTrue() - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByDevicePolicy() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isTrue() - - setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY) - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test fun widgetsAllowedForWorkProfile_isFalse_whenDisallowedByDevicePolicy() = kosmos.runTest { val widgetsAllowedForWorkProfile by @@ -269,36 +210,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT assertThat(widgetsAllowedForWorkProfile).isFalse() } - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsEnabled_whenDisallowedByDevicePolicyForWorkProfile() = - kosmos.runTest { - val enabledStateForPrimaryUser by - collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledStateForPrimaryUser?.enabled).isTrue() - - setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_WIDGETS_ALL) - assertThat(enabledStateForPrimaryUser?.enabled).isTrue() - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByUserAndDevicePolicy() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isTrue() - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) - setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) - - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState) - .containsExactly( - DisabledReason.DISABLED_REASON_DEVICE_POLICY, - DisabledReason.DISABLED_REASON_USER_SETTING, - ) - } - @Test @DisableFlags(FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND) fun backgroundType_defaultValue() = @@ -327,26 +238,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT } @Test - fun screensaverDisabledByUser() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 0, PRIMARY_USER.id) - - assertThat(enabledState).isFalse() - } - - @Test - fun screensaverEnabledByUser() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 1, PRIMARY_USER.id) - - assertThat(enabledState).isTrue() - } - - @Test fun whenToDream_charging() = kosmos.runTest { val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt new file mode 100644 index 000000000000..fc4cd43577b1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.carProjectionRepository +import com.android.systemui.communal.data.repository.fake +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CarProjectionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { carProjectionInteractor } + + @Test + fun testProjectionActive() = + kosmos.runTest { + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isFalse() + + carProjectionRepository.fake.setProjectionActive(true) + assertThat(projectionActive).isTrue() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt new file mode 100644 index 000000000000..f4a1c90a5471 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.posturing.data.repository.fake +import com.android.systemui.communal.posturing.data.repository.posturingRepository +import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.fakeDockManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAutoOpenInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { communalAutoOpenInteractor } + + @Before + fun setUp() { + runBlocking { kosmos.fakeUserRepository.asMainUser() } + with(kosmos.fakeSettings) { + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, false, MAIN_USER_ID) + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, false, MAIN_USER_ID) + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, false, MAIN_USER_ID) + } + } + + @Test + fun testStartWhileCharging() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(false) + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + batteryRepository.fake.setDevicePluggedIn(true) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartWhileDocked() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + fakeDockManager.setIsDocked(false) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + fakeDockManager.setIsDocked(true) + fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartWhilePostured() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + posturingRepository.fake.setPosturedState(PosturedState.NotPostured) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartNever() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + false, + MAIN_USER_ID, + ) + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + false, + MAIN_USER_ID, + ) + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + false, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + fakeDockManager.setIsDocked(true) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt deleted file mode 100644 index beec184b80e7..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.communal.domain.interactor - -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.FakeCommunalSceneRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository -import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.kosmos.testScope -import com.android.systemui.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 - -/** - * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled. - */ -@SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var communalRepository: FakeCommunalSceneRepository - private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - - private lateinit var underTest: CommunalInteractor - - @Before - fun setUp() { - communalRepository = kosmos.fakeCommunalSceneRepository - widgetRepository = kosmos.fakeCommunalWidgetRepository - keyguardRepository = kosmos.fakeKeyguardRepository - - mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB) - - underTest = kosmos.communalInteractor - } - - @Test - fun isCommunalEnabled_false() = - testScope.runTest { assertThat(underTest.isCommunalEnabled.value).isFalse() } - - @Test - fun isCommunalAvailable_whenStorageUnlock_false() = - testScope.runTest { - val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable) - - assertThat(isCommunalAvailable).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - runCurrent() - - assertThat(isCommunalAvailable).isFalse() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 8424746f3db5..b65ecf46dcca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -21,7 +21,6 @@ import android.app.admin.DevicePolicyManager import android.app.admin.devicePolicyManager import android.content.Intent import android.content.pm.UserInfo -import android.content.res.mainResources import android.os.UserHandle import android.os.UserManager import android.os.userManager @@ -39,9 +38,8 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository @@ -50,14 +48,9 @@ import com.android.systemui.communal.data.repository.fakeCommunalTutorialReposit import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel -import com.android.systemui.communal.posturing.data.repository.fake -import com.android.systemui.communal.posturing.data.repository.posturingRepository -import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.dock.DockManager -import com.android.systemui.dock.fakeDockManager import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -75,19 +68,16 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.fakeUserTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -98,10 +88,6 @@ import org.mockito.Mockito.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters -/** - * This class of test cases assume that communal is enabled. For disabled cases, see - * [CommunalInteractorCommunalDisabledTest]. - */ @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -109,10 +95,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) - private val kosmos = - testKosmos() - .apply { mainResources = mContext.orCreateTestableResources.resources } - .useUnconfinedTestDispatcher() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val Kosmos.underTest by Kosmos.Fixture { communalInteractor } @@ -128,104 +111,40 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) - - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault, - false, - ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault, - false, - ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault, - false, - ) - } - - @After - fun tearDown() { - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault - ) - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault - ) - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault - ) } @Test fun communalEnabled_true() = kosmos.runTest { - fakeUserRepository.setSelectedUserInfo(mainUser) + communalSettingsInteractor.setSuppressionReasons(emptyList()) assertThat(underTest.isCommunalEnabled.value).isTrue() } @Test - fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() = + fun isCommunalAvailable_whenKeyguardShowing_true() = kosmos.runTest { - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - - assertThat(isAvailable).isTrue() - } + communalSettingsInteractor.setSuppressionReasons(emptyList()) + fakeKeyguardRepository.setKeyguardShowing(false) - @Test - fun isCommunalAvailable_mainUserLockedAndMainUser_false() = - kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) - fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) - - assertThat(isAvailable).isFalse() + assertThat(isAvailable).isTrue() } @Test - fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() = + fun isCommunalAvailable_suppressed() = kosmos.runTest { - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(secondaryUser) + communalSettingsInteractor.setSuppressionReasons(emptyList()) fakeKeyguardRepository.setKeyguardShowing(true) - assertThat(isAvailable).isFalse() - } - - @Test - fun isCommunalAvailable_whenKeyguardShowing_true() = - kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - assertThat(isAvailable).isTrue() - } - - @Test - fun isCommunalAvailable_communalDisabled_false() = - kosmos.runTest { - mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown()) + ) assertThat(isAvailable).isFalse() } @@ -1280,66 +1199,6 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { .inOrder() } - @Test - fun showCommunalWhileCharging() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - mainUser.id, - ) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - batteryRepository.fake.setDevicePluggedIn(false) - assertThat(shouldShowCommunal).isFalse() - - batteryRepository.fake.setDevicePluggedIn(true) - assertThat(shouldShowCommunal).isTrue() - } - - @Test - fun showCommunalWhilePosturedAndCharging() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, - 1, - mainUser.id, - ) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - batteryRepository.fake.setDevicePluggedIn(true) - posturingRepository.fake.setPosturedState(PosturedState.NotPostured) - assertThat(shouldShowCommunal).isFalse() - - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) - assertThat(shouldShowCommunal).isTrue() - } - - @Test - fun showCommunalWhileDocked() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id) - - batteryRepository.fake.setDevicePluggedIn(true) - fakeDockManager.setIsDocked(false) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - assertThat(shouldShowCommunal).isFalse() - - fakeDockManager.setIsDocked(true) - fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) - assertThat(shouldShowCommunal).isTrue() - } - private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 8dc7a331dc2d..b8dbc9f77076 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -32,9 +32,9 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository @@ -50,6 +50,7 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.communal.domain.interactor.communalTutorialInteractor +import com.android.systemui.communal.domain.interactor.setCommunalEnabled import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalMetricsLogger @@ -101,7 +102,6 @@ import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy @@ -128,7 +128,9 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler + @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -212,11 +214,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = testScope.runTest { - // Keyguard showing, storage unlocked, main user, and tutorial not started. keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - setIsMainUser(true) + kosmos.setCommunalEnabled(true) tutorialRepository.setTutorialSettingState( Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED ) @@ -951,21 +950,16 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { fun swipeToCommunal() = kosmos.runTest { setCommunalV2ConfigEnabled(true) - val mainUser = fakeUserRepository.asMainUser() - fakeKeyguardRepository.setKeyguardShowing(true) - fakeUserRepository.setUserUnlocked(mainUser.id, true) - fakeUserTracker.set(userInfos = listOf(mainUser), selectedUserIndex = 0) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - mainUser.id, + // Suppress manual opening + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown(FEATURE_MANUAL_OPEN)) ) val viewModel = createViewModel() val swipeToHubEnabled by collectLastValue(viewModel.swipeToHubEnabled) assertThat(swipeToHubEnabled).isFalse() - batteryRepository.fake.setDevicePluggedIn(true) + communalSettingsInteractor.setSuppressionReasons(emptyList()) assertThat(swipeToHubEnabled).isTrue() keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index c15f797aad5d..df10d058c5d1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.communal.widgets import android.content.pm.UserInfo -import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB @@ -25,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor +import com.android.systemui.communal.domain.interactor.setCommunalEnabled import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.coroutines.collectLastValue @@ -37,11 +37,9 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent @@ -282,22 +280,12 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } - private suspend fun setCommunalAvailable( - available: Boolean, - setKeyguardShowing: Boolean = true, - ) = + private fun setCommunalAvailable(available: Boolean, setKeyguardShowing: Boolean = true) = with(kosmos) { - fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + setCommunalEnabled(available) if (setKeyguardShowing) { fakeKeyguardRepository.setKeyguardShowing(true) } - val settingsValue = if (available) 1 else 0 - fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - settingsValue, - MAIN_USER_INFO.id, - ) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 6c4325adced4..046d92d58978 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -35,7 +35,6 @@ package com.android.systemui.keyguard.domain.interactor import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags @@ -43,8 +42,6 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.setCommunalV2Available @@ -72,7 +69,6 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.runBlocking @@ -433,9 +429,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun testTransitionToGlanceableHub_onWakeUpFromAod() = kosmos.runTest { - val user = setCommunalV2Available(true) - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, user.id) - batteryRepository.fake.setDevicePluggedIn(true) + setCommunalV2Available(true) val currentScene by collectLastValue(communalSceneInteractor.currentScene) fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 9be786fab34d..096c3dafd01c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -193,6 +193,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() = kosmos.runTest { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index e203a276a2f2..1dddfc1bba9c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt @@ -18,11 +18,17 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings import android.view.accessibility.accessibilityManagerWrapper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.logging.uiEventLogger +import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor @@ -39,6 +45,8 @@ import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -46,14 +54,19 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().apply { @@ -61,17 +74,23 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { this.uiEventLogger = mock<UiEventLoggerFake>() } + @get:Rule val setFlagsRule = SetFlagsRule() + private lateinit var underTest: KeyguardTouchHandlingInteractor private val logger = kosmos.uiEventLogger private val testScope = kosmos.testScope private val keyguardRepository = kosmos.fakeKeyguardRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository + + @Mock private lateinit var powerManager: PowerManager @Before fun setUp() { MockitoAnnotations.initMocks(this) overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true) + overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true) whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt())) .thenAnswer { it.arguments[0] } @@ -80,13 +99,13 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { @After fun tearDown() { - mContext - .getOrCreateTestableResources() - .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + val testableResource = mContext.getOrCreateTestableResources() + testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep) } @Test - fun isEnabled() = + fun isLongPressEnabled() = testScope.runTest { val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled) KeyguardState.values().forEach { keyguardState -> @@ -101,7 +120,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() = + fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() = testScope.runTest { val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled) KeyguardState.values().forEach { keyguardState -> @@ -112,7 +131,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = + fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = testScope.runTest { overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false) createUnderTest() @@ -294,6 +313,119 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { assertThat(isMenuVisible).isFalse() } + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + if (keyguardState == KeyguardState.LOCKSCREEN) { + assertThat(isEnabled()).isTrue() + } else { + assertThat(isEnabled()).isFalse() + } + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false) + createUnderTest() + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun onDoubleClick_doubleTapEnabled() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled) + runCurrent() + + underTest.onDoubleClick() + + assertThat(isEnabled).isTrue() + verify(powerManager).goToSleep(anyLong()) + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun onDoubleClick_doubleTapDisabled() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false) + val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled) + runCurrent() + + underTest.onDoubleClick() + + assertThat(isEnabled).isFalse() + verify(powerManager, never()).goToSleep(anyLong()) + } + } + private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) { // This needs to be re-created for each test outside of kosmos since the flag values are // read during initialization to set up flows. Maybe there is a better way to handle that. @@ -309,6 +441,9 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { accessibilityManager = kosmos.accessibilityManagerWrapper, pulsingGestureListener = kosmos.pulsingGestureListener, faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor, + secureSettingsRepository = secureSettingsRepository, + powerManager = powerManager, + systemClock = kosmos.fakeSystemClock, ) setUpState() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 8df70ef0fd2e..7d5e9a5ed178 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -33,7 +33,7 @@ import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepositor import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor -import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2Available import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalScenes @@ -1004,16 +1004,15 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @BrokenWithSceneContainer(339465026) fun occludedToGlanceableHub_communalKtfRefactor() = testScope.runTest { - // GIVEN a device on lockscreen and communal is available - keyguardRepository.setKeyguardShowing(true) - kosmos.setCommunalAvailable(true) - runCurrent() - // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED) keyguardRepository.setKeyguardOccluded(true) runCurrent() + // GIVEN a device on lockscreen and communal is available + kosmos.setCommunalV2Available(true) + runCurrent() + // WHEN occlusion ends keyguardRepository.setKeyguardOccluded(false) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt index b82f5fce9e14..ff2e13b51e14 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt @@ -81,7 +81,7 @@ class SysUIStateDispatcherTest : SysuiTestCase() { private companion object { const val DISPLAY_1 = 1 const val DISPLAY_2 = 2 - const val FLAG_1 = 10L - const val FLAG_2 = 20L + const val FLAG_1 = 1L + const val FLAG_2 = 2L } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt index 09588f9f3751..d1b552906fbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt @@ -35,10 +35,10 @@ class SysUiStateExtTest : SysuiTestCase() { @Test fun updateFlags() { - underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 3L to true) + underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 4L to true) assertThat(underTest.flags and 1L).isNotEqualTo(0L) assertThat(underTest.flags and 2L).isEqualTo(0L) - assertThat(underTest.flags and 3L).isNotEqualTo(0L) + assertThat(underTest.flags and 4L).isNotEqualTo(0L) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt index 68a591dd075f..1d42424bc6ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt @@ -16,11 +16,9 @@ package com.android.systemui.qs.panels.ui.viewmodel - import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository @@ -48,45 +46,43 @@ class DetailsViewModelTest : SysuiTestCase() { } @Test - fun changeTileDetailsViewModel() = with(kosmos) { - testScope.runTest { - val specs = listOf( - spec, - specNoDetails, - ) - tileSpecRepository.setTiles(0, specs) - runCurrent() + fun changeTileDetailsViewModel() = + with(kosmos) { + testScope.runTest { + val specs = listOf(spec, specNoDetails) + tileSpecRepository.setTiles(0, specs) + runCurrent() - val tiles = currentTilesInteractor.currentTiles.value + val tiles = currentTilesInteractor.currentTiles.value - assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2) - assertThat(tiles[1].spec).isEqualTo(specNoDetails) - (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false + assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2) + assertThat(tiles[1].spec).isEqualTo(specNoDetails) + (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false - assertThat(underTest.activeTileDetails).isNull() + assertThat(underTest.activeTileDetails).isNull() - // Click on the tile who has the `spec`. - assertThat(underTest.onTileClicked(spec)).isTrue() - assertThat(underTest.activeTileDetails).isNotNull() - assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet") + // Click on the tile who has the `spec`. + assertThat(underTest.onTileClicked(spec)).isTrue() + assertThat(underTest.activeTileDetails).isNotNull() + assertThat(underTest.activeTileDetails?.title).isEqualTo("internet") - // Click on a tile who dose not have a valid spec. - assertThat(underTest.onTileClicked(null)).isFalse() - assertThat(underTest.activeTileDetails).isNull() + // Click on a tile who dose not have a valid spec. + assertThat(underTest.onTileClicked(null)).isFalse() + assertThat(underTest.activeTileDetails).isNull() - // Click again on the tile who has the `spec`. - assertThat(underTest.onTileClicked(spec)).isTrue() - assertThat(underTest.activeTileDetails).isNotNull() - assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet") + // Click again on the tile who has the `spec`. + assertThat(underTest.onTileClicked(spec)).isTrue() + assertThat(underTest.activeTileDetails).isNotNull() + assertThat(underTest.activeTileDetails?.title).isEqualTo("internet") - // Click on a tile who dose not have a detailed view. - assertThat(underTest.onTileClicked(specNoDetails)).isFalse() - assertThat(underTest.activeTileDetails).isNull() + // Click on a tile who dose not have a detailed view. + assertThat(underTest.onTileClicked(specNoDetails)).isFalse() + assertThat(underTest.activeTileDetails).isNull() - underTest.closeDetailedView() - assertThat(underTest.activeTileDetails).isNull() + underTest.closeDetailedView() + assertThat(underTest.activeTileDetails).isNull() - assertThat(underTest.onTileClicked(null)).isFalse() + assertThat(underTest.onTileClicked(null)).isFalse() + } } - } } 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 c3089761effc..5bde7ad27b7a 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 @@ -691,11 +691,11 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { var currentModel: TileDetailsViewModel? = null val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model } tiles!![0].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("a") + assertThat(currentModel?.title).isEqualTo("a") currentModel = null tiles!![1].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("b") + assertThat(currentModel?.title).isEqualTo("b") currentModel = null tiles!![2].tile.getDetailsViewModel(setCurrentModel) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt new file mode 100644 index 000000000000..e04162bf990a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt @@ -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.systemui.statusbar + +import android.platform.test.flag.junit.FlagsParameterization +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() { + + private lateinit var underTest: NotificationGroupingUtil + + private lateinit var testHelper: NotificationTestHelper + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setup() { + testHelper = NotificationTestHelper(mContext, mDependency) + } + + @Test + fun showsTime() { + val row = testHelper.createRow() + + underTest = NotificationGroupingUtil(row) + assertThat(underTest.showsTime(row)).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index f54c28f4295b..72b003f9f463 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -82,7 +82,6 @@ import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.log.LogWtfHandlerRule; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.UserTracker; @@ -108,7 +107,6 @@ import kotlinx.coroutines.flow.StateFlow; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -207,8 +205,6 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock); private final Executor mMainExecutor = Runnable::run; // Direct executor - @Rule public final LogWtfHandlerRule wtfHandlerRule = new LogWtfHandlerRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java index c9d910c530ea..01046cd10d87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java @@ -20,16 +20,29 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityOptions; import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; +import android.util.Pair; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RemoteViews; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -42,19 +55,37 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.util.kotlin.JavaAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper public class NotificationRemoteInputManagerTest extends SysuiTestCase { + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME); + } + private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = 0; @@ -69,14 +100,34 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private PowerInteractor mPowerInteractor; + @Mock + NotificationRemoteInputManager.RemoteInputListener mRemoteInputListener; + private ActionClickLogger mActionClickLogger; + @Captor + ArgumentCaptor<NotificationRemoteInputManager.ClickHandler> mClickHandlerArgumentCaptor; + private Context mSpyContext; + private NotificationTestHelper mTestHelper; private TestableNotificationRemoteInputManager mRemoteInputManager; private NotificationEntry mEntry; + public NotificationRemoteInputManagerTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mSpyContext = spy(mContext); + doNothing().when(mSpyContext).startIntentSender( + any(), any(), anyInt(), anyInt(), anyInt(), any()); + + + mTestHelper = new NotificationTestHelper(mSpyContext, mDependency); + mActionClickLogger = spy(new ActionClickLogger(logcatLogBuffer())); + mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext, mock(NotifPipelineFlags.class), mLockscreenUserManager, @@ -87,9 +138,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { mRemoteInputUriController, new RemoteInputControllerLogger(logcatLogBuffer()), mClickNotifier, - new ActionClickLogger(logcatLogBuffer()), + mActionClickLogger, mock(JavaAdapter.class), mock(ShadeInteractor.class)); + mRemoteInputManager.setRemoteInputListener(mRemoteInputListener); mEntry = new NotificationEntryBuilder() .setPkg(TEST_PACKAGE_NAME) .setOpPkg(TEST_PACKAGE_NAME) @@ -133,6 +185,70 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry)); } + @Test + public void testActionClick() throws Exception { + RemoteViews.RemoteResponse response = mock(RemoteViews.RemoteResponse.class); + when(response.getLaunchOptions(any())).thenReturn( + Pair.create(mock(Intent.class), mock(ActivityOptions.class))); + ExpandableNotificationRow row = getRowWithReplyAction(); + View actionView = ((LinearLayout) row.getPrivateLayout().getExpandedChild().findViewById( + com.android.internal.R.id.actions)).getChildAt(0); + Notification n = getNotification(row); + CountDownLatch latch = new CountDownLatch(1); + Consumer<NotificationEntry> consumer = notificationEntry -> latch.countDown(); + if (!NotificationBundleUi.isEnabled()) { + mRemoteInputManager.addActionPressListener(consumer); + } + + mRemoteInputManager.getRemoteViewsOnClickHandler().onInteraction( + actionView, + n.actions[0].actionIntent, + response); + + verify(mActionClickLogger).logInitialClick(row.getKey(), 0, n.actions[0].actionIntent); + verify(mClickNotifier).onNotificationActionClick( + eq(row.getKey()), eq(0), eq(n.actions[0]), any(), eq(false)); + verify(mCallback).handleRemoteViewClick(eq(actionView), eq(n.actions[0].actionIntent), + eq(false), eq(0), mClickHandlerArgumentCaptor.capture()); + + mClickHandlerArgumentCaptor.getValue().handleClick(); + verify(mActionClickLogger).logStartingIntentWithDefaultHandler( + row.getKey(), n.actions[0].actionIntent, 0); + + verify(mRemoteInputListener).releaseNotificationIfKeptForRemoteInputHistory(row.getKey()); + if (NotificationBundleUi.isEnabled()) { + verify(row.getEntryAdapter()).onNotificationActionClicked(); + } else { + latch.await(10, TimeUnit.MILLISECONDS); + } + } + + private Notification getNotification(ExpandableNotificationRow row) { + if (NotificationBundleUi.isEnabled()) { + return row.getEntryAdapter().getSbn().getNotification(); + } else { + return row.getEntry().getSbn().getNotification(); + } + } + + private ExpandableNotificationRow getRowWithReplyAction() throws Exception { + PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), + PendingIntent.FLAG_IMMUTABLE); + Notification n = new Notification.Builder(mSpyContext, "") + .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) + .addAction(new Notification.Action(com.android.systemui.res.R.drawable.ic_person, + "reply", pi)) + .build(); + ExpandableNotificationRow row = mTestHelper.createRow(n); + row.onNotificationUpdated(); + row.getPrivateLayout().setExpandedChild(Notification.Builder.recoverBuilder(mSpyContext, n) + .createBigContentView().apply( + mSpyContext, + row.getPrivateLayout(), + mRemoteInputManager.getRemoteViewsOnClickHandler())); + return row; + } + private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager { TestableNotificationRemoteInputManager( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index b085ba4a4dad..485b9febc284 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -19,8 +19,8 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.app.PendingIntent import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.view.View -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable @@ -33,13 +33,16 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos @@ -51,10 +54,16 @@ import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class CallChipViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val chipBackgroundView = mock<ChipBackgroundContainer>() @@ -87,9 +96,10 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = 0) + addOngoingCallState(startTimeMs = 0, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() } @Test @@ -97,9 +107,10 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = -2) + addOngoingCallState(startTimeMs = -2, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() } @Test @@ -107,9 +118,82 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = 345) + addOngoingCallState(startTimeMs = 345, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_zeroStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 0, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_zeroStartTime_isHiddenAsIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 0, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_negativeStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = -2, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_negativeStartTime_isHiddenAsIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = -2, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_positiveStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 345, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_positiveStartTime_isHiddenAsTimer() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 345, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() } @Test @@ -418,5 +502,18 @@ class CallChipViewModelTest : SysuiTestCase() { private const val PROMOTED_BACKGROUND_COLOR = 65 private const val PROMOTED_PRIMARY_TEXT_COLOR = 98 + + @get:Parameters(name = "{0}") + @JvmStatic + val flags: List<FlagsParameterization> + get() = buildList { + addAll( + FlagsParameterization.allCombinationsOf( + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarChipsReturnAnimations.FLAG_NAME, + ) + ) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index e2d1498270c8..e39fa7099953 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -137,7 +137,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -163,7 +163,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -178,7 +178,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - addOngoingCallState(key = notificationKey) + addOngoingCallState(key = notificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -190,7 +190,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { kosmos.runTest { // Start with just the lowest priority chip shown val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) // And everything else hidden mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -225,7 +225,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 670ebadcf5a7..f06244f4f637 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -236,7 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState("call") + addOngoingCallState("call", isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -249,7 +249,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -296,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chipsLegacy_oneChip_notSquished() = kosmos.runTest { - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -323,7 +323,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -385,7 +385,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -431,7 +431,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) // WHEN there's 2 chips - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // THEN they both become squished assertThat(latest!!.primary) @@ -487,7 +487,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoChips_isLandscape_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // WHEN we're in landscape val config = @@ -533,7 +533,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // WHEN we're on a large screen kosmos.displayStateRepository.setIsLargeScreen(true) @@ -627,7 +627,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -642,7 +642,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -681,7 +681,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -697,7 +697,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -913,7 +913,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ), activeNotificationModel( key = "fourthNotif", - statusBarChipIcon = thirdIcon, + statusBarChipIcon = fourthIcon, promotedContent = PromotedNotificationContentModel.Builder("fourthNotif").build(), ), @@ -975,7 +975,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) val callNotificationKey = "call" - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val firstIcon = createStatusBarIconViewOrNull() activeNotificationListRepository.addNotifs( @@ -1053,7 +1053,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) screenRecordState.value = ScreenRecordModel.Recording activeNotificationListRepository.addNotif( activeNotificationModel( @@ -1160,7 +1160,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest, context, notifIcon, "notif") // WHEN the higher priority call chip is added - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) // THEN the higher priority call chip is used assertIsCallChip(latest, callNotificationKey, context) @@ -1191,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val notifIcon = createStatusBarIconViewOrNull() activeNotificationListRepository.addNotif( activeNotificationModel( @@ -1253,7 +1253,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) // WHEN the higher priority call chip is added - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) // THEN the higher priority call chip is used as primary and notif is demoted to // secondary diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt index b6889afa4e8a..faafa073be4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt @@ -29,8 +29,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.mockNotificationActivityStarter import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.entryAdapterFactory +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.testKosmos @@ -355,16 +357,27 @@ class NotificationEntryAdapterTest : SysuiTestCase() { val notification: Notification = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() - val entry = - NotificationEntryBuilder() - .setNotification(notification) - .setImportance(NotificationManager.IMPORTANCE_MIN) - .build() + val entry = NotificationEntryBuilder().setNotification(notification).build() underTest = factory.create(entry) as NotificationEntryAdapter underTest.onNotificationBubbleIconClicked() - verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter()) - ?.onNotificationBubbleIconClicked(entry) + verify(kosmos.mockNotificationActivityStarter).onNotificationBubbleIconClicked(entry) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun onNotificationActionClicked() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .addAction(Mockito.mock(Notification.Action::class.java)) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + underTest.onNotificationActionClicked() + verify(kosmos.mockNotificationActionClickManager).onNotificationActionClicked(entry) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 30983550f0f9..44d88c31c5f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor @@ -138,6 +140,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, + kosmos.mockNotificationActionClickManager, launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, @@ -161,8 +164,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture()) } onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) } - actionPressListener = withArgCaptor { - verify(remoteInputManager).addActionPressListener(capture()) + actionPressListener = if (NotificationBundleUi.isEnabled) { + withArgCaptor { + verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture()) + } + } else { + withArgCaptor { + verify(remoteInputManager).addActionPressListener(capture()) + } } given(headsUpManager.allEntries).willAnswer { huns.stream() } given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation -> @@ -260,7 +269,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { addHUN(entry) actionPressListener.accept(entry) - verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry) + verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry.key) whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true) assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index c5752691da44..65763a359c0f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -45,6 +45,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; +import com.android.systemui.statusbar.notification.collection.BundleEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -273,6 +274,18 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @Test + public void testSilentSectioner_acceptsBundle() { + BundleEntry bundleEntry = new BundleEntry("testBundleKey"); + assertTrue(mSilentSectioner.isInSection(bundleEntry)); + } + + @Test + public void testMinimizedSectioner_rejectsBundle() { + BundleEntry bundleEntry = new BundleEntry("testBundleKey"); + assertFalse(mMinimizedSectioner.isInSection(bundleEntry)); + } + + @Test public void testMinSection() { when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); setRankingAmbient(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 9804932918dc..8560b66d961f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -1071,7 +1071,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse() - underTest.setUserActionMayIndirectlyRemove(notifEntry) + underTest.setUserActionMayIndirectlyRemove(notifEntry.key) assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 6415f8c25a37..e6b2c2541447 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -363,6 +364,7 @@ public class NotificationTestHelper { .setUid(UID) .setInitialPid(2000) .setNotification(summary) + .setUser(USER_HANDLE) .setParent(GroupEntry.ROOT_ENTRY) .build(); GroupEntryBuilder groupEntry = new GroupEntryBuilder() @@ -743,11 +745,12 @@ public class NotificationTestHelper { mock(MetricsLogger.class), mock(PeopleNotificationIdentifier.class), mock(NotificationIconStyleProvider.class), - mock(VisualStabilityCoordinator.class) + mock(VisualStabilityCoordinator.class), + mock(NotificationActionClickManager.class) ).create(entry); row.initialize( - entryAdapter, + spy(entryAdapter), entry, mock(RemoteInputViewSubcomponent.Factory.class), APP_NAME, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index 398f3fbecbd1..f4204af7829b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -82,6 +82,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { statusBarChipIconView = testIconView, contentIntent = testIntent, promotedContent = testPromotedContent, + isAppVisible = false, ) // Verify model is InCall and has the correct icon, intent, and promoted content. @@ -98,7 +99,6 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_setsAllFields_withAppVisible() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) // Set up notification with icon view and intent @@ -113,6 +113,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { statusBarChipIconView = testIconView, contentIntent = testIntent, promotedContent = testPromotedContent, + isAppVisible = true, ) // Verify model is InCall with visible app and has the correct icon, intent, and @@ -141,10 +142,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_appVisibleInitially_emitsInCallWithVisibleApp() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = true) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue() @@ -153,10 +153,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_appNotVisibleInitially_emitsInCall() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false val latest by collectLastValue(underTest.ongoingCallState) - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() @@ -168,8 +167,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.ongoingCallState) // Start with notification and app not visible - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() @@ -245,9 +243,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt index 83dc45c8c511..6d3813c90bfd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt @@ -23,6 +23,10 @@ import androidx.test.filters.SmallTest import com.android.app.viewcapture.ViewCapture import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.SysuiTestCase +import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository +import com.android.systemui.keyevent.data.repository.keyEventRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope @@ -31,6 +35,7 @@ import com.android.systemui.testKosmos import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,6 +60,9 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { @Mock private lateinit var viewCapture: Lazy<ViewCapture> + @Mock + private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory + private val Kosmos.underTest by Kosmos.Fixture { TopLevelWindowEffects( context = mContext, @@ -64,6 +72,8 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { lazyViewCapture = viewCapture, isViewCaptureEnabled = false ), + keyEventInteractor = keyEventInteractor, + viewModelFactory = viewModelFactory, squeezeEffectInteractor = SqueezeEffectInteractor( squeezeEffectRepository = fakeSqueezeEffectRepository ) @@ -93,6 +103,7 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { fun addViewToWindowWhenSqueezeEffectEnabled() = kosmos.runTest { fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true + fakeKeyEventRepository.setPowerButtonDown(true) underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt index 5d8d3f90e13c..9b01fd3242e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.topwindoweffects +package com.android.systemui.topwindoweffects.data.repository import android.os.Handler import android.platform.test.annotations.DisableFlags @@ -30,7 +30,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shared.Flags import com.android.systemui.testKosmos -import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl import com.android.systemui.util.settings.FakeGlobalSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt index fb19a884a210..a94d49c5d40e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.topwindoweffects +package com.android.systemui.topwindoweffects.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -25,7 +25,6 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository -import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt index b52db83d513c..7657a2179d4f 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt @@ -13,7 +13,6 @@ */ package com.android.systemui.plugins.clocks -import android.graphics.RectF import com.android.systemui.plugins.annotations.ProtectedInterface import com.android.systemui.plugins.annotations.SimpleProperty import java.io.PrintWriter @@ -50,5 +49,5 @@ interface ClockController { } interface ClockEventListener { - fun onBoundsChanged(bounds: RectF) + fun onBoundsChanged(bounds: VRectF) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt index 20ee6c120ee8..a658c15a1a99 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt @@ -45,6 +45,7 @@ interface ClockFaceEvents { * render within the centered targetRect to avoid obstructing other elements. The specified * targetRegion is relative to the parent view. */ + @Deprecated("No longer necessary, pending removal") fun onTargetRegionChanged(targetRegion: Rect?) /** Called to notify the clock about its display. */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt index 02a3902a042c..f9ff75d5fdc8 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt @@ -16,7 +16,6 @@ package com.android.systemui.plugins.clocks -import android.graphics.Rect import android.view.View import android.view.View.MeasureSpec import com.android.systemui.log.core.LogLevel @@ -56,12 +55,9 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) { + d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) { bool1 = changed - int1 = left - int2 = top - long1 = right.toLong() - long2 = bottom.toLong() + long1 = VRect(left, top, right, bottom).data.toLong() } } @@ -108,8 +104,11 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } - fun animateDoze() { - d("animateDoze()") + fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { + d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) { + bool1 = isDozing + bool2 = isAnimated + } } fun animateCharge() { @@ -117,10 +116,7 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } fun animateFidget(x: Float, y: Float) { - d({ "animateFidget($str1, $str2)" }) { - str1 = x.toString() - str2 = y.toString() - } + d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() } } companion object { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt index 3dae5305542b..1fb37ec28835 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shared.clocks +package com.android.systemui.plugins.clocks import android.graphics.Point import android.graphics.PointF @@ -30,22 +30,20 @@ private val Y_MASK: ULong = 0x00000000FFFFFFFFU private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt() -private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt() +private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt() private fun pack(x: Int, y: Int): ULong { - return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK) + return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK) } @JvmInline -value class VPointF(private val data: ULong) { +value class VPointF(val data: ULong) { val x: Float get() = Float.fromBits(unpackX(data)) val y: Float get() = Float.fromBits(unpackY(data)) - constructor() : this(0f, 0f) - constructor(pt: PointF) : this(pt.x, pt.y) constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat()) @@ -139,15 +137,13 @@ value class VPointF(private val data: ULong) { } @JvmInline -value class VPoint(private val data: ULong) { +value class VPoint(val data: ULong) { val x: Int get() = unpackX(data) val y: Int get() = unpackY(data) - constructor() : this(0, 0) - constructor(x: Int, y: Int) : this(pack(x, y)) fun toPoint() = Point(x, y) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt new file mode 100644 index 000000000000..3c1adf22a405 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.plugins.clocks + +import android.graphics.Rect +import android.graphics.RectF +import android.util.Half + +private val LEFT_MASK: ULong = 0xFFFF000000000000U +private val TOP_MASK: ULong = 0x0000FFFF00000000U +private val RIGHT_MASK: ULong = 0x00000000FFFF0000U +private val BOTTOM_MASK: ULong = 0x000000000000FFFFU + +private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort() + +private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort() + +private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort() + +private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort() + +private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong { + return ((left.toULong() shl 48) and LEFT_MASK) or + ((top.toULong() shl 32) and TOP_MASK) or + ((right.toULong() shl 16) and RIGHT_MASK) or + ((bottom.toULong() shl 0) and BOTTOM_MASK) +} + +@JvmInline +value class VRectF(val data: ULong) { + val left: Float + get() = fromBits(unpackLeft(data)) + + val top: Float + get() = fromBits(unpackTop(data)) + + val right: Float + get() = fromBits(unpackRight(data)) + + val bottom: Float + get() = fromBits(unpackBottom(data)) + + val width: Float + get() = right - left + + val height: Float + get() = bottom - top + + constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom) + + constructor( + rect: Rect + ) : this( + left = rect.left.toFloat(), + top = rect.top.toFloat(), + right = rect.right.toFloat(), + bottom = rect.bottom.toFloat(), + ) + + constructor( + left: Float, + top: Float, + right: Float, + bottom: Float, + ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom))) + + val center: VPointF + get() = VPointF(left, top) + size / 2f + + val size: VPointF + get() = VPointF(width, height) + + override fun toString() = "($left, $top) -> ($right, $bottom)" + + companion object { + private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value)) + + private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt())) + + fun fromCenter(center: VPointF, size: VPointF): VRectF { + return VRectF( + center.x - size.x / 2, + center.y - size.y / 2, + center.x + size.x / 2, + center.y + size.y / 2, + ) + } + + fun fromTopLeft(pos: VPointF, size: VPointF): VRectF { + return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y) + } + + val ZERO = VRectF(0f, 0f, 0f, 0f) + } +} + +@JvmInline +value class VRect(val data: ULong) { + val left: Int + get() = unpackLeft(data).toInt() + + val top: Int + get() = unpackTop(data).toInt() + + val right: Int + get() = unpackRight(data).toInt() + + val bottom: Int + get() = unpackBottom(data).toInt() + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top + + constructor( + rect: Rect + ) : this( + left = rect.left.toShort(), + top = rect.top.toShort(), + right = rect.right.toShort(), + bottom = rect.bottom.toShort(), + ) + + constructor( + left: Int, + top: Int, + right: Int, + bottom: Int, + ) : this( + left = left.toShort(), + top = top.toShort(), + right = right.toShort(), + bottom = bottom.toShort(), + ) + + constructor( + left: Short, + top: Short, + right: Short, + bottom: Short, + ) : this(pack(left, top, right, bottom)) + + val center: VPoint + get() = VPoint(left, top) + size / 2 + + val size: VPoint + get() = VPoint(width, height) + + override fun toString() = "($left, $top) -> ($right, $bottom)" + + companion object { + val ZERO = VRect(0, 0, 0, 0) + + fun fromCenter(center: VPoint, size: VPoint): VRect { + return VRect( + (center.x - size.x / 2).toShort(), + (center.y - size.y / 2).toShort(), + (center.x + size.x / 2).toShort(), + (center.y + size.y / 2).toShort(), + ) + } + + fun fromTopLeft(pos: VPoint, size: VPoint): VRect { + return VRect( + pos.x.toShort(), + pos.y.toShort(), + (pos.x + size.x).toShort(), + (pos.y + size.y).toShort(), + ) + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt index be0362fd7481..ac7a85742385 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt @@ -16,15 +16,13 @@ package com.android.systemui.plugins.qs -/** - * The base view model class for rendering the Tile's TileDetailsView. - */ -abstract class TileDetailsViewModel { +/** The view model interface for rendering the Tile's TileDetailsView. */ +interface TileDetailsViewModel { // The callback when the settings button is clicked. Currently this is the same as the on tile // long press callback - abstract fun clickOnSettingsButton() + fun clickOnSettingsButton() - abstract fun getTitle(): String + val title: String - abstract fun getSubTitle(): String + val subTitle: String } diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json index b1d6a270bc67..3f03fcff7603 100644 --- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json +++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json @@ -1 +1 @@ -{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file +{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 1549b699eee6..763b1072f968 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Resources -import android.graphics.RectF import android.os.Trace import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_OFF @@ -60,6 +59,7 @@ import com.android.systemui.plugins.clocks.ClockEventListener import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockTickRate +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode @@ -250,7 +250,7 @@ constructor( private var largeClockOnSecondaryDisplay = false val dozeAmount = MutableStateFlow(0f) - val onClockBoundsChanged = MutableStateFlow<RectF?>(null) + val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO) private fun isDarkTheme(): Boolean { val isLightTheme = TypedValue() @@ -315,7 +315,7 @@ constructor( private val clockListener = object : ClockEventListener { - override fun onBoundsChanged(bounds: RectF) { + override fun onBoundsChanged(bounds: VRectF) { onClockBoundsChanged.value = bounds } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index ec97b8a96c1f..b8726101602c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.hardware.input.InputManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -219,6 +220,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; private final BouncerHapticPlayer mBouncerHapticPlayer; private final UserActivityNotifier mUserActivityNotifier; + private final InputManager mInputManager; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -235,7 +237,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> UiEventLogger uiEventLogger, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -254,6 +257,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; mBouncerHapticPlayer = bouncerHapticPlayer; mUserActivityNotifier = userActivityNotifier; + mInputManager = inputManager; } /** Create a new {@link KeyguardInputViewController}. */ @@ -285,22 +289,23 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, mUiEventLogger, mKeyguardKeyboardInteractor, mBouncerHapticPlayer, - mUserActivityNotifier); + mUserActivityNotifier, mInputManager); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier); + mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier, + mInputManager); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier - ); + mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier, + mInputManager); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 0e9d8fec9717..ec9aedfc7551 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -18,12 +18,14 @@ package com.android.keyguard; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; +import static com.android.internal.widget.flags.Flags.hideLastCharWithPhysicalInput; import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; +import android.hardware.input.InputManager; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; @@ -43,11 +45,13 @@ import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView> - extends KeyguardAbsKeyInputViewController<T> { + extends KeyguardAbsKeyInputViewController<T> implements InputManager.InputDeviceListener { private final FalsingCollector mFalsingCollector; private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; protected PasswordTextView mPasswordEntry; + private Boolean mShowAnimations; + private InputManager mInputManager; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -79,7 +83,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, emergencyButtonController, featureFlags, selectedUserInteractor, @@ -87,6 +92,51 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB mFalsingCollector = falsingCollector; mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); + mInputManager = inputManager; + mShowAnimations = null; + } + + private void updateAnimations(Boolean showAnimations) { + if (!hideLastCharWithPhysicalInput()) return; + + if (showAnimations == null) { + showAnimations = !mLockPatternUtils + .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId()); + } + if (mShowAnimations != null && showAnimations.equals(mShowAnimations)) return; + mShowAnimations = showAnimations; + + for (NumPadKey button : mView.getButtons()) { + button.setAnimationEnabled(mShowAnimations); + } + mPasswordEntry.setShowPassword(mShowAnimations); + } + + @Override + public void onInputDeviceAdded(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + // If we were showing animations before maybe the new device is a keyboard. + if (mShowAnimations) { + updateAnimations(null); + } + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + // If we were hiding animations because of a keyboard the keyboard may have been unplugged. + if (!mShowAnimations) { + updateAnimations(null); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + updateAnimations(null); } @Override @@ -95,7 +145,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB boolean showAnimations = !mLockPatternUtils .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId()); - mPasswordEntry.setShowPassword(showAnimations); + if (hideLastCharWithPhysicalInput()) { + mInputManager.registerInputDeviceListener(this, null); + updateAnimations(showAnimations); + } else { + mPasswordEntry.setShowPassword(showAnimations); + } + for (NumPadKey button : mView.getButtons()) { button.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { @@ -103,7 +159,9 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB } return false; }); - button.setAnimationEnabled(showAnimations); + if (!hideLastCharWithPhysicalInput()) { + button.setAnimationEnabled(showAnimations); + } button.setBouncerHapticHelper(mBouncerHapticPlayer); } mPasswordEntry.setOnKeyListener(mOnKeyListener); @@ -191,6 +249,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB protected void onViewDetached() { super.onViewDetached(); + if (hideLastCharWithPhysicalInput()) { + mInputManager.unregisterInputDeviceListener(this); + } + for (NumPadKey button : mView.getButtons()) { button.setOnTouchListener(null); button.setBouncerHapticHelper(null); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 9ae4cc6a4b4f..eefcab38ecd3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -18,6 +18,7 @@ package com.android.keyguard; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; +import android.hardware.input.InputManager; import android.view.View; import com.android.internal.logging.UiEvent; @@ -63,11 +64,13 @@ public class KeyguardPinViewController SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 24f77d77dbe1..a5bb62c04d00 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -29,6 +29,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.hardware.input.InputManager; import android.telephony.PinResult; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -97,11 +98,13 @@ public class KeyguardSimPinViewController SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index e17e8cc05f7e..adede3dc058d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -25,6 +25,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.hardware.input.InputManager; import android.telephony.PinResult; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -95,11 +96,13 @@ public class KeyguardSimPukViewController SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt index 5863a9385234..7d8752ef7222 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt @@ -21,18 +21,14 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel class BluetoothDetailsViewModel( private val onSettingsClick: () -> Unit, val detailsContentViewModel: BluetoothDetailsContentViewModel, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { onSettingsClick() } - override fun getTitle(): String { - // TODO: b/378513956 Update the placeholder text - return "Bluetooth" - } + // TODO: b/378513956 Update the placeholder text + override val title = "Bluetooth" - override fun getSubTitle(): String { - // TODO: b/378513956 Update the placeholder text - return "Tap to connect or disconnect a device" - } + // TODO: b/378513956 Update the placeholder text + override val subTitle = "Tap to connect or disconnect a device" } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt index 42f1b738ec20..6c3535a42a6e 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt @@ -27,17 +27,17 @@ import android.view.ViewConfiguration import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.android.systemui.Flags.doubleTapToSleep import com.android.systemui.log.TouchHandlingViewLogger import com.android.systemui.shade.TouchLogger -import kotlin.math.pow -import kotlin.math.sqrt import kotlinx.coroutines.DisposableHandle /** - * View designed to handle long-presses. + * View designed to handle long-presses and double taps. * - * The view will not handle any long pressed by default. To set it up, set up a listener and, when - * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`. + * The view will not handle any gestures by default. To set it up, set up a listener and, when ready + * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled], + * [setDoublePressHandlingEnabled]) to `true`. */ class TouchHandlingView( context: Context, @@ -62,6 +62,9 @@ class TouchHandlingView( /** Notifies that the gesture was too short for a long press, it is actually a click. */ fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit + + /** Notifies that a double tap has been detected by the given view. */ + fun onDoubleTapDetected(view: View) = Unit } var listener: Listener? = null @@ -70,6 +73,7 @@ class TouchHandlingView( private val interactionHandler: TouchHandlingViewInteractionHandler by lazy { TouchHandlingViewInteractionHandler( + context = context, postDelayed = { block, timeoutMs -> val dispatchToken = Any() @@ -84,6 +88,9 @@ class TouchHandlingView( onSingleTapDetected = { x, y -> listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y) }, + onDoubleTapDetected = { + if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView) + }, longPressDuration = longPressDuration, allowedTouchSlop = allowedTouchSlop, logger = logger, @@ -100,13 +107,17 @@ class TouchHandlingView( interactionHandler.isLongPressHandlingEnabled = isEnabled } + fun setDoublePressHandlingEnabled(isEnabled: Boolean) { + interactionHandler.isDoubleTapHandlingEnabled = isEnabled + } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event)) } @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent?): Boolean { - return interactionHandler.onTouchEvent(event?.toModel()) + override fun onTouchEvent(event: MotionEvent): Boolean { + return interactionHandler.onTouchEvent(event) } private fun setupAccessibilityDelegate() { @@ -154,33 +165,3 @@ class TouchHandlingView( } } } - -private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel { - return when (actionMasked) { - MotionEvent.ACTION_DOWN -> - TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt()) - MotionEvent.ACTION_MOVE -> - TouchHandlingViewInteractionHandler.MotionEventModel.Move( - distanceMoved = distanceMoved() - ) - MotionEvent.ACTION_UP -> - TouchHandlingViewInteractionHandler.MotionEventModel.Up( - distanceMoved = distanceMoved(), - gestureDuration = gestureDuration(), - ) - MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel - else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other - } -} - -private fun MotionEvent.distanceMoved(): Float { - return if (historySize > 0) { - sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2)) - } else { - 0f - } -} - -private fun MotionEvent.gestureDuration(): Long { - return eventTime - downTime -} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt index 5863fc644c8e..fe509d74edc0 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt @@ -17,12 +17,20 @@ package com.android.systemui.common.ui.view +import android.content.Context import android.graphics.Point +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ViewConfiguration import com.android.systemui.log.TouchHandlingViewLogger +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.properties.Delegates import kotlinx.coroutines.DisposableHandle /** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */ class TouchHandlingViewInteractionHandler( + context: Context, /** * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle] * allowing the delayed runnable to be canceled before it is run. @@ -34,6 +42,8 @@ class TouchHandlingViewInteractionHandler( private val onLongPressDetected: (x: Int, y: Int) -> Unit, /** Callback reporting the a single tap gesture was detected at the given coordinates. */ private val onSingleTapDetected: (x: Int, y: Int) -> Unit, + /** Callback reporting that a double tap gesture was detected. */ + private val onDoubleTapDetected: () -> Unit, /** Time for the touch to be considered a long-press in ms */ var longPressDuration: () -> Long, /** @@ -58,48 +68,98 @@ class TouchHandlingViewInteractionHandler( } var isLongPressHandlingEnabled: Boolean = false + var isDoubleTapHandlingEnabled: Boolean = false var scheduledLongPressHandle: DisposableHandle? = null + private var doubleTapAwaitingUp: Boolean = false + private var lastDoubleTapDownEventTime: Long? = null + /** Record coordinate for last DOWN event for single tap */ val lastEventDownCoordinate = Point(-1, -1) - fun onTouchEvent(event: MotionEventModel?): Boolean { - if (!isLongPressHandlingEnabled) { - return false - } - return when (event) { - is MotionEventModel.Down -> { - scheduleLongPress(event.x, event.y) - lastEventDownCoordinate.x = event.x - lastEventDownCoordinate.y = event.y - true + private val gestureDetector = + GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(event: MotionEvent): Boolean { + if (isDoubleTapHandlingEnabled) { + doubleTapAwaitingUp = true + lastDoubleTapDownEventTime = event.eventTime + return true + } + return false + } + }, + ) + + fun onTouchEvent(event: MotionEvent): Boolean { + if (isDoubleTapHandlingEnabled) { + gestureDetector.onTouchEvent(event) + if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) { + lastDoubleTapDownEventTime?.let { time -> + if ( + event.eventTime - time < ViewConfiguration.getDoubleTapTimeout() + ) { + cancelScheduledLongPress() + onDoubleTapDetected() + } + } + doubleTapAwaitingUp = false + } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) { + doubleTapAwaitingUp = false } - is MotionEventModel.Move -> { - if (event.distanceMoved > allowedTouchSlop) { - logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop) + } + + if (isLongPressHandlingEnabled) { + val motionEventModel = event.toModel() + + return when (motionEventModel) { + is MotionEventModel.Down -> { + scheduleLongPress(motionEventModel.x, motionEventModel.y) + lastEventDownCoordinate.x = motionEventModel.x + lastEventDownCoordinate.y = motionEventModel.y + true + } + + is MotionEventModel.Move -> { + if (motionEventModel.distanceMoved > allowedTouchSlop) { + logger?.cancelingLongPressDueToTouchSlop( + motionEventModel.distanceMoved, + allowedTouchSlop, + ) + cancelScheduledLongPress() + } + false + } + + is MotionEventModel.Up -> { + logger?.onUpEvent( + motionEventModel.distanceMoved, + allowedTouchSlop, + motionEventModel.gestureDuration, + ) cancelScheduledLongPress() + if ( + motionEventModel.distanceMoved <= allowedTouchSlop && + motionEventModel.gestureDuration < longPressDuration() + ) { + logger?.dispatchingSingleTap() + dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y) + } + false } - false - } - is MotionEventModel.Up -> { - logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration) - cancelScheduledLongPress() - if ( - event.distanceMoved <= allowedTouchSlop && - event.gestureDuration < longPressDuration() - ) { - logger?.dispatchingSingleTap() - dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y) + + is MotionEventModel.Cancel -> { + logger?.motionEventCancelled() + cancelScheduledLongPress() + false } - false - } - is MotionEventModel.Cancel -> { - logger?.motionEventCancelled() - cancelScheduledLongPress() - false + + else -> false } - else -> false } + + return false } private fun scheduleLongPress(x: Int, y: Int) { @@ -134,4 +194,30 @@ class TouchHandlingViewInteractionHandler( onSingleTapDetected(x, y) } + + private fun MotionEvent.toModel(): MotionEventModel { + return when (actionMasked) { + MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt()) + MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved()) + MotionEvent.ACTION_UP -> + MotionEventModel.Up( + distanceMoved = distanceMoved(), + gestureDuration = gestureDuration(), + ) + MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel + else -> MotionEventModel.Other + } + } + + private fun MotionEvent.distanceMoved(): Float { + return if (historySize > 0) { + sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2)) + } else { + 0f + } + } + + private fun MotionEvent.gestureDuration(): Long { + return eventTime - downTime + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt new file mode 100644 index 000000000000..6a611ec5b647 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@SysUISingleton +class CommunalSuppressionStartable +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val suppressionFlows: Set<@JvmSuppressWildcards Flow<SuppressionReason?>>, + private val communalSettingsInteractor: CommunalSettingsInteractor, + @CommunalTableLog private val tableLogBuffer: TableLogBuffer, +) : CoreStartable { + override fun start() { + getSuppressionReasons() + .onEach { reasons -> communalSettingsInteractor.setSuppressionReasons(reasons) } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "suppressionReasons", + initialValue = emptyList(), + ) + .flowOn(bgDispatcher) + .launchIn(applicationScope) + } + + private fun getSuppressionReasons(): Flow<List<SuppressionReason>> { + if (!communalSettingsInteractor.isCommunalFlagEnabled()) { + return flowOf(listOf(SuppressionReason.ReasonFlagDisabled)) + } + return combine(suppressionFlows) { reasons -> reasons.filterNotNull() } + } +} 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 bb3be531aa8a..a31c0bd35453 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -29,6 +29,7 @@ import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositor import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor +import com.android.systemui.communal.domain.suppression.dagger.CommunalSuppressionModule import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl import com.android.systemui.communal.shared.model.CommunalScenes @@ -70,6 +71,7 @@ import kotlinx.coroutines.CoroutineScope CommunalSmartspaceRepositoryModule::class, CommunalStartableModule::class, GlanceableHubWidgetManagerModule::class, + CommunalSuppressionModule::class, ] ) interface CommunalModule { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt index 7358aa7b3fcd..a4f75e81b6ae 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt @@ -22,6 +22,7 @@ import com.android.systemui.communal.CommunalDreamStartable import com.android.systemui.communal.CommunalMetricsStartable import com.android.systemui.communal.CommunalOngoingContentStartable import com.android.systemui.communal.CommunalSceneStartable +import com.android.systemui.communal.CommunalSuppressionStartable import com.android.systemui.communal.DevicePosturingListener import com.android.systemui.communal.log.CommunalLoggerStartable import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable @@ -73,4 +74,9 @@ interface CommunalStartableModule { @IntoMap @ClassKey(DevicePosturingListener::class) fun bindDevicePosturingistener(impl: DevicePosturingListener): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalSuppressionStartable::class) + fun bindCommunalSuppressionStartable(impl: CommunalSuppressionStartable): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt deleted file mode 100644 index 83a5bdb14ebd..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.communal.data.model - -import com.android.systemui.log.table.Diffable -import com.android.systemui.log.table.TableRowLogger -import java.util.EnumSet - -/** Reasons that communal is disabled, primarily for logging. */ -enum class DisabledReason(val loggingString: String) { - /** Communal should be disabled due to invalid current user */ - DISABLED_REASON_INVALID_USER("invalidUser"), - /** Communal should be disabled due to the flag being off */ - DISABLED_REASON_FLAG("flag"), - /** Communal should be disabled because the user has turned off the setting */ - DISABLED_REASON_USER_SETTING("userSetting"), - /** Communal is disabled by the device policy app */ - DISABLED_REASON_DEVICE_POLICY("devicePolicy"), -} - -/** - * Model representing the reasons communal hub should be disabled. Allows logging reasons separately - * for debugging. - */ -@JvmInline -value class CommunalEnabledState( - private val disabledReasons: EnumSet<DisabledReason> = - EnumSet.noneOf(DisabledReason::class.java) -) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons { - - /** Creates [CommunalEnabledState] with a single reason for being disabled */ - constructor(reason: DisabledReason) : this(EnumSet.of(reason)) - - /** Checks if there are any reasons communal should be disabled. If none, returns true. */ - val enabled: Boolean - get() = isEmpty() - - override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) { - for (reason in DisabledReason.entries) { - val newVal = contains(reason) - if (newVal != prevVal.contains(reason)) { - row.logChange( - columnName = reason.loggingString, - value = newVal, - ) - } - } - } - - override fun logFull(row: TableRowLogger) { - for (reason in DisabledReason.entries) { - row.logChange(columnName = reason.loggingString, value = contains(reason)) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt new file mode 100644 index 000000000000..5fb1c4e84eef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.model + +import android.annotation.IntDef + +@Retention(AnnotationRetention.SOURCE) +@IntDef( + flag = true, + prefix = ["FEATURE_"], + value = [FEATURE_AUTO_OPEN, FEATURE_MANUAL_OPEN, FEATURE_ENABLED, FEATURE_ALL], +) +annotation class CommunalFeature + +/** If we should automatically open the hub */ +const val FEATURE_AUTO_OPEN: Int = 1 + +/** If the user is allowed to manually open the hub */ +const val FEATURE_MANUAL_OPEN: Int = 1 shl 1 + +/** + * If the hub should be considered enabled. If not, it may be cleaned up entirely to reduce memory + * footprint. + */ +const val FEATURE_ENABLED: Int = 1 shl 2 + +const val FEATURE_ALL: Int = FEATURE_ENABLED or FEATURE_MANUAL_OPEN or FEATURE_AUTO_OPEN diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt new file mode 100644 index 000000000000..de05bed7ef57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt @@ -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.systemui.communal.data.model + +sealed interface SuppressionReason { + @CommunalFeature val suppressedFeatures: Int + + /** Whether this reason suppresses a particular feature. */ + fun isSuppressed(@CommunalFeature feature: Int): Boolean { + return (suppressedFeatures and feature) != 0 + } + + /** Suppress hub automatically opening due to Android Auto projection */ + data object ReasonCarProjection : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_AUTO_OPEN + } + + /** Suppress hub due to the "When to dream" conditions not being met */ + data class ReasonWhenToAutoShow(override val suppressedFeatures: Int) : SuppressionReason + + /** Suppress hub due to device policy */ + data object ReasonDevicePolicy : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the user disabling the setting */ + data object ReasonSettingDisabled : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the user being locked */ + data object ReasonUserLocked : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due the a secondary user being active */ + data object ReasonSecondaryUser : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the flag being disabled */ + data object ReasonFlagDisabled : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to an unknown reason, used as initial state and in tests */ + data class ReasonUnknown(override val suppressedFeatures: Int = FEATURE_ALL) : + SuppressionReason +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt new file mode 100644 index 000000000000..4fe641a78d4b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.UiModeManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +interface CarProjectionRepository { + /** Whether car projection is active. */ + val projectionActive: Flow<Boolean> + + /** + * Checks the system for the current car projection state. + * + * @return True if projection is active, false otherwise. + */ + suspend fun isProjectionActive(): Boolean +} + +@SysUISingleton +class CarProjectionRepositoryImpl +@Inject +constructor( + private val uiModeManager: UiModeManager, + @Background private val bgDispatcher: CoroutineDispatcher, +) : CarProjectionRepository { + override val projectionActive: Flow<Boolean> = + conflatedCallbackFlow { + val listener = + UiModeManager.OnProjectionStateChangedListener { _, _ -> trySend(Unit) } + uiModeManager.addOnProjectionStateChangedListener( + UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, + bgDispatcher.asExecutor(), + listener, + ) + awaitClose { uiModeManager.removeOnProjectionStateChangedListener(listener) } + } + .emitOnStart() + .map { isProjectionActive() } + .flowOn(bgDispatcher) + + override suspend fun isProjectionActive(): Boolean = + withContext(bgDispatcher) { + (uiModeManager.activeProjectionTypes and UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt index 7f137f3b976b..0d590db97860 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt @@ -22,4 +22,6 @@ import dagger.Module @Module interface CommunalRepositoryModule { @Binds fun communalRepository(impl: CommunalSceneRepositoryImpl): CommunalSceneRepository + + @Binds fun carProjectionRepository(impl: CarProjectionRepositoryImpl): CarProjectionRepository } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 4c291a0c5a2e..6f688d172843 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -26,12 +26,9 @@ import android.provider.Settings import com.android.systemui.Flags.communalHub import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.communal.data.model.CommunalEnabledState -import com.android.systemui.communal.data.model.DisabledReason -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING +import com.android.systemui.communal.data.model.CommunalFeature +import com.android.systemui.communal.data.model.FEATURE_ALL +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule.Companion.DEFAULT_BACKGROUND_TYPE import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream @@ -43,22 +40,23 @@ import com.android.systemui.flags.Flags import com.android.systemui.util.kotlin.emitOnStart import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow -import java.util.EnumSet import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart interface CommunalSettingsRepository { - /** A [CommunalEnabledState] for the specified user. */ - fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> + /** Whether a particular feature is enabled */ + fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> - fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> + /** + * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be + * suppressed. + */ + fun setSuppressionReasons(reasons: List<SuppressionReason>) /** * Returns a [WhenToDream] for the specified user, indicating what state the device should be in @@ -66,6 +64,9 @@ interface CommunalSettingsRepository { */ fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> + /** Returns whether glanceable hub is enabled by the current user. */ + fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * @@ -123,6 +124,19 @@ constructor( resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault) } + private val _suppressionReasons = + MutableStateFlow<List<SuppressionReason>>( + // Suppress hub by default until we get an initial update. + listOf(SuppressionReason.ReasonUnknown(FEATURE_ALL)) + ) + + override fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> = + _suppressionReasons.map { reasons -> reasons.none { it.isSuppressed(feature) } } + + override fun setSuppressionReasons(reasons: List<SuppressionReason>) { + _suppressionReasons.value = reasons + } + override fun getFlagEnabled(): Boolean { return if (getV2FlagEnabled()) { true @@ -138,44 +152,6 @@ constructor( glanceableHubV2() } - override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> { - if (!user.isMain) { - return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER)) - } - if (!getFlagEnabled()) { - return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG)) - } - return combine( - getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING), - getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY), - ) { reasons -> - reasons.filterNotNull() - } - .map { reasons -> - if (reasons.isEmpty()) { - EnumSet.noneOf(DisabledReason::class.java) - } else { - EnumSet.copyOf(reasons) - } - } - .map { reasons -> CommunalEnabledState(reasons) } - .flowOn(bgDispatcher) - } - - override fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> = - secureSettings - .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.SCREENSAVER_ENABLED)) - // Force an update - .onStart { emit(Unit) } - .map { - secureSettings.getIntForUser( - Settings.Secure.SCREENSAVER_ENABLED, - SCREENSAVER_ENABLED_SETTING_DEFAULT, - user.id, - ) == 1 - } - .flowOn(bgDispatcher) - override fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> = secureSettings .observerFlow( @@ -247,11 +223,11 @@ constructor( ?: defaultBackgroundType } - private fun getEnabledByUser(user: UserInfo): Flow<Boolean> = + override fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> = secureSettings .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.GLANCEABLE_HUB_ENABLED)) // Force an update - .onStart { emit(Unit) } + .emitOnStart() .map { secureSettings.getIntForUser( Settings.Secure.GLANCEABLE_HUB_ENABLED, @@ -259,17 +235,13 @@ constructor( user.id, ) == 1 } + .flowOn(bgDispatcher) companion object { const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background" private const val ENABLED_SETTING_DEFAULT = 1 - private const val SCREENSAVER_ENABLED_SETTING_DEFAULT = 0 } } private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0 - -private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled -> - if (enabled) null else reason -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt new file mode 100644 index 000000000000..17b61e1c6fdf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.repository.CarProjectionRepository +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class CarProjectionInteractor @Inject constructor(repository: CarProjectionRepository) { + /** Whether car projection is active. */ + val projectionActive = repository.projectionActive +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt new file mode 100644 index 000000000000..51df3338a18e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt @@ -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.systemui.communal.domain.interactor + +import com.android.systemui.common.domain.interactor.BatteryInteractor +import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor +import com.android.systemui.communal.shared.model.WhenToDream +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.retrieveIsDocked +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated +import javax.inject.Inject +import javax.inject.Named +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@SysUISingleton +class CommunalAutoOpenInteractor +@Inject +constructor( + communalSettingsInteractor: CommunalSettingsInteractor, + @Background private val backgroundContext: CoroutineContext, + private val batteryInteractor: BatteryInteractor, + private val posturingInteractor: PosturingInteractor, + private val dockManager: DockManager, + @Named(SWIPE_TO_HUB) private val allowSwipeAlways: Boolean, +) { + val shouldAutoOpen: Flow<Boolean> = + communalSettingsInteractor.whenToDream + .flatMapLatestConflated { whenToDream -> + when (whenToDream) { + WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn + WhenToDream.WHILE_DOCKED -> { + allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked()) + } + WhenToDream.WHILE_POSTURED -> { + allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured) + } + WhenToDream.NEVER -> flowOf(false) + } + } + .flowOn(backgroundContext) + + val suppressionReason: Flow<SuppressionReason?> = + shouldAutoOpen.map { conditionMet -> + if (conditionMet) { + null + } else { + var suppressedFeatures = FEATURE_AUTO_OPEN + if (!allowSwipeAlways) { + suppressedFeatures = suppressedFeatures or FEATURE_MANUAL_OPEN + } + SuppressionReason.ReasonWhenToAutoShow(suppressedFeatures) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 564628d3f52f..684c52ad45f3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -30,13 +30,11 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.Flags.communalResponsiveGrid import com.android.systemui.Flags.glanceableHubBlurredBackground import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.data.repository.CommunalMediaRepository import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent -import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL @@ -45,14 +43,11 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize. import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dock.DockManager -import com.android.systemui.dock.retrieveIsDocked import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -69,11 +64,8 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.ManagedProfileController -import com.android.systemui.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf -import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart -import com.android.systemui.util.kotlin.isDevicePluggedIn import javax.inject.Inject import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher @@ -125,10 +117,6 @@ constructor( @CommunalLog logBuffer: LogBuffer, @CommunalTableLog tableLogBuffer: TableLogBuffer, private val managedProfileController: ManagedProfileController, - private val batteryInteractor: BatteryInteractor, - private val dockManager: DockManager, - private val posturingInteractor: PosturingInteractor, - private val userLockedInteractor: UserLockedInteractor, ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -162,11 +150,7 @@ constructor( /** Whether communal features are enabled and available. */ val isCommunalAvailable: Flow<Boolean> = - allOf( - communalSettingsInteractor.isCommunalEnabled, - userLockedInteractor.isUserUnlocked(userManager.mainUser), - keyguardInteractor.isKeyguardShowing, - ) + allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing) .distinctUntilChanged() .onEach { available -> logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { @@ -184,37 +168,6 @@ constructor( replay = 1, ) - /** - * Whether communal hub should be shown automatically, depending on the user's [WhenToDream] - * state. - */ - val shouldShowCommunal: StateFlow<Boolean> = - allOf( - isCommunalAvailable, - communalSettingsInteractor.whenToDream - .flatMapLatest { whenToDream -> - when (whenToDream) { - WhenToDream.NEVER -> flowOf(false) - - WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn - - WhenToDream.WHILE_DOCKED -> - allOf( - batteryInteractor.isDevicePluggedIn, - dockManager.retrieveIsDocked(), - ) - - WhenToDream.WHILE_POSTURED -> - allOf( - batteryInteractor.isDevicePluggedIn, - posturingInteractor.postured, - ) - } - } - .flowOn(bgDispatcher), - ) - .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) - private val _isDisclaimerDismissed = MutableStateFlow(false) val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index a0b1261df346..ae89b39175c1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -18,17 +18,18 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.communal.data.model.CommunalEnabledState +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_ENABLED +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.CommunalSettingsRepository import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.log.dagger.CommunalTableLog -import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.settings.UserTracker import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -53,33 +54,43 @@ constructor( private val repository: CommunalSettingsRepository, userInteractor: SelectedUserInteractor, private val userTracker: UserTracker, - @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { - /** Whether or not communal is enabled for the currently selected user. */ + /** Whether communal is enabled at all. */ val isCommunalEnabled: StateFlow<Boolean> = - userInteractor.selectedUserInfo - .flatMapLatest { user -> repository.getEnabledState(user) } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnPrefix = "disabledReason", - initialValue = CommunalEnabledState(), - ) - .map { model -> model.enabled } - // Start this eagerly since the value is accessed synchronously in many places. + repository + .isEnabled(FEATURE_ENABLED) .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) - /** Whether or not screensaver (dreams) is enabled for the currently selected user. */ - val isScreensaverEnabled: Flow<Boolean> = - userInteractor.selectedUserInfo.flatMapLatest { user -> - repository.getScreensaverEnabledState(user) - } + /** Whether manually opening the hub is enabled */ + val manualOpenEnabled: StateFlow<Boolean> = + repository + .isEnabled(FEATURE_MANUAL_OPEN) + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) + + /** Whether auto-opening the hub is enabled */ + val autoOpenEnabled: StateFlow<Boolean> = + repository + .isEnabled(FEATURE_AUTO_OPEN) + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) /** When to dream for the currently selected user. */ val whenToDream: Flow<WhenToDream> = - userInteractor.selectedUserInfo.flatMapLatest { user -> + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> repository.getWhenToDreamState(user) } + /** Whether communal hub is allowed by device policy for the current user */ + val allowedForCurrentUserByDevicePolicy: Flow<Boolean> = + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> + repository.getAllowedByDevicePolicy(user) + } + + /** Whether the hub is enabled for the current user */ + val settingEnabledForCurrentUser: Flow<Boolean> = + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> + repository.getSettingEnabledByUser(user) + } + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * @@ -109,6 +120,14 @@ constructor( */ fun isV2FlagEnabled(): Boolean = repository.getV2FlagEnabled() + /** + * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be + * suppressed. + */ + fun setSuppressionReasons(reasons: List<SuppressionReason>) { + repository.setSuppressionReasons(reasons) + } + /** The type of background to use for the hub. Used to experiment with different backgrounds */ val communalBackground: Flow<CommunalBackgroundType> = userInteractor.selectedUserInfo diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt new file mode 100644 index 000000000000..a10e90f09cc2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.suppression + +import com.android.systemui.communal.data.model.SuppressionReason +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +fun Flow<Boolean>.mapToReasonIfNotAllowed(reason: SuppressionReason): Flow<SuppressionReason?> = + this.map { allowed -> if (allowed) null else reason } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt new file mode 100644 index 000000000000..c62d77eee287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.suppression.dagger + +import com.android.systemui.Flags.glanceableHubV2 +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.domain.interactor.CarProjectionInteractor +import com.android.systemui.communal.domain.interactor.CommunalAutoOpenInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.domain.suppression.mapToReasonIfNotAllowed +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.user.domain.interactor.UserLockedInteractor +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import dagger.multibindings.Multibinds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@Module +interface CommunalSuppressionModule { + /** + * A set of reasons why communal may be suppressed. Ensures that this can be injected even if + * it's empty. + */ + @Multibinds fun suppressorSet(): Set<Flow<SuppressionReason?>> + + companion object { + @Provides + @IntoSet + fun provideCarProjectionSuppressor( + interactor: CarProjectionInteractor + ): Flow<SuppressionReason?> { + if (!glanceableHubV2()) { + return flowOf(null) + } + return not(interactor.projectionActive) + .mapToReasonIfNotAllowed(SuppressionReason.ReasonCarProjection) + } + + @Provides + @IntoSet + fun provideDevicePolicySuppressor( + interactor: CommunalSettingsInteractor + ): Flow<SuppressionReason?> { + return interactor.allowedForCurrentUserByDevicePolicy.mapToReasonIfNotAllowed( + SuppressionReason.ReasonDevicePolicy + ) + } + + @Provides + @IntoSet + fun provideSettingDisabledSuppressor( + interactor: CommunalSettingsInteractor + ): Flow<SuppressionReason?> { + return interactor.settingEnabledForCurrentUser.mapToReasonIfNotAllowed( + SuppressionReason.ReasonSettingDisabled + ) + } + + @Provides + @IntoSet + fun bindUserLockedSuppressor(interactor: UserLockedInteractor): Flow<SuppressionReason?> { + return interactor.currentUserUnlocked.mapToReasonIfNotAllowed( + SuppressionReason.ReasonUserLocked + ) + } + + @Provides + @IntoSet + fun provideAutoOpenSuppressor( + interactor: CommunalAutoOpenInteractor + ): Flow<SuppressionReason?> { + return interactor.suppressionReason + } + + @Provides + @IntoSet + fun provideMainUserSuppressor( + interactor: SelectedUserInteractor + ): Flow<SuppressionReason?> { + return interactor.selectedUserInfo + .map { it.isMain } + .mapToReasonIfNotAllowed(SuppressionReason.ReasonSecondaryUser) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 62a98d7a48ea..857fa5cac3e2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -369,12 +369,10 @@ constructor( val swipeToHubEnabled: Flow<Boolean> by lazy { val inAllowedDeviceState = - if (swipeToHub) { - MutableStateFlow(true) - } else if (v2FlagEnabled()) { - communalInteractor.shouldShowCommunal + if (v2FlagEnabled()) { + communalSettingsInteractor.manualOpenEnabled } else { - MutableStateFlow(false) + MutableStateFlow(swipeToHub) } if (v2FlagEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt index a7c078f235b4..36b75c6fc6b8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt @@ -61,7 +61,7 @@ constructor( fun startTransitionFromDream() { val showGlanceableHub = if (communalSettingsInteractor.isV2FlagEnabled()) { - communalInteractor.shouldShowCommunal.value + communalSettingsInteractor.autoOpenEnabled.value } else { communalInteractor.isCommunalEnabled.value && !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index ef06a85bd0d9..54af8f5b9806 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -20,7 +20,6 @@ import android.animation.ValueAnimator import android.util.Log import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -60,7 +59,6 @@ constructor( private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSceneInteractor: CommunalSceneInteractor, - private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.AOD, @@ -110,7 +108,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value - val shouldShowCommunal = communalInteractor.shouldShowCommunal.value + val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value if (!maybeHandleInsecurePowerGesture()) { val shouldTransitionToLockscreen = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 6f5f662d6fa3..1fc41085f772 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -153,7 +153,7 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( communalInteractor.isCommunalAvailable, - communalInteractor.shouldShowCommunal, + communalSettingsInteractor.autoOpenEnabled, ) .collect { (_, isCommunalAvailable, shouldShowCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value @@ -209,7 +209,7 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( - communalInteractor.shouldShowCommunal, + communalSettingsInteractor.autoOpenEnabled, communalInteractor.isCommunalAvailable, keyguardInteractor.biometricUnlockState, wakeToGoneInteractor.canWakeDirectlyToGone, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 0fb98ffa4a30..3b1b6fcc45f2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -115,7 +115,7 @@ constructor( powerInteractor.isAwake .debounce(50L) .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample(communalInteractor.shouldShowCommunal) + .sample(communalSettingsInteractor.autoOpenEnabled) .collect { shouldShowCommunal -> if (shouldShowCommunal) { // This case handles tapping the power button to transition through diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index a01dc02bbd9f..f8c7a86687dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -20,7 +20,6 @@ import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -69,7 +68,6 @@ constructor( private val shadeRepository: ShadeRepository, powerInteractor: PowerInteractor, private val communalSettingsInteractor: CommunalSettingsInteractor, - private val communalInteractor: CommunalInteractor, private val communalSceneInteractor: CommunalSceneInteractor, private val swipeToDismissInteractor: SwipeToDismissInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -355,7 +353,7 @@ constructor( private fun listenForLockscreenToGlanceableHubV2() { scope.launch { - communalInteractor.shouldShowCommunal + communalSettingsInteractor.autoOpenEnabled .filterRelevantKeyguardStateAnd { shouldShow -> shouldShow } .collect { communalSceneInteractor.changeScene( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 705eaa22aa9a..55534c4f1444 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -20,11 +20,14 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.PowerManager +import android.provider.Settings import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.systemui.Flags.doubleTapToSleep import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -36,10 +39,13 @@ import com.android.systemui.res.R import com.android.systemui.shade.PulsingGestureListener import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -66,10 +72,13 @@ constructor( private val accessibilityManager: AccessibilityManagerWrapper, private val pulsingGestureListener: PulsingGestureListener, private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val secureSettingsRepository: UserAwareSecureSettingsRepository, + private val powerManager: PowerManager, + private val systemClock: SystemClock, ) { /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: StateFlow<Boolean> = - if (isFeatureEnabled()) { + if (isLongPressFeatureEnabled()) { combine( transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), repository.isQuickSettingsVisible, @@ -85,6 +94,30 @@ constructor( initialValue = false, ) + /** Whether the double tap handling handling feature should be enabled. */ + val isDoubleTapHandlingEnabled: StateFlow<Boolean> = + if (isDoubleTapFeatureEnabled()) { + combine( + transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN), + repository.isQuickSettingsVisible, + isDoubleTapSettingEnabled(), + ) { + isFullyTransitionedToLockScreen, + isQuickSettingsVisible, + isDoubleTapSettingEnabled -> + isFullyTransitionedToLockScreen == 1f && + !isQuickSettingsVisible && + isDoubleTapSettingEnabled + } + } else { + flowOf(false) + } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + private val _isMenuVisible = MutableStateFlow(false) /** Model for whether the menu should be shown. */ val isMenuVisible: StateFlow<Boolean> = @@ -116,7 +149,7 @@ constructor( private var delayedHideMenuJob: Job? = null init { - if (isFeatureEnabled()) { + if (isLongPressFeatureEnabled()) { broadcastDispatcher .broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) .onEach { hideMenu() } @@ -175,17 +208,30 @@ constructor( /** Notifies that the lockscreen has been double clicked. */ fun onDoubleClick() { - pulsingGestureListener.onDoubleTapEvent() + if (isDoubleTapHandlingEnabled.value) { + powerManager.goToSleep(systemClock.uptimeMillis()) + } else { + pulsingGestureListener.onDoubleTapEvent() + } + } + + private fun isDoubleTapSettingEnabled(): Flow<Boolean> { + return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP) } private fun showSettings() { _shouldOpenSettings.value = true } - private fun isFeatureEnabled(): Boolean { + private fun isLongPressFeatureEnabled(): Boolean { return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) } + private fun isDoubleTapFeatureEnabled(): Boolean { + return doubleTapToSleep() && + context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep) + } + /** Updates application state to ask to show the menu. */ private fun showMenu() { _isMenuVisible.value = true diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 17e14c3e83da..70a827d5e45b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -48,7 +48,7 @@ object DeviceEntryIconViewBinder { /** * Updates UI for: * - device entry containing view (parent view for the below views) - * - long-press handling view (transparent, no UI) + * - touch handling view (transparent, no UI) * - foreground icon view (lock/unlock/fingerprint) * - background view (optional) */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index e81d5354ec6e..5ef2d6fd3256 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.res.R import com.android.systemui.shared.R as sharedR import kotlinx.coroutines.DisposableHandle @@ -135,7 +136,7 @@ object KeyguardSmartspaceViewBinder { } } - if (clockBounds == null) return@collect + if (clockBounds == VRectF.ZERO) return@collect if (isLargeClock) { val largeDateHeight = keyguardRootView diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt index 195413a80f4b..485e1ce5b2f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt @@ -75,6 +75,13 @@ object KeyguardTouchViewBinder { onSingleTap(x, y) } + + override fun onDoubleTapDetected(view: View) { + if (falsingManager.isFalseDoubleTap()) { + return + } + viewModel.onDoubleClick() + } } view.repeatWhenAttached { @@ -90,9 +97,20 @@ object KeyguardTouchViewBinder { } } } + launch("$TAG#viewModel.isDoubleTapHandlingEnabled") { + viewModel.isDoubleTapHandlingEnabled.collect { isEnabled -> + view.setDoublePressHandlingEnabled(isEnabled) + view.contentDescription = + if (isEnabled) { + view.resources.getString(R.string.accessibility_desc_lock_screen) + } else { + null + } + } + } } } } - private const val TAG = "KeyguardLongPressViewBinder" + private const val TAG = "KeyguardTouchViewBinder" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt index 1d2edc6d406b..d4e7af48adfe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt @@ -33,6 +33,9 @@ constructor( /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled + /** Whether the double tap handling feature should be enabled. */ + val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled + /** Notifies that the user has long-pressed on the lock screen. * * @param isA11yAction: Whether the action was performed as an a11y action diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java index e7c2a454e16c..b71d8c995e51 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -17,6 +17,7 @@ package com.android.systemui.media; import android.annotation.Nullable; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; @@ -126,6 +127,8 @@ public class RingtonePlayer implements CoreStartable { Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" + Binder.getCallingUid() + ")"); } + enforceUriUserId(uri); + Client client; synchronized (mClients) { client = mClients.get(token); @@ -207,6 +210,7 @@ public class RingtonePlayer implements CoreStartable { @Override public String getTitle(Uri uri) { + enforceUriUserId(uri); final UserHandle user = Binder.getCallingUserHandle(); return Ringtone.getTitle(getContextForUser(user), uri, false /*followSettingsUri*/, false /*allowRemote*/); @@ -214,6 +218,7 @@ public class RingtonePlayer implements CoreStartable { @Override public ParcelFileDescriptor openRingtone(Uri uri) { + enforceUriUserId(uri); final UserHandle user = Binder.getCallingUserHandle(); final ContentResolver resolver = getContextForUser(user).getContentResolver(); @@ -241,6 +246,28 @@ public class RingtonePlayer implements CoreStartable { } }; + /** + * Must be called from the Binder calling thread. + * Ensures caller is from the same userId as the content they're trying to access. + * @param uri the URI to check + * @throws SecurityException when in a non-system call and userId in uri differs from the + * caller's userId + */ + private void enforceUriUserId(Uri uri) throws SecurityException { + final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()); + // for a non-system call, verify the URI to play belongs to the same user as the caller + if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) { + final String errorMessage = "Illegal access to uri=" + uri + + " content associated with user=" + uriUserId + + ", current userID: " + UserHandle.myUserId(); + if (android.media.audio.Flags.ringtoneUserUriCheck()) { + throw new SecurityException(errorMessage); + } else { + Log.e(TAG, errorMessage, new Exception()); + } + } + } + private Context getContextForUser(UserHandle user) { try { return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index 49b53c2d78ae..dfb32e66dae5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -37,6 +37,7 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice import com.android.settingslib.media.flags.Flags +import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -94,8 +95,39 @@ constructor( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaLoaded(key, oldKey, data) } + } else { + onMediaLoaded(key, oldKey, data) + } + } + + override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaRemoved(key, userInitiated) } + } else { + onMediaRemoved(key, userInitiated) + } + } + + fun dump(pw: PrintWriter) { + with(pw) { + println("MediaDeviceManager state:") + entries.forEach { (key, entry) -> + println(" key=$key") + entry.dump(pw) + } + } + } + + @MainThread + private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { + listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } + } + + private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) { if (oldKey != null && oldKey != key) { val oldEntry = entries.remove(oldKey) oldEntry?.stop() @@ -104,9 +136,13 @@ constructor( if (entry == null || entry.token != data.token) { entry?.stop() if (data.device != null) { - // If we were already provided device info (e.g. from RCN), keep that and don't - // listen for updates, but process once to push updates to listeners - processDevice(key, oldKey, data.device) + // If we were already provided device info (e.g. from RCN), keep that and + // don't listen for updates, but process once to push updates to listeners + if (mediaControlsDeviceManagerBackgroundExecution()) { + fgExecutor.execute { processDevice(key, oldKey, data.device) } + } else { + processDevice(key, oldKey, data.device) + } return } val controller = data.token?.let { controllerFactory.create(it) } @@ -120,27 +156,18 @@ constructor( } } - override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + private fun onMediaRemoved(key: String, userInitiated: Boolean) { val token = entries.remove(key) token?.stop() - token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } - } - - fun dump(pw: PrintWriter) { - with(pw) { - println("MediaDeviceManager state:") - entries.forEach { (key, entry) -> - println(" key=$key") - entry.dump(pw) + if (mediaControlsDeviceManagerBackgroundExecution()) { + fgExecutor.execute { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } + } else { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } } - @MainThread - private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { - listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } - } - interface Listener { /** Called when the route has changed for a given notification. */ fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) @@ -260,7 +287,7 @@ constructor( override fun onAboutToConnectDeviceAdded( deviceAddress: String, deviceName: String, - deviceIcon: Drawable? + deviceIcon: Drawable?, ) { aboutToConnectDeviceOverride = AboutToConnectDevice( @@ -270,8 +297,8 @@ constructor( /* enabled */ enabled = true, /* icon */ deviceIcon, /* name */ deviceName, - /* showBroadcastButton */ showBroadcastButton = false - ) + /* showBroadcastButton */ showBroadcastButton = false, + ), ) updateCurrent() } @@ -292,7 +319,7 @@ constructor( override fun onBroadcastMetadataChanged( broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata + metadata: BluetoothLeBroadcastMetadata, ) { logger.logBroadcastMetadataChanged(broadcastId, metadata.toString()) updateCurrent() @@ -352,14 +379,14 @@ constructor( // route. connectedDevice?.copy( name = it.name ?: connectedDevice.name, - icon = icon + icon = icon, ) } ?: MediaDeviceData( enabled = false, icon = MediaControlDrawables.getHomeDevices(context), name = context.getString(R.string.media_seamless_other_device), - showBroadcastButton = false + showBroadcastButton = false, ) logger.logRemoteDevice(routingSession?.name, connectedDevice) } else { @@ -398,7 +425,7 @@ constructor( device?.iconWithoutBackground, name, id = device?.id, - showBroadcastButton = false + showBroadcastButton = false, ) } } @@ -415,7 +442,7 @@ constructor( icon = iconWithoutBackground, name = name, id = id, - showBroadcastButton = false + showBroadcastButton = false, ) private fun getLeAudioBroadcastDeviceData(): MediaDeviceData { @@ -425,7 +452,7 @@ constructor( icon = MediaControlDrawables.getLeAudioSharing(context), name = context.getString(R.string.audio_sharing_description), intent = null, - showBroadcastButton = false + showBroadcastButton = false, ) } else { MediaDeviceData( @@ -433,7 +460,7 @@ constructor( icon = MediaControlDrawables.getAntenna(context), name = broadcastDescription, intent = null, - showBroadcastButton = true + showBroadcastButton = true, ) } } @@ -449,7 +476,7 @@ constructor( device, controller, routingSession?.name, - selectedRoutes?.firstOrNull()?.name + selectedRoutes?.firstOrNull()?.name, ) if (controller == null) { @@ -514,7 +541,7 @@ constructor( MediaDataUtils.getAppLabel( context, localMediaManager.packageName, - context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) + context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name), ) val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) if (isCurrentBroadcastedApp) { @@ -538,5 +565,5 @@ constructor( */ private data class AboutToConnectDevice( val fullMediaDevice: MediaDevice? = null, - val backupMediaDeviceData: MediaDeviceData? = null + val backupMediaDeviceData: MediaDeviceData? = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt index 0e8e595f5b06..848d8221e4db 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt @@ -26,6 +26,10 @@ import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.internal.widget.CachingIconView +import com.android.systemui.FontStyles.GSF_HEADLINE_SMALL +import com.android.systemui.FontStyles.GSF_LABEL_LARGE +import com.android.systemui.FontStyles.GSF_LABEL_MEDIUM +import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM import com.android.systemui.res.R import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -177,9 +181,9 @@ class MediaViewHolder constructor(itemView: View) { R.id.touch_ripple_view, ) - val headlineSmallTF: Typeface = Typeface.create("gsf-headline-small", Typeface.NORMAL) - val titleMediumTF: Typeface = Typeface.create("gsf-title-medium", Typeface.NORMAL) - val labelMediumTF: Typeface = Typeface.create("gsf-label-medium", Typeface.NORMAL) - val labelLargeTF: Typeface = Typeface.create("gsf-label-large", Typeface.NORMAL) + val headlineSmallTF: Typeface = Typeface.create(GSF_HEADLINE_SMALL, Typeface.NORMAL) + val titleMediumTF: Typeface = Typeface.create(GSF_TITLE_MEDIUM, Typeface.NORMAL) + val labelMediumTF: Typeface = Typeface.create(GSF_LABEL_MEDIUM, Typeface.NORMAL) + val labelLargeTF: Typeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java index 795e811db2bc..6ab4a52dc919 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java @@ -68,7 +68,6 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { private final Executor mMainExecutor; private final Executor mBackgroundExecutor; View mHolderView; - private boolean mIsInitVolumeFirstTime; public MediaOutputAdapterLegacy( MediaSwitchingController controller, @@ -78,7 +77,6 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { super(controller); mMainExecutor = mainExecutor; mBackgroundExecutor = backgroundExecutor; - mIsInitVolumeFirstTime = true; } @Override @@ -261,10 +259,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { updateSeekbarProgressBackground(); } } - boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); if (showSeekBar) { - initSeekbar(device, isCurrentSeekbarInvisible); + initSeekbar(device); updateContainerContentA11yImportance(false /* isImportant */); mSeekBar.setContentDescription(contentDescription); } else { @@ -274,9 +271,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { void updateGroupSeekBar(String contentDescription) { updateSeekbarProgressBackground(); - boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; mSeekBar.setVisibility(View.VISIBLE); - initGroupSeekbar(isCurrentSeekbarInvisible); + initGroupSeekbar(); updateContainerContentA11yImportance(false /* isImportant */); mSeekBar.setContentDescription(contentDescription); } @@ -364,31 +360,21 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mActiveRadius, 0, 0}); } - private void initializeSeekbarVolume( - @Nullable MediaDevice device, int currentVolume, - boolean isCurrentSeekbarInvisible) { + private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) { if (!isDragging()) { if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1 || currentVolume == mLatestUpdateVolume)) { // Update only if volume of device and value of volume bar doesn't match. // Check if response volume match with the latest request, to ignore obsolete // response - if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { + if (!mVolumeAnimator.isStarted()) { if (currentVolume == 0) { updateMutedVolumeIcon(device); } else { updateUnmutedVolumeIcon(device); } - } else { - if (!mVolumeAnimator.isStarted()) { - if (currentVolume == 0) { - updateMutedVolumeIcon(device); - } else { - updateUnmutedVolumeIcon(device); - } - mSeekBar.setVolume(currentVolume); - mLatestUpdateVolume = -1; - } + mSeekBar.setVolume(currentVolume); + mLatestUpdateVolume = -1; } } else if (currentVolume == 0) { mSeekBar.resetVolume(); @@ -398,12 +384,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mLatestUpdateVolume = -1; } } - if (mIsInitVolumeFirstTime) { - mIsInitVolumeFirstTime = false; - } } - void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) { + void initSeekbar(@NonNull MediaDevice device) { SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { @Override public int getVolume() { @@ -432,7 +415,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } mSeekBar.setMaxVolume(device.getMaxVolume()); final int currentVolume = device.getCurrentVolume(); - initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible); + initializeSeekbarVolume(device, currentVolume); mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( device, volumeControl) { @@ -445,7 +428,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } // Initializes the seekbar for a group of devices. - void initGroupSeekbar(boolean isCurrentSeekbarInvisible) { + void initGroupSeekbar() { SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { @Override public int getVolume() { @@ -472,7 +455,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mSeekBar.setMaxVolume(mController.getSessionVolumeMax()); final int currentVolume = mController.getSessionVolume(); - initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible); + initializeSeekbarVolume(null, currentVolume); mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( null, volumeControl) { @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 9e6fa48d6f98..f79693138e24 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -145,7 +145,7 @@ public class MediaSwitchingController @VisibleForTesting final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); - private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>(); + private final OutputMediaItemListProxy mOutputMediaItemListProxy; private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>(); private final AudioManager mAudioManager; private final PowerExemptionManager mPowerExemptionManager; @@ -226,6 +226,7 @@ public class MediaSwitchingController InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); + mOutputMediaItemListProxy = new OutputMediaItemListProxy(); mDialogTransitionAnimator = dialogTransitionAnimator; mNearbyMediaDevicesManager = nearbyMediaDevicesManager; mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); @@ -245,7 +246,7 @@ public class MediaSwitchingController protected void start(@NonNull Callback cb) { synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } mNearbyDeviceInfoMap.clear(); if (mNearbyMediaDevicesManager != null) { @@ -291,7 +292,7 @@ public class MediaSwitchingController mLocalMediaManager.stopScan(); synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } if (mNearbyMediaDevicesManager != null) { mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); @@ -333,7 +334,7 @@ public class MediaSwitchingController @Override public void onDeviceListUpdate(List<MediaDevice> devices) { - boolean isListEmpty = mOutputMediaItemList.isEmpty(); + boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); if (isListEmpty || !mIsRefreshing) { buildMediaItems(devices); mCallback.onDeviceListChanged(); @@ -347,11 +348,12 @@ public class MediaSwitchingController } @Override - public void onSelectedDeviceStateChanged(MediaDevice device, - @LocalMediaManager.MediaDeviceState int state) { + public void onSelectedDeviceStateChanged( + MediaDevice device, @LocalMediaManager.MediaDeviceState int state) { mCallback.onRouteChanged(); mMetricLogger.logOutputItemSuccess( - device.toString(), new ArrayList<>(mOutputMediaItemList)); + device.toString(), + new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList())); } @Override @@ -362,7 +364,8 @@ public class MediaSwitchingController @Override public void onRequestFailed(int reason) { mCallback.onRouteChanged(); - mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason); + mMetricLogger.logOutputItemFailure( + new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason); } /** @@ -381,7 +384,7 @@ public class MediaSwitchingController } try { synchronized (mMediaDevicesLock) { - mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + mOutputMediaItemListProxy.removeMutingExpectedDevices(); } mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); } catch (Exception e) { @@ -574,14 +577,14 @@ public class MediaSwitchingController private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices); - mOutputMediaItemList.clear(); - mOutputMediaItemList.addAll(updatedMediaItems); + List<MediaItem> updatedMediaItems = + buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices); + mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems); } } - protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems, - List<MediaDevice> devices) { + protected List<MediaItem> buildMediaItems( + List<MediaItem> oldMediaItems, List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { if (!mLocalMediaManager.isPreferenceRouteListingExist()) { attachRangeInfo(devices); @@ -689,7 +692,8 @@ public class MediaSwitchingController * list. */ @GuardedBy("mMediaDevicesLock") - private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice, + private List<MediaItem> categorizeMediaItemsLocked( + MediaDevice connectedMediaDevice, List<MediaDevice> devices, boolean needToHandleMutingExpectedDevice) { List<MediaItem> finalMediaItems = new ArrayList<>(); @@ -748,6 +752,14 @@ public class MediaSwitchingController } private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) { + MediaItem connectNewDeviceItem = getConnectNewDeviceItem(); + if (connectNewDeviceItem != null) { + mediaItems.add(connectNewDeviceItem); + } + } + + @Nullable + private MediaItem getConnectNewDeviceItem() { boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1; if (enableInputRouting()) { // When input routing is enabled, there are expected to be at least 2 total selected @@ -756,9 +768,9 @@ public class MediaSwitchingController } // Attach "Connect a device" item only when current output is not remote and not a group - if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) { - mediaItems.add(MediaItem.createPairNewDeviceMediaItem()); - } + return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) + ? MediaItem.createPairNewDeviceMediaItem() + : null; } private void attachRangeInfo(List<MediaDevice> devices) { @@ -847,13 +859,13 @@ public class MediaSwitchingController mediaItems.add( MediaItem.createGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title))); - mediaItems.addAll(mOutputMediaItemList); + mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList()); } public List<MediaItem> getMediaItemList() { // If input routing is not enabled, only return output media items. if (!enableInputRouting()) { - return mOutputMediaItemList; + return mOutputMediaItemListProxy.getOutputMediaItemList(); } // If input routing is enabled, return both output and input media items. @@ -959,7 +971,7 @@ public class MediaSwitchingController public boolean isAnyDeviceTransferring() { synchronized (mMediaDevicesLock) { - for (MediaItem mediaItem : mOutputMediaItemList) { + for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) { if (mediaItem.getMediaDevice().isPresent() && mediaItem.getMediaDevice().get().getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { @@ -999,8 +1011,11 @@ public class MediaSwitchingController startActivity(launchIntent, controller); } - void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender, - BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) { + void launchLeBroadcastNotifyDialog( + View mediaOutputDialog, + BroadcastSender broadcastSender, + BroadcastNotifyDialog action, + final DialogInterface.OnClickListener listener) { final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); switch (action) { case ACTION_FIRST_LAUNCH: @@ -1230,8 +1245,8 @@ public class MediaSwitchingController return !sourceList.isEmpty(); } - boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink, - BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { + boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant( + BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { LocalBluetoothLeBroadcastAssistant assistant = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); if (assistant == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java new file mode 100644 index 000000000000..1c9c0b102cb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** A proxy of holding the list of Output Switcher's output media items. */ +public class OutputMediaItemListProxy { + private final List<MediaItem> mOutputMediaItemList; + + public OutputMediaItemListProxy() { + mOutputMediaItemList = new CopyOnWriteArrayList<>(); + } + + /** Returns the list of output media items. */ + public List<MediaItem> getOutputMediaItemList() { + return mOutputMediaItemList; + } + + /** Updates the list of output media items with the given list. */ + public void clearAndAddAll(List<MediaItem> updatedMediaItems) { + mOutputMediaItemList.clear(); + mOutputMediaItemList.addAll(updatedMediaItems); + } + + /** Removes the media items with muting expected devices. */ + public void removeMutingExpectedDevices() { + mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + } + + /** Clears the output media item list. */ + public void clear() { + mOutputMediaItemList.clear(); + } + + /** Returns whether the output media item list is empty. */ + public boolean isEmpty() { + return mOutputMediaItemList.isEmpty(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index 9c6568057d6f..9eb55a8eff2e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource @@ -44,7 +45,6 @@ import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -65,6 +65,7 @@ import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults.colors import androidx.compose.material3.SliderState import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -376,29 +377,32 @@ private fun ContentScope.CardForegroundContent( ) { Column( modifier = - modifier - .combinedClickable( - onClick = viewModel.onClick, - onLongClick = viewModel.onLongClick, - onClickLabel = viewModel.onClickLabel, - ) - .padding(16.dp) + modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick, + onClickLabel = viewModel.onClickLabel, + ) ) { // Always add the first/top row, regardless of presentation style. - Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.fillMaxWidth()) { // Icon. Icon( icon = viewModel.icon, tint = colorScheme.primary, - modifier = Modifier.size(24.dp).clip(CircleShape), + modifier = + Modifier.align(Alignment.TopStart) + .padding(top = 16.dp, start = 16.dp) + .size(24.dp) + .clip(CircleShape), ) - Spacer(modifier = Modifier.weight(1f)) - viewModel.outputSwitcherChips.fastForEach { chip -> - OutputSwitcherChip( - viewModel = chip, - colorScheme = colorScheme, - modifier = Modifier.padding(start = 8.dp), - ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.align(Alignment.TopEnd), + ) { + viewModel.outputSwitcherChips.fastForEach { chip -> + OutputSwitcherChip(viewModel = chip, colorScheme = colorScheme) + } } } @@ -415,7 +419,7 @@ private fun ContentScope.CardForegroundContent( // Second row. Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp), ) { Metadata( title = viewModel.title, @@ -441,7 +445,7 @@ private fun ContentScope.CardForegroundContent( Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 24.dp), + modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp), ) { Navigation( viewModel = viewModel.navigation, @@ -464,7 +468,7 @@ private fun ContentScope.CardForegroundContent( // Bottom row. Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 36.dp), + modifier = Modifier.padding(start = 16.dp, top = 36.dp, end = 16.dp, bottom = 16.dp), ) { Metadata( title = viewModel.title, @@ -914,20 +918,48 @@ private fun OutputSwitcherChip( colorScheme: AnimatedColorScheme, modifier: Modifier = Modifier, ) { - PlatformButton( - onClick = viewModel.onClick, - colors = ButtonDefaults.buttonColors(containerColor = colorScheme.primary), - contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), - modifier = modifier.height(24.dp), + // For accessibility reasons, the touch area for the chip needs to be at least 48dp in height. + // At the same time, the rounded corner chip should only be as tall as it needs to be to contain + // its contents and look like a nice design; also, the ripple effect should only be shown within + // the bounds of the chip. + // + // This is achieved by sharing this InteractionSource between the outer and inner composables. + // + // The outer composable hosts that clickable that writes user events into the InteractionSource. + // The inner composable consumes the user events from the InteractionSource and feeds them into + // its indication. + val clickInteractionSource = remember { MutableInteractionSource() } + Box( + modifier = + modifier + .height(48.dp) + .clickable(interactionSource = clickInteractionSource, indication = null) { + viewModel.onClick() + } + .padding(top = 16.dp, end = 16.dp, bottom = 8.dp) ) { - Icon(icon = viewModel.icon, tint = colorScheme.onPrimary, modifier = Modifier.size(16.dp)) - viewModel.text?.let { - Spacer(Modifier.size(4.dp)) - Text( - text = viewModel.text, - style = MaterialTheme.typography.bodySmall, - color = colorScheme.onPrimary, + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip(RoundedCornerShape(12.dp)) + .background(colorScheme.primary) + .indication(clickInteractionSource, ripple()) + .padding(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), + ) { + Icon( + icon = viewModel.icon, + tint = colorScheme.onPrimary, + modifier = Modifier.size(16.dp), ) + + viewModel.text?.let { + Text( + text = viewModel.text, + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onPrimary, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt index aaed606f8fb2..cec846e84f01 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt @@ -43,8 +43,6 @@ class StateChange { return this } - fun hasChanges() = flagsToSet != 0L || flagsToClear != 0L - /** * Applies all changed flags to [sysUiState]. * @@ -83,6 +81,7 @@ class StateChange { iterateBits(flagsToSet or flagsToClear) { bit -> sysUiState.setFlag(bit, false) } } + /** Resets all the pending changes. */ fun clear() { flagsToSet = 0 flagsToClear = 0 diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt index e99ee7ddb919..71cb74543485 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState.SysUiStateCallback +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags import dagger.assisted.Assisted @@ -29,6 +30,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dalvik.annotation.optimization.NeverCompile import java.io.PrintWriter +import java.lang.Long.bitCount import javax.inject.Inject /** Contains sysUi state flags and notifies registered listeners whenever changes happen. */ @@ -111,8 +113,7 @@ constructor( get() = _flags private var _flags: Long = 0 - private var flagsToSet: Long = 0 - private var flagsToClear: Long = 0 + private val stateChange = StateChange() /** * Add listener to be notified of changes made to SysUI state. The callback will also be called @@ -132,13 +133,11 @@ constructor( /** Methods to this call can be chained together before calling [.commitUpdate]. */ override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState { - val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) - - if (toSet) { - flagsToSet = flagsToSet or flag - } else { - flagsToClear = flagsToClear or flag + if (ShadeWindowGoesAround.isEnabled && bitCount(flag) > 1) { + error("Flags should be a single bit.") } + val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) + stateChange.setFlag(flag, toSet) return this } @@ -147,27 +146,19 @@ constructor( ReplaceWith("commitUpdate()"), ) override fun commitUpdate(displayId: Int) { - // TODO b/398011576 - handle updates for different displays. commitUpdate() } override fun commitUpdate() { - updateFlags() - flagsToSet = 0 - flagsToClear = 0 - } - - private fun updateFlags() { - var newState = flags - newState = newState or flagsToSet - newState = newState and flagsToClear.inv() + val newState = stateChange.applyTo(flags) notifyAndSetSystemUiStateChanged(newState, flags) + stateChange.clear() } /** Notify all those who are registered that the state has changed. */ private fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) { if (SysUiState.DEBUG) { - Log.d(TAG, "SysUiState changed: old=$oldFlags new=$newFlags") + Log.d(TAG, "SysUiState changed for displayId=$displayId: old=$oldFlags new=$newFlags") } if (newFlags != oldFlags) { _flags = newFlags @@ -185,6 +176,8 @@ constructor( pw.println(QuickStepContract.isBackGestureDisabled(flags, false /* forTrackpad */)) pw.print(" assistantGestureDisabled=") pw.println(QuickStepContract.isAssistantGestureDisabled(flags)) + pw.print(" pendingStateChanges=") + pw.println(stateChange.toString()) } override fun destroy() { diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index d8fc52bcc55a..8dc27bf4ac3e 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -162,10 +162,6 @@ constructor( ): Boolean { return this@NoteTaskInitializer.handleKeyGestureEvent(event) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return this@NoteTaskInitializer.isKeyGestureSupported(gestureType) - } } /** @@ -225,10 +221,6 @@ constructor( return true } - private fun isKeyGestureSupported(gestureType: Int): Boolean { - return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES - } - companion object { val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong() val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt index 701f44e9981c..d40ecc9565ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt @@ -61,6 +61,9 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } } + val title = tileDetailedViewModel.title + val subTitle = tileDetailedViewModel.subTitle + Column( modifier = modifier @@ -90,7 +93,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode ) } Text( - text = tileDetailedViewModel.getTitle(), + text = title, modifier = Modifier.align(Alignment.CenterVertically), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge, @@ -110,7 +113,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode } } Text( - text = tileDetailedViewModel.getSubTitle(), + text = subTitle, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleSmall, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 99f52c28a137..3ae90d2f976b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -16,14 +16,20 @@ package com.android.systemui.qs.panels.ui.compose.toolbar +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.qs.footer.ui.compose.IconButton import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus @Composable fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { @@ -44,7 +50,18 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Modifier.sysuiResTag("settings_button_container"), ) - Spacer(modifier = Modifier.weight(1f)) + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { + BuildNumber( + viewModelFactory = viewModel.buildNumberViewModelFactory, + textColor = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(1.dp), + ) + .wrapContentSize(), + ) + } IconButton( { viewModel.powerButtonViewModel }, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt index e54bfa29d2db..10d7871b8ea2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import com.android.systemui.animation.Expandable import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap +import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -46,6 +47,7 @@ class ToolbarViewModel @AssistedInject constructor( editModeButtonViewModelFactory: EditModeButtonViewModel.Factory, + val buildNumberViewModelFactory: BuildNumberViewModel.Factory, private val footerActionsInteractor: FooterActionsInteractor, private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, private val falsingInteractor: FalsingInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt index 7d396c58630e..8ffba1e5f3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt @@ -19,26 +19,14 @@ package com.android.systemui.qs.tiles.dialog import android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.res.R @Composable fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - val internetDetailsContentManager = remember { - viewModel.contentManagerFactory.create( - canConfigMobileData = viewModel.getCanConfigMobileData(), - canConfigWifi = viewModel.getCanConfigWifi(), - coroutineScope = coroutineScope, - context = context, - ) - } AndroidView( modifier = Modifier.fillMaxSize(), @@ -46,11 +34,11 @@ fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { // Inflate with the existing dialog xml layout and bind it with the manager val view = LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null) - internetDetailsContentManager.bind(view) + viewModel.internetDetailsContentManager.bind(view, coroutineScope) view // TODO: b/377388104 - Polish the internet details view UI }, - onRelease = { internetDetailsContentManager.unBind() }, + onRelease = { viewModel.internetDetailsContentManager.unBind() }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt index 659488bdd0d3..d8e1755e6cca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt @@ -43,6 +43,9 @@ import android.widget.Switch import android.widget.TextView import androidx.annotation.MainThread import androidx.annotation.WorkerThread +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry @@ -79,8 +82,6 @@ constructor( private val internetDetailsContentController: InternetDetailsContentController, @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean, @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean, - @Assisted private val coroutineScope: CoroutineScope, - @Assisted private var context: Context, private val uiEventLogger: UiEventLogger, @Main private val handler: Handler, @Background private val backgroundExecutor: Executor, @@ -121,26 +122,29 @@ constructor( private lateinit var shareWifiButton: Button private lateinit var airplaneModeButton: Button private var alertDialog: AlertDialog? = null - - private val canChangeWifiState = - WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) + private var canChangeWifiState = false private var wifiNetworkHeight = 0 private var backgroundOn: Drawable? = null private var backgroundOff: Drawable? = null private var clickJob: Job? = null private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId - @VisibleForTesting - internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope) + @VisibleForTesting internal lateinit var adapter: InternetAdapter @VisibleForTesting internal var wifiEntriesCount: Int = 0 @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false + private lateinit var context: Context + private lateinit var coroutineScope: CoroutineScope + + var title by mutableStateOf("") + private set + + var subTitle by mutableStateOf("") + private set @AssistedFactory interface Factory { fun create( @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean, @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean, - coroutineScope: CoroutineScope, - context: Context, ): InternetDetailsContentManager } @@ -152,12 +156,16 @@ constructor( * * @param contentView The view to which the content manager should be bound. */ - fun bind(contentView: View) { + fun bind(contentView: View, coroutineScope: CoroutineScope) { if (DEBUG) { Log.d(TAG, "Bind InternetDetailsContentManager") } this.contentView = contentView + context = contentView.context + this.coroutineScope = coroutineScope + adapter = InternetAdapter(internetDetailsContentController, coroutineScope) + canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) initializeLifecycle() initializeViews() @@ -323,11 +331,11 @@ constructor( } } - fun getTitleText(): String { + private fun getTitleText(): String { return internetDetailsContentController.getDialogTitleText().toString() } - fun getSubtitleText(): String { + private fun getSubtitleText(): String { return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() } @@ -336,6 +344,13 @@ constructor( Log.d(TAG, "updateDetailsUI ") } + if (!::context.isInitialized) { + return + } + + title = getTitleText() + subTitle = getSubtitleText() + airplaneModeButton.visibility = if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE 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 6709fd2bb508..fb63bea4fb9f 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 @@ -28,38 +28,27 @@ class InternetDetailsViewModel @AssistedInject constructor( private val accessPointController: AccessPointController, - val contentManagerFactory: InternetDetailsContentManager.Factory, + private val contentManagerFactory: InternetDetailsContentManager.Factory, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, -) : TileDetailsViewModel() { - override fun clickOnSettingsButton() { - qsTileIntentUserActionHandler.handle( - /* expandable= */ null, - Intent(Settings.ACTION_WIFI_SETTINGS), +) : TileDetailsViewModel { + val internetDetailsContentManager by lazy { + contentManagerFactory.create( + canConfigMobileData = accessPointController.canConfigMobileData(), + canConfigWifi = accessPointController.canConfigWifi(), ) } - override fun getTitle(): String { - // TODO: b/377388104 make title and sub title mutable states of string - // by internetDetailsContentManager.getTitleText() - // TODO: test title change between airplane mode and not airplane mode - // TODO: b/377388104 Update the placeholder text - return "Internet" - } + override val title: String + get() = internetDetailsContentManager.title - override fun getSubTitle(): String { - // TODO: b/377388104 make title and sub title mutable states of string - // by internetDetailsContentManager.getSubtitleText() - // TODO: test subtitle change between airplane mode and not airplane mode - // TODO: b/377388104 Update the placeholder text - return "Tab a network to connect" - } + override val subTitle: String + get() = internetDetailsContentManager.subTitle - fun getCanConfigMobileData(): Boolean { - return accessPointController.canConfigMobileData() - } - - fun getCanConfigWifi(): Boolean { - return accessPointController.canConfigWifi() + override fun clickOnSettingsButton() { + qsTileIntentUserActionHandler.handle( + /* expandable= */ null, + Intent(Settings.ACTION_WIFI_SETTINGS), + ) } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt index 9a39c3c095ef..4f7e03bd3bc3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt @@ -23,18 +23,14 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogView class ModesDetailsViewModel( private val onSettingsClick: () -> Unit, val viewModel: ModesDialogViewModel, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { onSettingsClick() } - override fun getTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file. - return "Modes" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + override val title = "Modes" - override fun getSubTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file. - return "Silences interruptions from people and apps in different circumstances" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + override val subTitle = "Silences interruptions from people and apps in different circumstances" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt index c84ddb6fdb36..59f209edb546 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -23,19 +23,15 @@ import com.android.systemui.screenrecord.RecordingController class ScreenRecordDetailsViewModel( val recordingController: RecordingController, val onStartRecordingClicked: Runnable, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { // No settings button in this tile. } - override fun getTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file, - return "Screen recording" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + override val title = "Screen recording" - override fun getSubTitle(): String { - // No sub-title in this tile. - return "" - } + // No sub-title in this tile. + override val subTitle = "" } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 3be2f1b7b957..362b5db012e1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.content.Context +import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock @@ -25,11 +26,13 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.WindowInsets import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.ui.platform.ComposeView +import androidx.core.view.updateMargins import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleObserver @@ -101,7 +104,10 @@ constructor( ) : LifecycleOwner { private val logger = Logger(logBuffer, TAG) - private class CommunalWrapper(context: Context) : FrameLayout(context) { + private class CommunalWrapper( + context: Context, + private val communalSettingsInteractor: CommunalSettingsInteractor, + ) : FrameLayout(context) { private val consumers: MutableSet<Consumer<Boolean>> = ArraySet() override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { @@ -121,6 +127,24 @@ constructor( consumers.clear() } } + + override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets { + if ( + !communalSettingsInteractor.isV2FlagEnabled() || + resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE + ) { + return super.onApplyWindowInsets(windowInsets) + } + val type = WindowInsets.Type.displayCutout() + val insets = windowInsets.getInsets(type) + + // Reset horizontal margins added by window insets, so hub can be edge to edge. + if (insets.left > 0 || insets.right > 0) { + val lp = layoutParams as LayoutParams + lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin) + } + return WindowInsets.CONSUMED + } } /** The container view for the hub. This will not be initialized until [initView] is called. */ @@ -443,7 +467,8 @@ constructor( collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it }) - communalContainerWrapper = CommunalWrapper(containerView.context) + communalContainerWrapper = + CommunalWrapper(containerView.context, communalSettingsInteractor) communalContainerWrapper?.addView(communalContainerView) logger.d("Hub container initialized") return communalContainerWrapper!! diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index fa17b4fad592..dafb1a559d59 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -498,17 +498,18 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private boolean isExpanded(NotificationShadeWindowState state) { + boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0; boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded() || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing || state.headsUpNotificationShowing || state.scrimsVisibility != ScrimController.TRANSPARENT) - || state.backgroundBlurRadius > 0 + || visForBlur || state.launchingActivityFromNotification; mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed, state.isKeyguardShowingAndNotOccluded(), state.panelVisible, state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing, state.scrimsVisibility != ScrimController.TRANSPARENT, - state.backgroundBlurRadius > 0, state.launchingActivityFromNotification); + visForBlur, state.launchingActivityFromNotification); return isExpanded; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index c2e355d07e9c..03c191e40ccf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -22,6 +22,7 @@ import android.app.Flags; import android.app.Notification; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -31,6 +32,8 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.ConversationLayout; @@ -39,6 +42,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationContentView; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.ArrayList; import java.util.HashSet; @@ -214,7 +218,7 @@ public class NotificationGroupingUtil { } // in case no view is visible we make sure the time is visible int timeVisibility = !hasVisibleText - || row.getEntry().getSbn().getNotification().showsTime() + || showsTime(row) ? View.VISIBLE : View.GONE; time.setVisibility(timeVisibility); View left = null; @@ -243,6 +247,17 @@ public class NotificationGroupingUtil { } } + @VisibleForTesting + boolean showsTime(ExpandableNotificationRow row) { + StatusBarNotification sbn; + if (NotificationBundleUi.isEnabled()) { + sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null; + } else { + sbn = row.getEntry().getSbn(); + } + return (sbn != null && sbn.getNotification().showsTime()); + } + /** * Reset the modifications to this row for removing it from the group. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 0d34bdc7e477..041ed6504634 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -59,11 +59,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.RemoteInputControllerLogger; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.RemoteInputView; @@ -134,20 +136,21 @@ public class NotificationRemoteInputManager implements CoreStartable { view.getTag(com.android.internal.R.id.notification_action_index_tag); final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent()); - final NotificationEntry entry = getNotificationForParent(view.getParent()); - mLogger.logInitialClick( - row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent); + if (row == null) { + return false; + } + mLogger.logInitialClick(row.getLoggingKey(), actionIndex, pendingIntent); if (handleRemoteInput(view, pendingIntent)) { - mLogger.logRemoteInputWasHandled( - row != null ? row.getLoggingKey() : null, actionIndex); + mLogger.logRemoteInputWasHandled(row.getLoggingKey(), actionIndex); return true; } if (DEBUG) { Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); } - logActionClick(view, entry, pendingIntent); + Notification.Action action = getActionFromView(view, row, pendingIntent); + logActionClick(view, row.getKey(), action); // The intent we are sending is for the application, which // won't have permission to immediately start an activity after // the user switches to home. We know it is safe to do at this @@ -156,33 +159,47 @@ public class NotificationRemoteInputManager implements CoreStartable { ActivityManager.getService().resumeAppSwitches(); } catch (RemoteException e) { } - Notification.Action action = getActionFromView(view, entry, pendingIntent); return mCallback.handleRemoteViewClick(view, pendingIntent, action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> { Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); mLogger.logStartingIntentWithDefaultHandler( - row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex); + row.getLoggingKey(), pendingIntent, actionIndex); boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options); - if (started) releaseNotificationIfKeptForRemoteInputHistory(entry); + if (started) { + if (NotificationBundleUi.isEnabled()) { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntryAdapter()); + } else { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntry()); + } + } return started; }); } private @Nullable Notification.Action getActionFromView(View view, - NotificationEntry entry, PendingIntent actionIntent) { + ExpandableNotificationRow row, PendingIntent actionIntent) { Integer actionIndex = (Integer) view.getTag(com.android.internal.R.id.notification_action_index_tag); if (actionIndex == null) { return null; } - if (entry == null) { + StatusBarNotification statusBarNotification = null; + if (NotificationBundleUi.isEnabled()) { + if (row.getEntryAdapter() != null) { + statusBarNotification = row.getEntryAdapter().getSbn(); + } + } else { + if (row.getEntry() != null) { + statusBarNotification = row.getEntry().getSbn(); + } + } + if (statusBarNotification == null) { Log.w(TAG, "Couldn't determine notification for click."); return null; } // Notification may be updated before this function is executed, and thus play safe // here and verify that the action object is still the one that where the click happens. - StatusBarNotification statusBarNotification = entry.getSbn(); Notification.Action[] actions = statusBarNotification.getNotification().actions; if (actions == null || actionIndex >= actions.length) { Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid"); @@ -199,14 +216,12 @@ public class NotificationRemoteInputManager implements CoreStartable { private void logActionClick( View view, - NotificationEntry entry, - PendingIntent actionIntent) { - Notification.Action action = getActionFromView(view, entry, actionIntent); + String key, + Notification.Action action) { if (action == null) { return; } ViewParent parent = view.getParent(); - String key = entry.getSbn().getKey(); int buttonIndex = -1; // If this is a default template, determine the index of the button. if (view.getId() == com.android.internal.R.id.action0 && @@ -214,20 +229,10 @@ public class NotificationRemoteInputManager implements CoreStartable { ViewGroup actionGroup = (ViewGroup) parent; buttonIndex = actionGroup.indexOfChild(view); } - final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); + final NotificationVisibility nv = mVisibilityProvider.obtain(key, true); mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false); } - private NotificationEntry getNotificationForParent(ViewParent parent) { - while (parent != null) { - if (parent instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) parent).getEntry(); - } - parent = parent.getParent(); - } - return null; - } - private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) { while (parent != null) { if (parent instanceof ExpandableNotificationRow) { @@ -394,11 +399,21 @@ public class NotificationRemoteInputManager implements CoreStartable { } } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void addActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.addIfAbsent(listener); } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void removeActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.remove(listener); } @@ -681,12 +696,30 @@ public class NotificationRemoteInputManager implements CoreStartable { * (after unlock, if applicable), and will then wait a short time to allow the app to update the * notification in response to the action. */ + private void releaseNotificationIfKeptForRemoteInputHistory(EntryAdapter entryAdapter) { + if (entryAdapter == null) { + return; + } + if (mRemoteInputListener != null) { + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory( + entryAdapter.getKey()); + } + entryAdapter.onNotificationActionClicked(); + } + + /** + * Checks if the notification is being kept due to the user sending an inline reply, and if + * so, releases that hold. This is called anytime an action on the notification is dispatched + * (after unlock, if applicable), and will then wait a short time to allow the app to update the + * notification in response to the action. + */ private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry == null) { return; } if (mRemoteInputListener != null) { - mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry); + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry.getKey()); } for (Consumer<NotificationEntry> listener : mActionPressListeners) { listener.accept(entry); @@ -866,7 +899,7 @@ public class NotificationRemoteInputManager implements CoreStartable { boolean isNotificationKeptForRemoteInputHistory(@NonNull String key); /** Called on user interaction to end lifetime extension for history */ - void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry); + void releaseNotificationIfKeptForRemoteInputHistory(@NonNull String entryKey); /** Called when the RemoteInputController is attached to the manager */ void setRemoteInputController(@NonNull RemoteInputController remoteInputController); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS index b2764e1a2302..6cebcd98a0ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS @@ -24,6 +24,8 @@ per-file *Blur* = set noparent per-file *Blur* = shanh@google.com, rahulbanerjee@google.com # Not setting noparent here, since *Mode* matches many other classes (e.g., *ViewModel*) per-file *Mode* = file:notification/OWNERS +per-file *SmartReply* = set noparent +per-file *SmartReply* = file:notification/OWNERS per-file *RemoteInput* = set noparent per-file *RemoteInput* = file:notification/OWNERS per-file *EmptyShadeView* = set noparent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt new file mode 100644 index 000000000000..176419454c21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips + +import com.android.systemui.Flags +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization + +/** Helper for reading or using the status_bar_chips_return_animations flag state. */ +object StatusBarChipsReturnAnimations { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS + + /** Is the feature enabled. */ + @JvmStatic + inline val isEnabled + get() = StatusBarChipsModernization.isEnabled && Flags.statusBarChipsReturnAnimations() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 16bf43bbf272..7e7031200988 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -32,6 +32,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog +import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -43,8 +44,10 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCall import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -60,25 +63,60 @@ constructor( private val activityStarter: ActivityStarter, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = - interactor.ongoingCallState - .map { state -> - when (state) { - is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() - is OngoingCallModel.InCall -> - if (state.isAppVisible) { - OngoingActivityChipModel.Inactive() - } else { - prepareChip(state, systemClock) - } + private val chipWithReturnAnimation: StateFlow<OngoingActivityChipModel> = + if (StatusBarChipsReturnAnimations.isEnabled) { + interactor.ongoingCallState + .map { state -> + when (state) { + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + is OngoingCallModel.InCall -> + prepareChip(state, systemClock, isHidden = state.isAppVisible) + } } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive()) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + OngoingActivityChipModel.Inactive(), + ) + } else { + MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow() + } + + private val chipLegacy: StateFlow<OngoingActivityChipModel> = + if (!StatusBarChipsReturnAnimations.isEnabled) { + interactor.ongoingCallState + .map { state -> + when (state) { + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + is OngoingCallModel.InCall -> + if (state.isAppVisible) { + OngoingActivityChipModel.Inactive() + } else { + prepareChip(state, systemClock, isHidden = false) + } + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + OngoingActivityChipModel.Inactive(), + ) + } else { + MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow() + } + + override val chip: StateFlow<OngoingActivityChipModel> = + if (StatusBarChipsReturnAnimations.isEnabled) { + chipWithReturnAnimation + } else { + chipLegacy + } /** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */ private fun prepareChip( state: OngoingCallModel.InCall, systemClock: SystemClock, + isHidden: Boolean, ): OngoingActivityChipModel.Active { val key = state.notificationKey val contentDescription = getContentDescription(state.appName) @@ -110,6 +148,7 @@ constructor( colors = colors, onClickListenerLegacy = getOnClickListener(state.intent), clickBehavior = getClickBehavior(state.intent), + isHidden = isHidden, ) } else { val startTimeInElapsedRealtime = @@ -121,6 +160,7 @@ constructor( startTimeMs = startTimeInElapsedRealtime, onClickListenerLegacy = getOnClickListener(state.intent), clickBehavior = getClickBehavior(state.intent), + isHidden = isHidden, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 4d68f2e6ef1b..f6535730cf77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -33,7 +33,8 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; - +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import kotlinx.coroutines.flow.MutableStateFlow; @@ -51,10 +52,27 @@ public class BundleEntry extends PipelineEntry { // TODO (b/389839319): implement the row private ExpandableNotificationRow mRow; + private final List<ListEntry> mChildren = new ArrayList<>(); + + private final List<ListEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren); + public BundleEntry(String key) { super(key); } + void addChild(ListEntry child) { + mChildren.add(child); + } + + @NonNull + public List<ListEntry> getChildren() { + return mUnmodifiableChildren; + } + + /** + * @return Null because bundles do not have an associated NotificationEntry. + */ + @Nullable @Override public NotificationEntry getRepresentativeEntry() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt index 64db9df8270c..26c302bf6409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.content.Context import android.os.Build import android.service.notification.StatusBarNotification +import android.util.Log import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import kotlinx.coroutines.flow.StateFlow @@ -118,5 +119,13 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter { override fun onNotificationBubbleIconClicked() { // do nothing. these cannot be a bubble + Log.wtf(TAG, "onNotificationBubbleIconClicked() called") + } + + override fun onNotificationActionClicked() { + // do nothing. these have no actions + Log.wtf(TAG, "onNotificationActionClicked() called") } } + +private const val TAG = "BundleEntryAdapter" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 0e75b6050678..3118ce56ac69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -140,5 +140,10 @@ public interface EntryAdapter { * Process a click on a notification bubble icon */ void onNotificationBubbleIconClicked(); + + /** + * Processes a click on a notification action + */ + void onNotificationActionClicked(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt index 779c25a3b402..a5169865c3c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection -import androidx.annotation.VisibleForTesting import com.android.internal.logging.MetricsLogger import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import javax.inject.Inject @@ -33,6 +33,7 @@ constructor( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, ) : EntryAdapterFactory { override fun create(entry: PipelineEntry): EntryAdapter { return if (entry is NotificationEntry) { @@ -42,15 +43,11 @@ constructor( peopleNotificationIdentifier, iconStyleProvider, visualStabilityCoordinator, + notificationActionClickManager, entry, ) } else { BundleEntryAdapter((entry as BundleEntry)) } } - - @VisibleForTesting - fun getNotificationActivityStarter() : NotificationActivityStarter { - return notificationActivityStarter - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt index 0ff2dd7c7f43..1168c647c26a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.Visual import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import kotlinx.coroutines.flow.StateFlow @@ -33,6 +34,7 @@ class NotificationEntryAdapter( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, private val entry: NotificationEntry, ) : EntryAdapter { @@ -142,4 +144,8 @@ class NotificationEntryAdapter( override fun onNotificationBubbleIconClicked() { notificationActivityStarter.onNotificationBubbleIconClicked(entry) } + + override fun onNotificationActionClicked() { + notificationActionClickManager.onNotificationActionClicked(entry) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 59fe42823994..238ba8d9f490 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -282,16 +282,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { Assert.isMainThread(); mPipelineState.requireState(STATE_IDLE); - // TODO(b/396301289) throw exception when setting more than once? mNotifBundler = bundler; + if (mNotifBundler == null) { + throw new IllegalStateException(TAG + ".setBundler: null"); + } - if (mIdToBundleEntry.isEmpty()) { - if (mNotifBundler == null) { - throw new IllegalStateException("NotifBundler not attached."); - } - for (String id: mNotifBundler.getBundleIds()) { - mIdToBundleEntry.put(id, new BundleEntry(id)); - } + mIdToBundleEntry.clear(); + for (String id: mNotifBundler.getBundleIds()) { + mIdToBundleEntry.put(id, new BundleEntry(id)); } } @@ -681,7 +679,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { j--; } } - } else { + } else if (tle instanceof NotificationEntry) { // maybe put top-level-entries back into their previous groups if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) { // entry was put back into its previous group, so we remove it from the list of @@ -689,7 +687,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { topLevelList.remove(i); i--; } - } + } // Promoters ignore bundles so we don't have to demote any here. } Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index fdb8cd871dd9..a0eab43f854b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotifPipelineFlags +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -47,7 +48,9 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock @@ -82,6 +85,7 @@ constructor( private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider, private val mRemoteInputManager: NotificationRemoteInputManager, + private val notificationActionClickManager: NotificationActionClickManager, private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mFlags: NotifPipelineFlags, private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor, @@ -107,7 +111,11 @@ constructor( pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) pipeline.addPromoter(mNotifPromoter) pipeline.addNotificationLifetimeExtender(mLifetimeExtender) - mRemoteInputManager.addActionPressListener(mActionPressListener) + if (NotificationBundleUi.isEnabled) { + notificationActionClickManager.addActionClickListener(mActionPressListener) + } else { + mRemoteInputManager.addActionPressListener(mActionPressListener) + } if (StatusBarNotifChips.isEnabled) { applicationScope.launch { @@ -423,6 +431,7 @@ constructor( map[child.key] = GroupLocation.Child } } + is BundleEntry -> map[topLevelEntry.key] = GroupLocation.Bundle else -> error("unhandled type $topLevelEntry") } } @@ -781,7 +790,7 @@ constructor( */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> - mHeadsUpManager.setUserActionMayIndirectlyRemove(entry) + mHeadsUpManager.setUserActionMayIndirectlyRemove(entry.key) mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) } } @@ -950,6 +959,7 @@ private enum class GroupLocation { Isolated, Summary, Child, + Bundle, } private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 3fad8f0510a1..1f32b945ce7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.notification.collection.BundleEntry; import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -116,6 +117,9 @@ public class RankingCoordinator implements Coordinator { NotificationPriorityBucketKt.BUCKET_SILENT) { @Override public boolean isInSection(PipelineEntry entry) { + if (entry instanceof BundleEntry) { + return true; + } return !mHighPriorityProvider.isHighPriority(entry) && entry.getRepresentativeEntry() != null && !entry.getRepresentativeEntry().isAmbient(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt index e7c767f42c12..27c0dcccfe43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt @@ -221,20 +221,20 @@ constructor( mSmartReplyHistoryExtender.isExtending(key) } else false - override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { - if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") + override fun releaseNotificationIfKeptForRemoteInputHistory(entryKey: String) { + if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entryKey})") if (!lifetimeExtensionRefactor()) { mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index c2f0806a9cd6..6b32c6a18ec0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -276,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { if (inflaterParams.isChildInGroup() - && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + && redactionType != REDACTION_TYPE_NONE) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index ef3da9498f70..1e5aa01714be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -346,4 +347,5 @@ public interface NotificationsModule { /** Provides an instance of {@link EntryAdapterFactory} */ @Binds EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl); + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt index 9728fcfcd6ba..25ae50c34659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.headsup import android.graphics.Region import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import dagger.Binds @@ -155,9 +154,9 @@ interface HeadsUpManager : Dumpable { fun setAnimationStateHandler(handler: AnimationStateHandler) /** - * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until - * it's collapsed again. - */ + * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until + * it's collapsed again. + */ fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean) /** @@ -199,12 +198,12 @@ interface HeadsUpManager : Dumpable { * Notes that the user took an action on an entry that might indirectly cause the system or the * app to remove the notification. * - * @param entry the entry that might be indirectly removed by the user's action + * @param entry the key of the entry that might be indirectly removed by the user's action * @see * com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator.mActionPressListener * @see .canRemoveImmediately */ - fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) + fun setUserActionMayIndirectlyRemove(entryKey: String) /** * Decides whether a click is invalid for a notification, i.e. it has not been shown long enough @@ -332,7 +331,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun setUser(user: Int) {} - override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {} + override fun setUserActionMayIndirectlyRemove(entryKey: String) {} override fun shouldSwallowClick(key: String): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index ca94655318b9..ca83666079ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -1126,8 +1126,8 @@ public class HeadsUpManagerImpl * @see HeadsUpCoordinator.mActionPressListener * @see #canRemoveImmediately(String) */ - public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { - HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); + public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey); if (headsUpEntry != null) { headsUpEntry.mUserActionMayIndirectlyRemove = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index b481455699ef..8da2f768bf71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -3054,6 +3054,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); + if (android.app.Flags.expandingPublicView()) { + mPublicLayout.setUserExpanding(userLocked); + } // This is intentionally not guarded with mIsSummaryWithChildren since we might have had // children but not anymore. if (mChildrenContainer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt new file mode 100644 index 000000000000..2b451406eaad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.util.ListenerSet +import java.util.function.Consumer +import javax.inject.Inject + +/** + * Pipeline components can register consumers here to be informed when a notification action is + * clicked + */ +@SysUISingleton +class NotificationActionClickManager @Inject constructor() { + private val actionClickListeners = ListenerSet<Consumer<NotificationEntry>>() + + fun addActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.addIfAbsent(listener) + } + + fun removeActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.remove(listener) + } + + fun onNotificationActionClicked(entry: NotificationEntry) { + for (listener in actionClickListeners) { + listener.accept(entry) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 193203720aa5..676c96ebc9d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -293,8 +293,8 @@ public class NotificationContentView extends FrameLayout implements Notification useExactly = true; } int spec = MeasureSpec.makeMeasureSpec(size, useExactly - ? MeasureSpec.EXACTLY - : MeasureSpec.AT_MOST); + ? MeasureSpec.EXACTLY + : MeasureSpec.AT_MOST); measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0); maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); } @@ -721,14 +721,14 @@ public class NotificationContentView extends FrameLayout implements Notification private int getMinContentHeightHint() { if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) { return mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.notification_action_list_height); + com.android.internal.R.dimen.notification_action_list_height); } // Transition between heads-up & expanded, or pinned. if (mHeadsUpChild != null && mExpandedChild != null) { boolean transitioningBetweenHunAndExpanded = isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) || - isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); + isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED) && (mIsHeadsUp || mHeadsUpAnimatingAway) && mContainingNotification.canShowHeadsUp(); @@ -758,7 +758,7 @@ public class NotificationContentView extends FrameLayout implements Notification } else if (mContractedChild != null) { hint = getViewHeight(VISIBLE_TYPE_CONTRACTED) + mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.notification_action_list_height); + com.android.internal.R.dimen.notification_action_list_height); } else { hint = getMinHeight(); } @@ -1053,8 +1053,8 @@ public class NotificationContentView extends FrameLayout implements Notification // the original type final int visibleType = ( isGroupExpanded() || mContainingNotification.isUserLocked()) - ? calculateVisibleType() - : getVisibleType(); + ? calculateVisibleType() + : getVisibleType(); return getBackgroundColor(visibleType); } @@ -1240,7 +1240,14 @@ public class NotificationContentView extends FrameLayout implements Notification height = mContentHeight; } int expandedVisualType = getVisualTypeForHeight(height); - int collapsedVisualType = mIsChildInGroup && !isGroupExpanded() + final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded(); + final boolean isSingleLineViewPresent = mSingleLineView != null; + + if (shouldShowSingleLineView && !isSingleLineViewPresent) { + Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!"); + } + + final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent ? VISIBLE_TYPE_SINGLELINE : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight()); return mTransformationStartVisibleType == collapsedVisualType @@ -1261,7 +1268,13 @@ public class NotificationContentView extends FrameLayout implements Notification if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) { return VISIBLE_TYPE_EXPANDED; } - if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) { + final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded(); + final boolean isSingleLinePresent = mSingleLineView != null; + if (shouldShowSingleLineView && !isSingleLinePresent) { + Log.wtf(TAG, "getVisualTypeForHeight: singleLineView is not available."); + } + + if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) { return VISIBLE_TYPE_SINGLELINE; } @@ -1276,7 +1289,7 @@ public class NotificationContentView extends FrameLayout implements Notification if (noExpandedChild || (mContractedChild != null && viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED) && (!mIsChildInGroup || isGroupExpanded() - || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { + || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { return VISIBLE_TYPE_CONTRACTED; } else if (!noExpandedChild) { return VISIBLE_TYPE_EXPANDED; @@ -1687,7 +1700,7 @@ public class NotificationContentView extends FrameLayout implements Notification : smartReplies.fromAssistant; boolean editBeforeSending = smartReplies != null && mSmartReplyConstants.getEffectiveEditChoicesBeforeSending( - smartReplies.remoteInput.getEditChoicesBeforeSending()); + smartReplies.remoteInput.getEditChoicesBeforeSending()); mSmartReplyController.smartSuggestionsAdded(mNotificationEntry, numSmartReplies, numSmartActions, fromAssistant, editBeforeSending); @@ -2114,8 +2127,8 @@ public class NotificationContentView extends FrameLayout implements Notification public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded); if (mUserExpanding) { - needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded, - bottomRounded); + needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded, + bottomRounded); } return needsPaddings; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 509f836ffb8d..b1c145e08777 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -1292,6 +1292,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view synchronously", @@ -1317,6 +1318,7 @@ constructor( runningInflations, InflationException(invalidReason), row, + entry, callback, logger, "applied invalid view", @@ -1376,6 +1378,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view", @@ -1479,6 +1482,7 @@ constructor( runningInflations: HashMap<Int, CancellationSignal>, e: Exception, notification: ExpandableNotificationRow?, + entry: NotificationEntry, callback: InflationCallback?, logger: NotificationRowContentBinderLogger, logContext: String, @@ -1486,7 +1490,7 @@ constructor( Assert.isMainThread() logger.logAsyncTaskException(notification?.loggingKey, logContext, e) runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() }) - callback?.handleInflationException(notification?.entry, e) + callback?.handleInflationException(entry, e) } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index c2c271bd6e81..e6bb1b9f0273 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -64,7 +64,6 @@ import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; -import android.util.Pair; import android.view.DisplayCutout; import android.view.InputDevice; import android.view.LayoutInflater; @@ -141,7 +140,6 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.policy.ScrollAdapter; import com.android.systemui.statusbar.policy.SplitShadeStateController; -import com.android.systemui.statusbar.ui.SystemBarUtilsProxy; import com.android.systemui.util.Assert; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.DumpUtilsKt; @@ -6004,6 +6002,7 @@ public class NotificationStackScrollLayout * LockscreenShadeTransitionController resets fraction to 0 * where it remains until the next lockscreen-to-shade transition. */ + @Override public void setFractionToShade(float fraction) { mAmbientState.setFractionToShade(fraction); updateContentHeight(); // Recompute stack height with different section gap. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index bb3abc1fba38..f7f8acf5fda9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1735,6 +1735,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { * they remain until the next lockscreen-to-shade transition. */ public void setTransitionToFullShadeAmount(float fraction) { + SceneContainerFlag.assertInLegacyMode(); mView.setFractionToShade(fraction); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 9c855e9cd9b7..ac89f3a63fcd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -107,6 +107,9 @@ interface NotificationScrollView { /** sets the current expand fraction */ fun setExpandFraction(expandFraction: Float) + /** Sets the fraction of the LockScreen -> Shade transition. */ + fun setFractionToShade(fraction: Float) + /** sets the current QS expand fraction */ fun setQsExpandFraction(expandFraction: Float) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 653344ae9203..40739b386d20 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -95,6 +95,11 @@ constructor( view.setExpandFraction(it.coerceIn(0f, 1f)) } } + launch { + viewModel.lockScreenToShadeTransitionProgress.collectTraced { + view.setFractionToShade(it.coerceIn(0f, 1f)) + } + } launch { viewModel.qsExpandFraction.collectTraced { view.setQsExpandFraction(it) } } launch { viewModel.blurRadius(maxBlurRadius).collect(view::setBlurRadius) } launch { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index c1aa5f12aa99..940b2e541758 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -207,6 +207,44 @@ constructor( val qsExpandFraction: Flow<Float> = shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction") + /** + * Fraction of the LockScreen -> Shade transition. 0..1 while the transition in progress, and + * snaps back to 0 when it is Idle. + */ + val lockScreenToShadeTransitionProgress: Flow<Float> = + combine( + shadeInteractor.shadeExpansion, + shadeModeInteractor.shadeMode, + sceneInteractor.transitionState, + ) { shadeExpansion, _, transitionState -> + when (transitionState) { + is Idle -> 0f + is ChangeScene -> + if ( + transitionState.isTransitioning( + from = Scenes.Lockscreen, + to = Scenes.Shade, + ) + ) { + shadeExpansion + } else { + 0f + } + + is Transition.OverlayTransition -> + if ( + transitionState.currentScene == Scenes.Lockscreen && + transitionState.isTransitioning(to = Overlays.NotificationsShade) + ) { + shadeExpansion + } else { + 0f + } + } + } + .distinctUntilChanged() + .dumpWhileCollecting("lockScreenToShadeTransitionProgress") + val isOccluded: Flow<Boolean> = bouncerInteractor.bouncerExpansion .map { it == 1f } diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt index 7dc6fa8e9f70..c11e4c507914 100644 --- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt @@ -25,8 +25,10 @@ import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -37,7 +39,9 @@ class TopLevelWindowEffects @Inject constructor( @Application private val context: Context, @Application private val applicationScope: CoroutineScope, private val windowManager: ViewCaptureAwareWindowManager, - private val squeezeEffectInteractor: SqueezeEffectInteractor + private val squeezeEffectInteractor: SqueezeEffectInteractor, + private val keyEventInteractor: KeyEventInteractor, + private val viewModelFactory: SqueezeEffectViewModel.Factory ) : CoreStartable { override fun start() { @@ -45,12 +49,25 @@ class TopLevelWindowEffects @Inject constructor( var root: EffectsWindowRoot? = null squeezeEffectInteractor.isSqueezeEffectEnabled.collectLatest { enabled -> // TODO: move window ops to a separate UI thread - if (enabled && root == null) { - root = EffectsWindowRoot(context) - root?.let { windowManager.addView(it, getWindowManagerLayoutParams()) } - } else if (root?.isAttachedToWindow == true) { - windowManager.removeView(root) - root = null + if (enabled) { + keyEventInteractor.isPowerButtonDown.collectLatest { down -> + // TODO: ignore new window creation when ignoring short power press duration + if (down && root == null) { + root = EffectsWindowRoot( + context = context, + viewModelFactory = viewModelFactory, + onEffectFinished = { + if (root?.isAttachedToWindow == true) { + windowManager.removeView(root) + root = null + } + } + ) + root?.let { + windowManager.addView(it, getWindowManagerLayoutParams()) + } + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt index 0826917c5cb5..61448f45c0e2 100644 --- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt @@ -16,22 +16,19 @@ package com.android.systemui.topwindoweffects.ui.compose +import android.annotation.SuppressLint import android.content.Context -import android.util.AttributeSet import androidx.compose.runtime.Composable import androidx.compose.ui.platform.AbstractComposeView import com.android.systemui.compose.ComposeInitializer +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel -class EffectsWindowRoot : AbstractComposeView { - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor( - context: Context, - attrs: AttributeSet, - defStyleAttr: Int, - ) : super(context, attrs, defStyleAttr) +@SuppressLint("ViewConstructor") +class EffectsWindowRoot( + context: Context, + private val onEffectFinished: () -> Unit, + private val viewModelFactory: SqueezeEffectViewModel.Factory +) : AbstractComposeView(context) { override fun onAttachedToWindow() { ComposeInitializer.onAttachedToWindow(this) @@ -45,6 +42,9 @@ class EffectsWindowRoot : AbstractComposeView { @Composable override fun Content() { - SqueezeEffect() + SqueezeEffect( + viewModelFactory = viewModelFactory, + onEffectFinished = onEffectFinished + ) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt index 124724000aaf..9809b0592dcf 100644 --- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt @@ -16,9 +16,124 @@ package com.android.systemui.topwindoweffects.ui.compose +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.res.R +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel + +private val SqueezeEffectMaxThickness = 12.dp +private val SqueezeColor = Color.Black @Composable -fun SqueezeEffect() { - // TODO: add squeeze effect +fun SqueezeEffect( + viewModelFactory: SqueezeEffectViewModel.Factory, + onEffectFinished: () -> Unit, + modifier: Modifier = Modifier +) { + val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() } + val down = viewModel.isPowerButtonPressed + val longPressed = viewModel.isPowerButtonLongPressed + // TODO: Choose the correct resource based on primary / secondary display + val top = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_top)) + val bottom = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_bottom)) + + val squeezeProgress by animateFloatAsState( + targetValue = + if (down && !longPressed) { + 1f + } else { + 0f + }, + animationSpec = tween(durationMillis = 400), + finishedListener = { onEffectFinished() } + ) + + Canvas(modifier = modifier.fillMaxSize()) { + if (squeezeProgress <= 0) { + return@Canvas + } + + val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress + + drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness)) + + drawRect( + color = SqueezeColor, + topLeft = Offset(0f, size.height - squeezeThickness), + size = Size(size.width, squeezeThickness) + ) + + drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height)) + + drawRect( + color = SqueezeColor, + topLeft = Offset(size.width - squeezeThickness, 0f), + size = Size(squeezeThickness, size.height) + ) + + drawTransform( + dx = squeezeThickness, + dy = squeezeThickness, + rotation = 0f, + corner = top + ) + + drawTransform( + dx = size.width - squeezeThickness, + dy = squeezeThickness, + rotation = 90f, + corner = top + ) + + drawTransform( + dx = squeezeThickness, + dy = size.height - squeezeThickness, + rotation = 270f, + corner = bottom + ) + + drawTransform( + dx = size.width - squeezeThickness, + dy = size.height - squeezeThickness, + rotation = 180f, + corner = bottom + ) + } +} + +private fun DrawScope.drawTransform( + dx: Float, + dy: Float, + rotation: Float = 0f, + corner: VectorPainter, +) { + withTransform(transformBlock = { + transform(matrix = Matrix().apply { + translate(dx, dy) + if (rotation != 0f) { + rotateZ(rotation) + } + }) + }) { + with(corner) { + draw(size = intrinsicSize) + } + } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt new file mode 100644 index 000000000000..1cab327740ce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.topwindoweffects.ui.viewmodel + +import androidx.compose.runtime.getValue +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class SqueezeEffectViewModel +@AssistedInject +constructor( + keyEventInteractor: KeyEventInteractor +) : ExclusiveActivatable() { + private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator") + + val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( + traceName = "isPowerButtonPressed", + initialValue = false, + source = keyEventInteractor.isPowerButtonDown + ) + + val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( + traceName = "isPowerButtonLongPressed", + initialValue = false, + source = keyEventInteractor.isPowerButtonLongPressed + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): SqueezeEffectViewModel + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt index 3bd8af690763..6657c428a594 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt @@ -20,6 +20,7 @@ import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -31,7 +32,14 @@ class UserLockedInteractor constructor( @Background val backgroundDispatcher: CoroutineDispatcher, val userRepository: UserRepository, + val selectedUserInteractor: SelectedUserInteractor, ) { + /** Whether the current user is unlocked */ + val currentUserUnlocked: Flow<Boolean> = + selectedUserInteractor.selectedUserInfo.flatMapLatestConflated { user -> + isUserUnlocked(user.userHandle) + } + fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = userRepository.isUserUnlocked(userHandle).flowOn(backgroundDispatcher) } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt index 94b3fd244a92..6cdc94251d21 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt @@ -47,6 +47,11 @@ abstract class SettingsForUserRepository( .distinctUntilChanged() .flowOn(backgroundDispatcher) + fun intSettingForUser(userId: Int, name: String, defaultValue: Int = 0): Flow<Int> = + settingObserver(name, userId) { userSettings.getIntForUser(name, defaultValue, userId) } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> { return userSettings .observerFlow(userId, name) @@ -63,4 +68,14 @@ abstract class SettingsForUserRepository( userSettings.getBoolForUser(name, defaultValue, userId) } } + + suspend fun setIntForUser(userId: Int, name: String, value: Int) { + withContext(backgroundContext) { userSettings.putIntForUser(name, value, userId) } + } + + suspend fun getIntForUser(userId: Int, name: String, defaultValue: Int = 0): Int { + return withContext(backgroundContext) { + userSettings.getIntForUser(name, defaultValue, userId) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 845be0252581..60345a358bac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -24,12 +24,15 @@ import android.widget.LinearLayout import android.window.RemoteTransition import android.window.TransitionFilter import android.window.WindowAnimationState +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.activity.EmptyTestActivity +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope -import com.android.systemui.shared.Flags +import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.wm.shell.shared.ShellTransitions @@ -43,7 +46,6 @@ import kotlin.concurrent.thread import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -57,8 +59,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.Spy import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.spy @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -66,21 +68,23 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper class ActivityTransitionAnimatorTest : SysuiTestCase() { private val kosmos = testKosmos() - private val transitionContainer = LinearLayout(mContext) + private val mainExecutor = context.mainExecutor private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor) private val testShellTransitions = FakeShellTransitions() + + private val Kosmos.underTest by Kosmos.Fixture { activityTransitionAnimator } + @Mock lateinit var callback: ActivityTransitionAnimator.Callback @Mock lateinit var listener: ActivityTransitionAnimator.Listener - @Spy private val controller = TestTransitionAnimatorController(transitionContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback - private lateinit var underTest: ActivityTransitionAnimator - @get:Rule val rule = MockitoJUnit.rule() + @get:Rule(order = 0) val mockitoRule = MockitoJUnit.rule() + @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java) @Before fun setup() { - underTest = + kosmos.activityTransitionAnimator = ActivityTransitionAnimator( mainExecutor, ActivityTransitionAnimator.TransitionRegister.fromShellTransitions( @@ -89,19 +93,20 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, testTransitionAnimator, disableWmTimeout = true, + skipReparentTransaction = true, ) - underTest.callback = callback - underTest.addListener(listener) + kosmos.activityTransitionAnimator.callback = callback + kosmos.activityTransitionAnimator.addListener(listener) } @After fun tearDown() { - underTest.removeListener(listener) + kosmos.activityTransitionAnimator.removeListener(listener) } private fun startIntentWithAnimation( - animator: ActivityTransitionAnimator = underTest, - controller: ActivityTransitionAnimator.Controller? = this.controller, + controller: ActivityTransitionAnimator.Controller?, + animator: ActivityTransitionAnimator = kosmos.activityTransitionAnimator, animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int, ) { @@ -119,129 +124,152 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun animationAdapterIsNullIfControllerIsNull() { - var startedIntent = false - var animationAdapter: RemoteAnimationAdapter? = null + kosmos.runTest { + var startedIntent = false + var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(controller = null) { adapter -> - startedIntent = true - animationAdapter = adapter + startIntentWithAnimation(controller = null) { adapter -> + startedIntent = true + animationAdapter = adapter - ActivityManager.START_SUCCESS - } + ActivityManager.START_SUCCESS + } - assertTrue(startedIntent) - assertNull(animationAdapter) + assertTrue(startedIntent) + assertNull(animationAdapter) + } } @Test fun animatesIfActivityOpens() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation { adapter -> - animationAdapter = adapter - ActivityManager.START_SUCCESS - } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + var animationAdapter: RemoteAnimationAdapter? = null + startIntentWithAnimation(controller) { adapter -> + animationAdapter = adapter + ActivityManager.START_SUCCESS + } - assertNotNull(animationAdapter) - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertTrue(willAnimateCaptor.value) + assertNotNull(animationAdapter) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertTrue(willAnimateCaptor.value) + } } @Test fun doesNotAnimateIfActivityIsAlreadyOpen() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + startIntentWithAnimation(controller) { ActivityManager.START_DELIVERED_TO_TOP } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertFalse(willAnimateCaptor.value) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertFalse(willAnimateCaptor.value) + } } @Test fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() { - `when`(callback.isOnKeyguard()).thenReturn(true) + kosmos.runTest { + `when`(callback.isOnKeyguard()).thenReturn(true) - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - var animationAdapter: RemoteAnimationAdapter? = null + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(underTest) { adapter -> - animationAdapter = adapter - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(controller, underTest) { adapter -> + animationAdapter = adapter + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - verify(callback).hideKeyguardWithAnimation(any()) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + verify(callback).hideKeyguardWithAnimation(any()) - assertTrue(willAnimateCaptor.value) - assertNull(animationAdapter) + assertTrue(willAnimateCaptor.value) + assertNull(animationAdapter) + } } @Test fun doesNotAnimateIfAnimateIsFalse() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + startIntentWithAnimation(controller, animate = false) { ActivityManager.START_SUCCESS } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertFalse(willAnimateCaptor.value) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertFalse(willAnimateCaptor.value) + } } - @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) @Test fun registersReturnIffCookieIsPresent() { - `when`(callback.isOnKeyguard()).thenReturn(false) + kosmos.runTest { + `when`(callback.isOnKeyguard()).thenReturn(false) - startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } + val controller = createController() + startIntentWithAnimation(controller, underTest) { + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - assertTrue(testShellTransitions.remotes.isEmpty()) - assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + waitForIdleSync() + assertTrue(testShellTransitions.remotes.isEmpty()) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) - val controller = - object : DelegateTransitionAnimatorController(controller) { - override val transitionCookie - get() = ActivityTransitionAnimator.TransitionCookie("testCookie") - } + val controllerWithCookie = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie + get() = ActivityTransitionAnimator.TransitionCookie("testCookie") + } - startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } + startIntentWithAnimation(controllerWithCookie, underTest) { + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - assertEquals(1, testShellTransitions.remotes.size) - assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + waitForIdleSync() + assertEquals(1, testShellTransitions.remotes.size) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun registersLongLivedTransition() { kosmos.runTest { - var factory = controllerFactory() + val controller = createController() + var factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(2, testShellTransitions.remotes.size) - factory = controllerFactory() + factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(4, testShellTransitions.remotes.size) } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun registersLongLivedTransitionOverridingPreviousRegistration() { kosmos.runTest { + val controller = createController() val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") - var factory = controllerFactory(cookie) + var factory = controllerFactory(controller, cookie) underTest.register(cookie, factory, testScope) val transitions = testShellTransitions.remotes.values.toList() - factory = controllerFactory(cookie) + factory = controllerFactory(controller, cookie) underTest.register(cookie, factory, testScope) assertEquals(2, testShellTransitions.remotes.size) for (transition in transitions) { @@ -250,23 +278,25 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } - @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + @DisableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { kosmos.runTest { - val factory = controllerFactory(component = null) + val factory = controllerFactory(createController(), component = null) assertThrows(IllegalStateException::class.java) { underTest.register(factory.cookie, factory, testScope) } } } - @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { kosmos.runTest { + val controller = createController() + // No ComponentName - var factory = controllerFactory(component = null) + var factory = controllerFactory(controller, component = null) assertThrows(IllegalStateException::class.java) { underTest.register(factory.cookie, factory, testScope) } @@ -280,7 +310,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, disableWmTimeout = true, ) - factory = controllerFactory() + factory = controllerFactory(controller) assertThrows(IllegalStateException::class.java) { activityTransitionAnimator.register(factory.cookie, factory, testScope) } @@ -288,17 +318,18 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun unregistersLongLivedTransition() { kosmos.runTest { + val controller = createController() val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) for (index in 0 until 3) { cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val factory = controllerFactory(cookies[index]!!) + val factory = controllerFactory(controller, cookies[index]!!) underTest.register(factory.cookie, factory, testScope) } @@ -315,75 +346,98 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun doesNotStartIfAnimationIsCancelled() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationCancelled() - runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationCancelled() + runner.onAnimationStart( + TRANSIT_NONE, + emptyArray(), + emptyArray(), + emptyArray(), + iCallback, + ) - waitForIdleSync() - verify(controller).onTransitionAnimationCancelled() - verify(controller, never()).onTransitionAnimationStart(anyBoolean()) - verify(listener).onTransitionAnimationCancelled() - verify(listener, never()).onTransitionAnimationStart() - assertNull(runner.delegate) + waitForIdleSync() + verify(controller).onTransitionAnimationCancelled() + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) + verify(listener).onTransitionAnimationCancelled() + verify(listener, never()).onTransitionAnimationStart() + assertNull(runner.delegate) + } } @Test fun cancelsIfNoOpeningWindowIsFound() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + emptyArray(), + emptyArray(), + emptyArray(), + iCallback, + ) - waitForIdleSync() - verify(controller).onTransitionAnimationCancelled() - verify(controller, never()).onTransitionAnimationStart(anyBoolean()) - verify(listener).onTransitionAnimationCancelled() - verify(listener, never()).onTransitionAnimationStart() - assertNull(runner.delegate) + waitForIdleSync() + verify(controller).onTransitionAnimationCancelled() + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) + verify(listener).onTransitionAnimationCancelled() + verify(listener, never()).onTransitionAnimationStart() + assertNull(runner.delegate) + } } @Test fun startsAnimationIfWindowIsOpening() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) - waitForIdleSync() - verify(listener).onTransitionAnimationStart() - verify(controller).onTransitionAnimationStart(anyBoolean()) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + verify(controller).onTransitionAnimationStart(anyBoolean()) + } } @Test fun creatingControllerFromNormalViewThrows() { - assertThrows(IllegalArgumentException::class.java) { - ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext)) + kosmos.runTest { + assertThrows(IllegalArgumentException::class.java) { + ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext)) + } } } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() { kosmos.runTest { assertThrows(IllegalStateException::class.java) { - val factory = controllerFactory() + val factory = controllerFactory(createController()) underTest.createLongLivedRunner(factory, testScope, forLaunch = true) } } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun runnerCreatesDelegateLazily_onAnimationStart() { kosmos.runTest { - val factory = controllerFactory() + val factory = controllerFactory(createController()) val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true) assertNull(runner.delegate) @@ -412,13 +466,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun runnerCreatesDelegateLazily_onAnimationTakeover() { kosmos.runTest { - val factory = controllerFactory() + val factory = controllerFactory(createController()) val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false) assertNull(runner.delegate) @@ -446,58 +500,78 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun animationTakeoverThrows_whenTheFlagsAreDisabled() { - val runner = underTest.createEphemeralRunner(controller) - assertThrows(IllegalStateException::class.java) { - runner.takeOverAnimation( - arrayOf(fakeWindow()), - emptyArray(), - SurfaceControl.Transaction(), - iCallback, - ) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + assertThrows(IllegalStateException::class.java) { + runner.takeOverAnimation( + arrayOf(fakeWindow()), + emptyArray(), + SurfaceControl.Transaction(), + iCallback, + ) + } } } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun disposeRunner_delegateDereferenced() { - val runner = underTest.createEphemeralRunner(controller) - assertNotNull(runner.delegate) - runner.dispose() - waitForIdleSync() - assertNull(runner.delegate) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + assertNotNull(runner.delegate) + runner.dispose() + waitForIdleSync() + assertNull(runner.delegate) + } } @Test fun concurrentListenerModification_doesNotThrow() { - // Need a second listener to trigger the concurrent modification. - underTest.addListener(object : ActivityTransitionAnimator.Listener {}) - `when`(listener.onTransitionAnimationStart()).thenAnswer { - underTest.removeListener(listener) - listener - } + kosmos.runTest { + // Need a second listener to trigger the concurrent modification. + underTest.addListener(object : ActivityTransitionAnimator.Listener {}) + `when`(listener.onTransitionAnimationStart()).thenAnswer { + underTest.removeListener(listener) + listener + } - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + } + } + private fun createController(): TestTransitionAnimatorController { + lateinit var transitionContainer: ViewGroup + activityRule.scenario.onActivity { activity -> + transitionContainer = LinearLayout(activity) + activity.setContentView(transitionContainer) + } waitForIdleSync() - verify(listener).onTransitionAnimationStart() + return spy(TestTransitionAnimatorController(transitionContainer)) } private fun controllerFactory( + controller: ActivityTransitionAnimator.Controller, cookie: ActivityTransitionAnimator.TransitionCookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java), component: ComponentName? = mock(ComponentName::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index b274f1c2c6df..d9c59d37b031 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -88,11 +88,6 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { companion object { private val INTENT = Intent("some.intent.action") - private val DRAWABLE = - mock<Icon> { - whenever(this.contentDescription) - .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 @Parameters( @@ -337,7 +332,17 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { homeControls.setState( lockScreenState = - KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE) + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + mock<Icon> { + whenever(contentDescription) + .thenReturn( + ContentDescription.Resource( + res = CONTENT_DESCRIPTION_RESOURCE_ID + ) + ) + } + ) ) homeControls.onTriggeredResult = if (startActivity) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 175e8d4331a1..e048d462ef8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -33,8 +33,8 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.settingslib.bluetooth.LocalBluetoothManager @@ -77,6 +77,8 @@ import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.eq +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val KEY = "TEST_KEY" private const val KEY_OLD = "TEST_KEY_OLD" @@ -89,12 +91,24 @@ private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME" private const val NORMAL_APP_NAME = "NORMAL_APP_NAME" @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @TestableLooper.RunWithLooper -public class MediaDeviceManagerTest : SysuiTestCase() { +public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() { - private companion object { + companion object { val OTHER_DEVICE_ICON_STUB = TestStubDrawable() + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.progressionOf( + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) } @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() @@ -187,6 +201,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun loadMediaData() { manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmmFactory).create(PACKAGE) } @@ -195,6 +211,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) manager.onMediaDataRemoved(KEY, false) fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmm).unregisterCallback(any()) verify(muteAwaitManager).stopListening() } @@ -406,6 +423,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) // WHEN the notification is removed manager.onMediaDataRemoved(KEY, true) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() // THEN the listener receives key removed event verify(listener).onKeyRemoved(eq(KEY), eq(true)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt index c20a801cd5e3..a8bfbd18d0c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt @@ -103,6 +103,8 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true) whenever(wifiEntries.size).thenReturn(1) whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE) + whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean())) + .thenReturn("") whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt())) .thenReturn(MOBILE_NETWORK_TITLE) whenever( @@ -128,15 +130,13 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { internetDetailsContentController, canConfigMobileData = true, canConfigWifi = true, - coroutineScope = scope, - context = mContext, uiEventLogger = mock<UiEventLogger>(), handler = handler, backgroundExecutor = bgExecutor, keyguard = keyguard, ) - internetDetailsContentManager.bind(contentView) + internetDetailsContentManager.bind(contentView, scope) internetDetailsContentManager.adapter = internetAdapter internetDetailsContentManager.connectedWifiEntry = internetWifiEntry internetDetailsContentManager.wifiEntriesCount = wifiEntries.size @@ -777,6 +777,26 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { } } + @Test + fun updateTitleAndSubtitle() { + assertThat(internetDetailsContentManager.title).isEqualTo("Internet") + assertThat(internetDetailsContentManager.subTitle).isEqualTo("") + + whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title") + whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean())) + .thenReturn("New subtitle") + + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(internetDetailsContentManager.title).isEqualTo("New title") + assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle") + } + } + companion object { private const val TITLE = "Internet" private const val MOBILE_NETWORK_TITLE = "Mobile Title" diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index 155059ea5ed9..e0118b18ff64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -244,7 +244,7 @@ class LauncherProxyServiceTest : SysuiTestCase() { `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(true) val spyContext = spy(context) - val ops = createLauncherProxyService(spyContext) + val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java new file mode 100644 index 000000000000..c231be181977 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.ringtone; + +import static org.junit.Assert.assertThrows; + +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Binder; +import android.os.UserHandle; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RingtonePlayerTest extends SysuiTestCase { + + private AudioManager mAudioManager; + + private static final String TAG = "RingtonePlayerTest"; + + @Before + public void setup() throws Exception { + mAudioManager = getContext().getSystemService(AudioManager.class); + } + + @Test + public void testRingtonePlayerUriUserCheck() { + android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer(); + final AudioAttributes aa = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); + // get a UserId that doesn't belong to mine + final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0; + // build a URI that I shouldn't have access to + final Uri uri = new Uri.Builder() + .scheme("content").authority(otherUserId + "@media") + .appendPath("external").appendPath("downloads") + .appendPath("bogusPathThatDoesNotMatter.mp3") + .build(); + if (android.media.audio.Flags.ringtoneUserUriCheck()) { + assertThrows(SecurityException.class, () -> + irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/) + ); + + assertThrows(SecurityException.class, () -> + irp.getTitle(uri)); + } + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 51bb38fa5ba9..f72645eb8596 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.assertLogsWtf import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootViewKeyEventHandler @@ -413,7 +414,9 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : // THEN move is ignored, down is handled, and window is notified assertThat(interactionEventHandler.handleDispatchTouchEvent(MOVE_EVENT)).isFalse() - assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + assertLogsWtf { + assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + } verify(notificationShadeWindowController).setLaunchingActivity(false) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 4315c0f638ac..84f39be2eeed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES; +import static com.android.systemui.log.LogAssertKt.assertRunnableLogsWtf; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE; @@ -1147,7 +1148,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void hasStatusBarChipDuringHeadsUpAnimation_flagOff_false() throws Exception { final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setHasStatusBarChipDuringHeadsUpAnimation(true); + assertRunnableLogsWtf(() -> row.setHasStatusBarChipDuringHeadsUpAnimation(true)); assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java index 1cadb3c0a909..e1bab8ec47e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java @@ -46,6 +46,7 @@ import androidx.test.filters.SmallTest; import com.android.server.notification.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.log.LogAssertKt; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -173,8 +174,10 @@ public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase { public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() { NotificationEntry entry = new NotificationEntryBuilder().build(); View view = new FrameLayout(mContext); - assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) - .isTrue(); + LogAssertKt.assertRunnableLogsWtf(() -> { + assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) + .isTrue(); + }); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index a978ecdb3534..0d7ce5353cd4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -204,8 +204,6 @@ import com.android.wm.shell.taskview.TaskViewRepository; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; -import kotlin.Lazy; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -216,9 +214,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -227,6 +222,10 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; +import kotlin.Lazy; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -602,14 +601,19 @@ public class BubblesTest extends SysuiTestCase { // Get a reference to KeyguardStateController.Callback verify(mKeyguardStateController, atLeastOnce()) .addCallback(mKeyguardStateControllerCallbackCaptor.capture()); + + // Make sure mocks are set up for current user + switchUser(ActivityManager.getCurrentUser()); } @After public void tearDown() throws Exception { - ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); - for (int i = 0; i < bubbles.size(); i++) { - mBubbleController.removeBubble(bubbles.get(i).getKey(), - Bubbles.DISMISS_NO_LONGER_BUBBLE); + if (mBubbleData != null) { + ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); + for (int i = 0; i < bubbles.size(); i++) { + mBubbleController.removeBubble(bubbles.get(i).getKey(), + Bubbles.DISMISS_NO_LONGER_BUBBLE); + } } mTestableLooper.processAllMessages(); @@ -2051,6 +2055,9 @@ public class BubblesTest extends SysuiTestCase { @Test public void testShowStackEdu_isConversationBubble() { + // TODO(b/401025577): Prevent this test from raising a WTF, and remove this exemption + mLogWtfRule.addFailureLogExemption(log-> log.getTag().equals("FloatingCoordinator")); + // Setup setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index 252c70a61b86..e550e88b7bc7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -46,6 +46,7 @@ import androidx.test.uiautomator.UiDevice; import com.android.internal.protolog.ProtoLog; import com.android.systemui.broadcast.FakeBroadcastDispatcher; import com.android.systemui.flags.SceneContainerRule; +import com.android.systemui.log.LogWtfHandlerRule; import org.junit.After; import org.junit.AfterClass; @@ -127,6 +128,8 @@ public abstract class SysuiTestCase { @Rule public final SetFlagsRule mSetFlagsRule = isRobolectricTest() ? new SetFlagsRule() : mSetFlagsClassRule.createSetFlagsRule(); + @Rule public final LogWtfHandlerRule mLogWtfRule = new LogWtfHandlerRule(); + @Rule(order = 10) public final SceneContainerRule mSceneContainerRule = new SceneContainerRule(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt new file mode 100644 index 000000000000..130c2987912e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.carProjectionRepository by + Kosmos.Fixture<CarProjectionRepository> { FakeCarProjectionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt new file mode 100644 index 000000000000..4042342923ea --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeCarProjectionRepository : CarProjectionRepository { + private val _projectionActive = MutableStateFlow(false) + override val projectionActive: Flow<Boolean> = _projectionActive.asStateFlow() + + override suspend fun isProjectionActive(): Boolean { + return _projectionActive.value + } + + fun setProjectionActive(active: Boolean) { + _projectionActive.value = active + } +} + +val CarProjectionRepository.fake + get() = this as FakeCarProjectionRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt new file mode 100644 index 000000000000..23bbe36203e6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.repository.carProjectionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.carProjectionInteractor by Fixture { CarProjectionInteractor(carProjectionRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt new file mode 100644 index 000000000000..5735cf82cca0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.common.domain.interactor.batteryInteractor +import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor +import com.android.systemui.dock.dockManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.backgroundCoroutineContext + +val Kosmos.communalAutoOpenInteractor by Fixture { + CommunalAutoOpenInteractor( + communalSettingsInteractor = communalSettingsInteractor, + backgroundContext = backgroundCoroutineContext, + batteryInteractor = batteryInteractor, + posturingInteractor = posturingInteractor, + dockManager = dockManager, + allowSwipeAlways = false, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index b8b2ec5a58ae..316fcbb85b26 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -16,17 +16,14 @@ package com.android.systemui.communal.domain.interactor -import android.content.pm.UserInfo import android.content.testableContext import android.os.userManager import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.common.domain.interactor.batteryInteractor +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.communalMediaRepository import com.android.systemui.communal.data.repository.communalSmartspaceRepository import com.android.systemui.communal.data.repository.communalWidgetRepository -import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.dock.dockManager import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -39,13 +36,9 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter -import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -70,10 +63,6 @@ val Kosmos.communalInteractor by Fixture { logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), managedProfileController = fakeManagedProfileController, - batteryInteractor = batteryInteractor, - dockManager = dockManager, - posturingInteractor = posturingInteractor, - userLockedInteractor = userLockedInteractor, ) } @@ -86,28 +75,28 @@ fun Kosmos.setCommunalV2ConfigEnabled(enabled: Boolean) { ) } -suspend fun Kosmos.setCommunalEnabled(enabled: Boolean): UserInfo { +fun Kosmos.setCommunalEnabled(enabled: Boolean) { fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled) - return if (enabled) { - fakeUserRepository.asMainUser() - } else { - fakeUserRepository.asDefaultUser() - } + val suppressionReasons = + if (enabled) { + emptyList() + } else { + listOf(SuppressionReason.ReasonUnknown()) + } + communalSettingsInteractor.setSuppressionReasons(suppressionReasons) } -suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean): UserInfo { +fun Kosmos.setCommunalV2Enabled(enabled: Boolean) { setCommunalV2ConfigEnabled(enabled) return setCommunalEnabled(enabled) } -suspend fun Kosmos.setCommunalAvailable(available: Boolean): UserInfo { - val user = setCommunalEnabled(available) +fun Kosmos.setCommunalAvailable(available: Boolean) { + setCommunalEnabled(available) fakeKeyguardRepository.setKeyguardShowing(available) - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available) - return user } -suspend fun Kosmos.setCommunalV2Available(available: Boolean): UserInfo { +fun Kosmos.setCommunalV2Available(available: Boolean) { setCommunalV2ConfigEnabled(available) - return setCommunalAvailable(available) + setCommunalAvailable(available) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt index fb983f7c605f..d2fbb515e686 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt @@ -24,7 +24,6 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.settings.userTracker import com.android.systemui.user.domain.interactor.selectedUserInteractor -import com.android.systemui.util.mockito.mock val Kosmos.communalSettingsInteractor by Fixture { CommunalSettingsInteractor( @@ -34,6 +33,5 @@ val Kosmos.communalSettingsInteractor by Fixture { repository = communalSettingsRepository, userInteractor = selectedUserInteractor, userTracker = userTracker, - tableLogBuffer = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java deleted file mode 100644 index b99310bcbe38..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.dock; - -/** - * A rudimentary fake for DockManager. - */ -public class DockManagerFake implements DockManager { - DockEventListener mCallback; - AlignmentStateListener mAlignmentListener; - private boolean mDocked; - - @Override - public void addListener(DockEventListener callback) { - this.mCallback = callback; - } - - @Override - public void removeListener(DockEventListener callback) { - this.mCallback = null; - } - - @Override - public void addAlignmentStateListener(AlignmentStateListener listener) { - mAlignmentListener = listener; - } - - @Override - public void removeAlignmentStateListener(AlignmentStateListener listener) { - mAlignmentListener = listener; - } - - @Override - public boolean isDocked() { - return mDocked; - } - - /** Sets the docked state */ - public void setIsDocked(boolean docked) { - mDocked = docked; - } - - @Override - public boolean isHidden() { - return false; - } - - /** Notifies callbacks of dock state change */ - public void setDockEvent(int event) { - mCallback.onEvent(event); - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt new file mode 100644 index 000000000000..6a43c40612a7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.dock + +import com.android.systemui.dock.DockManager.AlignmentStateListener + +/** A rudimentary fake for DockManager. */ +class DockManagerFake : DockManager { + private val callbacks = mutableSetOf<DockManager.DockEventListener>() + private val alignmentListeners = mutableSetOf<AlignmentStateListener>() + private var docked = false + + override fun addListener(callback: DockManager.DockEventListener) { + callbacks.add(callback) + } + + override fun removeListener(callback: DockManager.DockEventListener) { + callbacks.remove(callback) + } + + override fun addAlignmentStateListener(listener: AlignmentStateListener) { + alignmentListeners.add(listener) + } + + override fun removeAlignmentStateListener(listener: AlignmentStateListener) { + alignmentListeners.remove(listener) + } + + override fun isDocked(): Boolean { + return docked + } + + /** Sets the docked state */ + fun setIsDocked(docked: Boolean) { + this.docked = docked + } + + override fun isHidden(): Boolean { + return false + } + + /** Notifies callbacks of dock state change */ + fun setDockEvent(event: Int) { + for (callback in callbacks) { + callback.onEvent(event) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt index bdfa875f5429..9b0a9830dcc4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyguard.domain.interactor -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository @@ -43,6 +42,5 @@ val Kosmos.fromAodTransitionInteractor by wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, communalSettingsInteractor = communalSettingsInteractor, communalSceneInteractor = communalSceneInteractor, - communalInteractor = communalInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index 985044c80f18..511bede7349b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyguard.domain.interactor -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository @@ -42,7 +41,6 @@ var Kosmos.fromLockscreenTransitionInteractor by communalSettingsInteractor = communalSettingsInteractor, swipeToDismissInteractor = swipeToDismissInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, - communalInteractor = communalInteractor, communalSceneInteractor = communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt index 255a780a84be..113059222aa2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.applicationContext +import android.os.powerManager import android.view.accessibility.accessibilityManagerWrapper import com.android.internal.logging.uiEventLogger import com.android.systemui.broadcast.broadcastDispatcher @@ -26,6 +27,8 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.pulsingGestureListener +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository +import com.android.systemui.util.time.fakeSystemClock val Kosmos.keyguardTouchHandlingInteractor by Kosmos.Fixture { @@ -40,5 +43,8 @@ val Kosmos.keyguardTouchHandlingInteractor by accessibilityManager = accessibilityManagerWrapper, pulsingGestureListener = pulsingGestureListener, faceAuthInteractor = deviceEntryFaceAuthInteractor, + secureSettingsRepository = userAwareSecureSettingsRepository, + powerManager = powerManager, + systemClock = fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt index b41ceff5f581..a42f2025cdaa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt @@ -18,7 +18,6 @@ package com.android.systemui.log import android.util.Log import android.util.Log.TerribleFailureHandler import com.google.common.truth.Truth.assertWithMessage -import java.util.concurrent.Callable /** Asserts that [notLoggingBlock] does not make a call to [Log.wtf] */ fun <T> assertDoesNotLogWtf( @@ -65,15 +64,6 @@ fun <T> assertLogsWtf( return WtfBlockResult(caught, result) } -/** Assert that [loggingCallable] makes a call to [Log.wtf] */ -@JvmOverloads -fun <T> assertLogsWtf( - message: String = "Expected Log.wtf to be called", - allowMultiple: Boolean = false, - loggingCallable: Callable<T>, -): WtfBlockResult<T> = - assertLogsWtf(message = message, allowMultiple = allowMultiple, loggingCallable::call) - /** Assert that [loggingBlock] makes at least one call to [Log.wtf] */ @JvmOverloads fun <T> assertLogsWtfs( @@ -81,13 +71,6 @@ fun <T> assertLogsWtfs( loggingBlock: () -> T, ): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingBlock) -/** Assert that [loggingCallable] makes at least one call to [Log.wtf] */ -@JvmOverloads -fun <T> assertLogsWtfs( - message: String = "Expected Log.wtf to be called once or more", - loggingCallable: Callable<T>, -): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingCallable) - /** The data passed to [TerribleFailureHandler.onTerribleFailure] */ data class TerribleFailureLog( val tag: String, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt index 4f8d5a14e390..9457de18b3b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt @@ -18,18 +18,14 @@ package com.android.systemui.qs import com.android.systemui.plugins.qs.TileDetailsViewModel -class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() { +class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel { private var _clickOnSettingsButton = 0 override fun clickOnSettingsButton() { _clickOnSettingsButton++ } - override fun getTitle(): String { - return tileSpec ?: " Fake title" - } + override val title = tileSpec ?: " Fake title" - override fun getSubTitle(): String { - return tileSpec ?: "Fake sub title" - } + override val subTitle = tileSpec ?: "Fake sub title" } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt index 75ca311689ce..4aa4a2b1a73a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import android.content.applicationContext import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory import com.android.systemui.globalactions.globalActionsDialogLite import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.footerActionsInteractor @@ -29,6 +30,7 @@ val Kosmos.toolbarViewModelFactory by override fun create(): ToolbarViewModel { return ToolbarViewModel( editModeButtonViewModelFactory, + buildNumberViewModelFactory, footerActionsInteractor, { globalActionsDialogLite }, falsingInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt index e99f61e7cd13..067e420b89c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt @@ -25,12 +25,13 @@ import com.android.systemui.statusbar.notification.people.peopleNotificationIden import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider val Kosmos.entryAdapterFactory by -Kosmos.Fixture { - EntryAdapterFactoryImpl( - mockNotificationActivityStarter, - metricsLogger, - peopleNotificationIdentifier, - notificationIconStyleProvider, - visualStabilityCoordinator, - ) -}
\ No newline at end of file + Kosmos.Fixture { + EntryAdapterFactoryImpl( + mockNotificationActivityStarter, + metricsLogger, + peopleNotificationIdentifier, + notificationIconStyleProvider, + visualStabilityCoordinator, + mockNotificationActionClickManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 7f012ba25ab9..6a674ca29ca4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -375,6 +375,7 @@ class ExpandableNotificationRowBuilder( Mockito.mock(PeopleNotificationIdentifier::class.java), Mockito.mock(NotificationIconStyleProvider::class.java), Mockito.mock(VisualStabilityCoordinator::class.java), + Mockito.mock(NotificationActionClickManager::class.java), ) .create(entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt new file mode 100644 index 000000000000..8e62ae8825f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +var Kosmos.mockNotificationActionClickManager: NotificationActionClickManager by + Kosmos.Fixture { mock<NotificationActionClickManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index 08cfd9f385d3..3e96fd7c729f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model import android.app.PendingIntent +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -79,8 +81,10 @@ object OngoingCallTestHelper { contentIntent: PendingIntent? = null, uid: Int = DEFAULT_UID, appName: String = "Fake name", + isAppVisible: Boolean = false, ) { if (StatusBarChipsModernization.isEnabled) { + activityManagerRepository.fake.startingIsAppVisibleValue = isAppVisible activeNotificationListRepository.addNotif( activeNotificationModel( key = key, @@ -102,6 +106,7 @@ object OngoingCallTestHelper { notificationKey = key, appName = appName, promotedContent = promotedContent, + isAppVisible = isAppVisible, ) ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt index 933c351679a4..6bb908a6ef07 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt @@ -22,5 +22,9 @@ import com.android.systemui.user.data.repository.userRepository val Kosmos.userLockedInteractor by Kosmos.Fixture { - UserLockedInteractor(backgroundDispatcher = testDispatcher, userRepository = userRepository) + UserLockedInteractor( + backgroundDispatcher = testDispatcher, + userRepository = userRepository, + selectedUserInteractor = selectedUserInteractor, + ) } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index ccbc46fdb03b..5424ac3bd897 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -16,7 +16,7 @@ filegroup { srcs: [ "texts/ravenwood-common-policies.txt", ], - visibility: ["//visibility:private"], + visibility: [":__subpackages__"], } filegroup { @@ -44,6 +44,22 @@ filegroup { } filegroup { + name: "ravenwood-standard-annotations", + srcs: [ + "texts/ravenwood-standard-annotations.txt", + ], + visibility: [":__subpackages__"], +} + +filegroup { + name: "ravenizer-standard-options", + srcs: [ + "texts/ravenizer-standard-options.txt", + ], + visibility: [":__subpackages__"], +} + +filegroup { name: "ravenwood-annotation-allowed-classes", srcs: [ "texts/ravenwood-annotation-allowed-classes.txt", diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp index e36677189e02..f5b075b17fd4 100644 --- a/ravenwood/Framework.bp +++ b/ravenwood/Framework.bp @@ -33,6 +33,7 @@ genrule_defaults { ":ravenwood-common-policies", ":ravenwood-framework-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -44,6 +45,7 @@ genrule_defaults { framework_minus_apex_cmd = "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_framework-minus-apex.log) " + "--out-jar $(location ravenwood.jar) " + "--in-jar $(location :framework-minus-apex-for-host) " + @@ -178,6 +180,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_services.core.log) " + "--stats-file $(location hoststubgen_services.core_stats.csv) " + @@ -196,6 +199,7 @@ java_genrule { ":ravenwood-common-policies", ":ravenwood-services-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -247,6 +251,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " + "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " + @@ -265,6 +270,7 @@ java_genrule { ":ravenwood-common-policies", ":icu-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -301,6 +307,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-configinfrastructure.log) " + "--stats-file $(location framework-configinfrastructure_stats.csv) " + @@ -319,6 +326,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-configinfrastructure-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -355,6 +363,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-statsd.log) " + "--stats-file $(location framework-statsd_stats.csv) " + @@ -373,6 +382,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-statsd-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -409,6 +419,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-graphics.log) " + "--stats-file $(location framework-graphics_stats.csv) " + @@ -427,6 +438,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-graphics-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index df63cb9dfc50..1148539187ac 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -150,6 +150,10 @@ "host": true }, { + "name": "RavenwoodCoreTest-light", + "host": true + }, + { "name": "RavenwoodMinimumTest", "host": true }, @@ -168,6 +172,10 @@ { "name": "RavenwoodServicesTest", "host": true + }, + { + "name": "UinputTestsRavenwood", + "host": true } // AUTO-GENERATED-END ], diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java new file mode 100644 index 000000000000..cdfd4a877f43 --- /dev/null +++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.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.bivalenttest.ravenizer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import org.junit.Test; + +import java.io.FileDescriptor; +import java.util.LinkedHashMap; +import java.util.regex.Pattern; + +public class RavenwoodJdkPatchTest { + + @Test + public void testUnicodeRegex() { + var pattern = Pattern.compile("\\w+"); + assertTrue(pattern.matcher("über").matches()); + } + + @Test + public void testLinkedHashMapEldest() { + var map = new LinkedHashMap<String, String>(); + map.put("a", "b"); + map.put("x", "y"); + assertEquals(map.entrySet().iterator().next(), map.eldest()); + } + + @Test + public void testFileDescriptorGetSetInt() throws ErrnoException { + FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0); + try { + int fdRaw = fd.getInt$(); + assertNotEquals(-1, fdRaw); + fd.setInt$(-1); + assertEquals(-1, fd.getInt$()); + fd.setInt$(fdRaw); + Os.close(fd); + assertEquals(-1, fd.getInt$()); + } finally { + Os.close(fd); + } + } +} diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt new file mode 100644 index 000000000000..cef736f87e72 --- /dev/null +++ b/ravenwood/texts/ravenizer-standard-options.txt @@ -0,0 +1,13 @@ +# File containing standard options to Ravenizer for Ravenwood + +# Keep all classes / methods / fields in tests and its target +--default-keep + +--delete-finals + +# Include standard annotations +@jar:texts/ravenwood-standard-annotations.txt + +# Apply common policies +--policy-override-file + jar:texts/ravenwood-common-policies.txt diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt new file mode 100644 index 000000000000..75ec5cadb6fc --- /dev/null +++ b/ravenwood/texts/ravenwood-standard-annotations.txt @@ -0,0 +1,37 @@ +# Standard annotations. +# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`. +--keep-annotation + android.ravenwood.annotation.RavenwoodKeep + +--keep-annotation + android.ravenwood.annotation.RavenwoodKeepPartialClass + +--keep-class-annotation + android.ravenwood.annotation.RavenwoodKeepWholeClass + +--throw-annotation + android.ravenwood.annotation.RavenwoodThrow + +--remove-annotation + android.ravenwood.annotation.RavenwoodRemove + +--ignore-annotation + android.ravenwood.annotation.RavenwoodIgnore + +--partially-allowed-annotation + android.ravenwood.annotation.RavenwoodPartiallyAllowlisted + +--substitute-annotation + android.ravenwood.annotation.RavenwoodReplace + +--redirect-annotation + android.ravenwood.annotation.RavenwoodRedirect + +--redirection-class-annotation + android.ravenwood.annotation.RavenwoodRedirectionClass + +--class-load-hook-annotation + android.ravenwood.annotation.RavenwoodClassLoadHook + +--keep-static-initializer-annotation + android.ravenwood.annotation.RavenwoodKeepStaticInitializer diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt index 233657557747..0a650254a71f 100644 --- a/ravenwood/texts/ravenwood-standard-options.txt +++ b/ravenwood/texts/ravenwood-standard-options.txt @@ -15,41 +15,3 @@ #--default-class-load-hook # com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded - -# Standard annotations. -# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`. ---keep-annotation - android.ravenwood.annotation.RavenwoodKeep - ---keep-annotation - android.ravenwood.annotation.RavenwoodKeepPartialClass - ---keep-class-annotation - android.ravenwood.annotation.RavenwoodKeepWholeClass - ---throw-annotation - android.ravenwood.annotation.RavenwoodThrow - ---remove-annotation - android.ravenwood.annotation.RavenwoodRemove - ---ignore-annotation - android.ravenwood.annotation.RavenwoodIgnore - ---partially-allowed-annotation - android.ravenwood.annotation.RavenwoodPartiallyAllowlisted - ---substitute-annotation - android.ravenwood.annotation.RavenwoodReplace - ---redirect-annotation - android.ravenwood.annotation.RavenwoodRedirect - ---redirection-class-annotation - android.ravenwood.annotation.RavenwoodRedirectionClass - ---class-load-hook-annotation - android.ravenwood.annotation.RavenwoodClassLoadHook - ---keep-static-initializer-annotation - android.ravenwood.annotation.RavenwoodKeepStaticInitializer diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt index f59e143c1e4e..ae0a00855650 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt @@ -15,8 +15,6 @@ */ package com.android.hoststubgen -import java.io.File - /** * We will not print the stack trace for exceptions implementing it. */ @@ -64,9 +62,6 @@ class DuplicateAnnotationException(annotationName: String?) : class InputFileNotFoundException(filename: String) : ArgumentsException("File '$filename' not found") -fun String.ensureFileExists(): String { - if (!File(this).exists()) { - throw InputFileNotFoundException(this) - } - return this -} +/** Thrown when a JAR resource does not exist. */ +class JarResourceNotFoundException(path: String) : + ArgumentsException("JAR resource '$path' not found") diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt index 4fe21eac6972..98f96a89d889 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt @@ -16,6 +16,7 @@ package com.android.hoststubgen import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.findAnyAnnotation import com.android.hoststubgen.filters.AnnotationBasedFilter import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter import com.android.hoststubgen.filters.ConstantFilter @@ -26,21 +27,25 @@ import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder +import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep import com.android.hoststubgen.utils.ClassPredicate import com.android.hoststubgen.visitors.BaseAdapter +import com.android.hoststubgen.visitors.ImplGeneratingAdapter import com.android.hoststubgen.visitors.PackageRedirectRemapper +import java.io.PrintWriter import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper import org.objectweb.asm.util.CheckClassAdapter +import org.objectweb.asm.util.TraceClassVisitor /** * This class implements bytecode transformation of HostStubGen. */ class HostStubGenClassProcessor( private val options: HostStubGenClassProcessorOptions, - private val allClasses: ClassNodes, + val allClasses: ClassNodes, private val errors: HostStubGenErrors = HostStubGenErrors(), private val stats: HostStubGenStats? = null, ) { @@ -48,6 +53,7 @@ class HostStubGenClassProcessor( val remapper = FilterRemapper(filter) private val packageRedirector = PackageRedirectRemapper(options.packageRedirects) + private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR) /** * Build the filter, which decides what classes/methods/fields should be put in stub or impl @@ -130,15 +136,10 @@ class HostStubGenClassProcessor( return filter } - fun processClassBytecode(bytecode: ByteArray): ByteArray { - val cr = ClassReader(bytecode) - - // COMPUTE_FRAMES wouldn't be happy if code uses - val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES - val cw = ClassWriter(flags) + private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor { + // Connect to the base visitor + var outVisitor: ClassVisitor = base - // Connect to the class writer - var outVisitor: ClassVisitor = cw if (options.enableClassChecker.get) { outVisitor = CheckClassAdapter(outVisitor) } @@ -149,15 +150,59 @@ class HostStubGenClassProcessor( val visitorOptions = BaseAdapter.Options( errors = errors, stats = stats, - enablePreTrace = options.enablePreTrace.get, - enablePostTrace = options.enablePostTrace.get, deleteClassFinals = options.deleteFinals.get, deleteMethodFinals = options.deleteFinals.get, ) - outVisitor = BaseAdapter.getVisitor( - cr.className, allClasses, outVisitor, filter, - packageRedirector, visitorOptions - ) + + val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose)) + + // Inject TraceClassVisitor for debugging. + if (options.enablePostTrace.get) { + outVisitor = TraceClassVisitor(outVisitor, verbosePrinter) + } + + // Handle --package-redirect + if (!packageRedirector.isEmpty) { + // Don't apply the remapper on redirect-from classes. + // Otherwise, if the target jar actually contains the "from" classes (which + // may or may not be the case) they'd be renamed. + // But we update all references in other places, so, a method call to a "from" class + // would be replaced with the "to" class. All type references (e.g. variable types) + // will be updated too. + if (!packageRedirector.isTarget(className)) { + outVisitor = ClassRemapper(outVisitor, packageRedirector) + } else { + log.v( + "Class $className is a redirect-from class, not applying" + + " --package-redirect" + ) + } + } + + outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions) + + // Inject TraceClassVisitor for debugging. + if (options.enablePreTrace.get) { + outVisitor = TraceClassVisitor(outVisitor, verbosePrinter) + } + + return outVisitor + } + + fun processClassBytecode(bytecode: ByteArray): ByteArray { + val cr = ClassReader(bytecode) + + // If the class was already processed previously, skip + val clz = allClasses.getClass(cr.className) + if (clz.findAnyAnnotation(processedAnnotation) != null) { + return bytecode + } + + // COMPUTE_FRAMES wouldn't be happy if code uses + val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES + val cw = ClassWriter(flags) + + val outVisitor = buildVisitor(cw, cr.className) cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) return cw.toByteArray() diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt index c7c45e65a8b6..e7166f11f597 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt @@ -18,6 +18,7 @@ package com.android.hoststubgen import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions +import com.android.hoststubgen.utils.FileOrResource import com.android.hoststubgen.utils.SetOnce private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> { @@ -53,7 +54,7 @@ open class HostStubGenClassProcessorOptions( var defaultClassLoadHook: SetOnce<String?> = SetOnce(null), var defaultMethodCallHook: SetOnce<String?> = SetOnce(null), - var policyOverrideFiles: MutableList<String> = mutableListOf(), + var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(), var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove), @@ -73,15 +74,14 @@ open class HostStubGenClassProcessorOptions( return name } - override fun parseOption(option: String, ai: ArgIterator): Boolean { + override fun parseOption(option: String, args: ArgIterator): Boolean { // Define some shorthands... - fun nextArg(): String = ai.nextArgRequired(option) + fun nextArg(): String = args.nextArgRequired(option) fun MutableSet<String>.addUniqueAnnotationArg(): String = nextArg().also { this += ensureUniqueAnnotation(it) } when (option) { - "--policy-override-file" -> - policyOverrideFiles.add(nextArg().ensureFileExists()) + "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg())) "--default-remove" -> defaultPolicy.set(FilterPolicy.Remove) "--default-throw" -> defaultPolicy.set(FilterPolicy.Throw) diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt index b41ce0f65017..112ef01e20cb 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt @@ -217,7 +217,7 @@ private val numericalInnerClassName = """.*\$\d+$""".toRegex() fun isAnonymousInnerClass(cn: ClassNode): Boolean { // TODO: Is there a better way? - return cn.name.matches(numericalInnerClassName) + return cn.outerClass != null && cn.name.matches(numericalInnerClassName) } /** diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt index b8b0d8a31268..7f36aca33eee 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt @@ -19,9 +19,9 @@ package com.android.hoststubgen.filters * Base class for an [OutputFilter] that uses another filter as a fallback. */ abstract class DelegatingFilter( - // fallback shouldn't be used by subclasses directly, so make it private. - // They should instead be calling into `super` or `outermostFilter`. - private val fallback: OutputFilter + // fallback shouldn't be used by subclasses directly, so make it private. + // They should instead be calling into `super` or `outermostFilter`. + private val fallback: OutputFilter ) : OutputFilter() { init { fallback.outermostFilter = this @@ -50,24 +50,24 @@ abstract class DelegatingFilter( } override fun getPolicyForField( - className: String, - fieldName: String + className: String, + fieldName: String ): FilterPolicyWithReason { return fallback.getPolicyForField(className, fieldName) } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): FilterPolicyWithReason { return fallback.getPolicyForMethod(className, methodName, descriptor) } override fun getRenameTo( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): String? { return fallback.getRenameTo(className, methodName, descriptor) } @@ -97,13 +97,12 @@ abstract class DelegatingFilter( } override fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, className: String, methodName: String, descriptor: String, ): MethodReplaceTarget? { return fallback.getMethodCallReplaceTo( - callerClassName, callerMethodName, className, methodName, descriptor) + className, methodName, descriptor + ) } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt index 474da6dfa1b9..d44d016f7c5b 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt @@ -16,7 +16,6 @@ package com.android.hoststubgen.filters import com.android.hoststubgen.HostStubGenErrors -import com.android.hoststubgen.HostStubGenInternalException import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME import com.android.hoststubgen.asm.ClassNodes @@ -37,19 +36,15 @@ import org.objectweb.asm.tree.ClassNode * TODO: Do we need a way to make anonymous class methods and lambdas "throw"? */ class ImplicitOutputFilter( - private val errors: HostStubGenErrors, - private val classes: ClassNodes, - fallback: OutputFilter + private val errors: HostStubGenErrors, + private val classes: ClassNodes, + fallback: OutputFilter ) : DelegatingFilter(fallback) { - private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? { + private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? { if (isAnonymousInnerClass(cn)) { log.forDebug { // log.d(" anon-inner class: ${className} outer: ${cn.outerClass} ") } - if (cn.outerClass == null) { - throw HostStubGenInternalException( - "outerClass is null for anonymous inner class") - } // If the outer class needs to be in impl, it should be in impl too. val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass) if (outerPolicy.policy.needsInOutput) { @@ -65,15 +60,15 @@ class ImplicitOutputFilter( val cn = classes.getClass(className) // Use the implicit policy, if any. - getClassImplicitPolicy(className, cn)?.let { return it } + getClassImplicitPolicy(cn)?.let { return it } return fallback } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): FilterPolicyWithReason { val fallback = super.getPolicyForMethod(className, methodName, descriptor) val classPolicy = outermostFilter.getPolicyForClass(className) @@ -84,12 +79,14 @@ class ImplicitOutputFilter( // "keep" instead. // Unless it's an enum -- in that case, the below code would handle it. if (!cn.isEnum() && - fallback.policy == FilterPolicy.Throw && - methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) { + fallback.policy == FilterPolicy.Throw && + methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC + ) { // TODO Maybe show a warning?? But that'd be too noisy with --default-throw. return FilterPolicy.Ignore.withReason( "'throw' on static initializer is handled as 'ignore'" + - " [original throw reason: ${fallback.reason}]") + " [original throw reason: ${fallback.reason}]" + ) } log.d("Class ${cn.name} Class policy: $classPolicy") @@ -120,7 +117,8 @@ class ImplicitOutputFilter( // For synthetic methods (such as lambdas), let's just inherit the class's // policy. return memberPolicy.withReason(classPolicy.reason).wrapReason( - "is-synthetic-method") + "is-synthetic-method" + ) } } } @@ -129,8 +127,8 @@ class ImplicitOutputFilter( } override fun getPolicyForField( - className: String, - fieldName: String + className: String, + fieldName: String ): FilterPolicyWithReason { val fallback = super.getPolicyForField(className, fieldName) @@ -161,4 +159,4 @@ class ImplicitOutputFilter( return fallback } -}
\ No newline at end of file +} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt index fc885d6f463b..59da3da99ea5 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt @@ -28,10 +28,12 @@ class InMemoryOutputFilter( private val classes: ClassNodes, fallback: OutputFilter, ) : DelegatingFilter(fallback) { - private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf() - private val mRenames: MutableMap<String, String> = mutableMapOf() - private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf() - private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf() + private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>() + private val mRenames = mutableMapOf<String, String>() + private val mRedirectionClasses = mutableMapOf<String, String>() + private val mClassLoadHooks = mutableMapOf<String, String>() + private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>() + private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>() private fun getClassKey(className: String): String { return className.toHumanReadableClassName() @@ -45,10 +47,6 @@ class InMemoryOutputFilter( return getClassKey(className) + "." + methodName + ";" + signature } - override fun getPolicyForClass(className: String): FilterPolicyWithReason { - return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className) - } - private fun checkClass(className: String) { if (classes.findClass(className) == null) { log.w("Unknown class $className") @@ -74,6 +72,10 @@ class InMemoryOutputFilter( } } + override fun getPolicyForClass(className: String): FilterPolicyWithReason { + return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className) + } + fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) { checkClass(className) mPolicies[getClassKey(className)] = policy @@ -81,7 +83,7 @@ class InMemoryOutputFilter( override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason { return mPolicies[getFieldKey(className, fieldName)] - ?: super.getPolicyForField(className, fieldName) + ?: super.getPolicyForField(className, fieldName) } fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) { @@ -90,21 +92,21 @@ class InMemoryOutputFilter( } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - ): FilterPolicyWithReason { + className: String, + methodName: String, + descriptor: String, + ): FilterPolicyWithReason { return mPolicies[getMethodKey(className, methodName, descriptor)] ?: mPolicies[getMethodKey(className, methodName, "*")] ?: super.getPolicyForMethod(className, methodName, descriptor) } fun setPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - policy: FilterPolicyWithReason, - ) { + className: String, + methodName: String, + descriptor: String, + policy: FilterPolicyWithReason, + ) { checkMethod(className, methodName, descriptor) mPolicies[getMethodKey(className, methodName, descriptor)] = policy } @@ -123,7 +125,7 @@ class InMemoryOutputFilter( override fun getRedirectionClass(className: String): String? { return mRedirectionClasses[getClassKey(className)] - ?: super.getRedirectionClass(className) + ?: super.getRedirectionClass(className) } fun setRedirectionClass(from: String, to: String) { @@ -135,11 +137,52 @@ class InMemoryOutputFilter( } override fun getClassLoadHooks(className: String): List<String> { - return addNonNullElement(super.getClassLoadHooks(className), - mClassLoadHooks[getClassKey(className)]) + return addNonNullElement( + super.getClassLoadHooks(className), + mClassLoadHooks[getClassKey(className)] + ) } fun setClassLoadHook(className: String, methodName: String) { mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName() } + + override fun hasAnyMethodCallReplace(): Boolean { + return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace() + } + + override fun getMethodCallReplaceTo( + className: String, + methodName: String, + descriptor: String, + ): MethodReplaceTarget? { + // Maybe use 'Tri' if we end up having too many replacements. + mMethodCallReplaceSpecs.forEach { + if (className == it.fromClass && + methodName == it.fromMethod + ) { + if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) { + return MethodReplaceTarget(it.toClass, it.toMethod) + } + } + } + return super.getMethodCallReplaceTo(className, methodName, descriptor) + } + + fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) { + mMethodCallReplaceSpecs.add(spec) + } + + override fun remapType(className: String): String? { + mTypeRenameSpecs.forEach { + if (it.typeInternalNamePattern.matcher(className).matches()) { + return it.typeInternalNamePrefix + className + } + } + return super.remapType(className) + } + + fun setRemapTypeSpec(spec: TypeRenameSpec) { + mTypeRenameSpecs.add(spec) + } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt index f99ce906240a..c47bb302920f 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt @@ -41,10 +41,10 @@ abstract class OutputFilter { abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason abstract fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - ): FilterPolicyWithReason + className: String, + methodName: String, + descriptor: String, + ): FilterPolicyWithReason /** * If a given method is a substitute-from method, return the substitute-to method name. @@ -108,8 +108,6 @@ abstract class OutputFilter { * If a method call should be forwarded to another method, return the target's class / method. */ open fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, className: String, methodName: String, descriptor: String, diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index dd353e9caeff..97fc35302528 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -22,13 +22,13 @@ import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine +import com.android.hoststubgen.utils.FileOrResource import com.android.hoststubgen.whitespaceRegex -import org.objectweb.asm.tree.ClassNode import java.io.BufferedReader -import java.io.FileReader import java.io.PrintWriter import java.io.Reader import java.util.regex.Pattern +import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -58,6 +58,23 @@ enum class SpecialClass { RFile, } +data class MethodCallReplaceSpec( + val fromClass: String, + val fromMethod: String, + val fromDescriptor: String, + val toClass: String, + val toMethod: String, +) + +/** + * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix] + * to it. + */ +data class TypeRenameSpec( + val typeInternalNamePattern: Pattern, + val typeInternalNamePrefix: String, +) + /** * This receives [TextFileFilterPolicyBuilder] parsing result. */ @@ -99,7 +116,7 @@ interface PolicyFileProcessor { className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) } @@ -116,9 +133,6 @@ class TextFileFilterPolicyBuilder( private var featureFlagsPolicy: FilterPolicyWithReason? = null private var syspropsPolicy: FilterPolicyWithReason? = null private var rFilePolicy: FilterPolicyWithReason? = null - private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>() - private val methodReplaceSpec = - mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() /** * Fields for a filter chain used for "partial allowlisting", which are used by @@ -126,47 +140,34 @@ class TextFileFilterPolicyBuilder( */ private val annotationAllowedInMemoryFilter: InMemoryOutputFilter val annotationAllowedMembersFilter: OutputFilter + get() = annotationAllowedInMemoryFilter private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON) init { // Create a filter that checks "partial allowlisting". - var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed") - - aaf = InMemoryOutputFilter(classes, aaf) - annotationAllowedInMemoryFilter = aaf - - annotationAllowedMembersFilter = annotationAllowedInMemoryFilter + val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed") + annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter) } /** * Parse a given policy file. This method can be called multiple times to read from * multiple files. To get the resulting filter, use [createOutputFilter] */ - fun parse(file: String) { + fun parse(file: FileOrResource) { // We may parse multiple files, but we reuse the same parser, because the parser // will make sure there'll be no dupplicating "special class" policies. - parser.parse(FileReader(file), file, Processor()) + parser.parse(file.open(), file.path, Processor()) } /** * Generate the resulting [OutputFilter]. */ fun createOutputFilter(): OutputFilter { - var ret: OutputFilter = imf - if (typeRenameSpec.isNotEmpty()) { - ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret) - } - if (methodReplaceSpec.isNotEmpty()) { - ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret) - } - // Wrap the in-memory-filter with AHF. - ret = AndroidHeuristicsFilter( - classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret + return AndroidHeuristicsFilter( + classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf ) - - return ret } private inner class Processor : PolicyFileProcessor { @@ -180,9 +181,7 @@ class TextFileFilterPolicyBuilder( } override fun onRename(pattern: Pattern, prefix: String) { - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( - pattern, prefix - ) + imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix)) } override fun onClassStart(className: String) { @@ -284,12 +283,12 @@ class TextFileFilterPolicyBuilder( className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) { // Keep the source method, because the target method may call it. imf.setPolicyForMethod(className, methodName, methodDesc, FilterPolicy.Keep.withReason(FILTER_REASON)) - methodReplaceSpec.add(replaceSpec) + imf.setMethodCallReplaceSpec(replaceSpec) } } } @@ -630,13 +629,13 @@ class TextFileFilterPolicyParser { if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName!!.toJvmClassName(), - methodName, - signature, - classAndMethod.first.toJvmClassName(), - classAndMethod.second, - ) + val spec = MethodCallReplaceSpec( + className.toJvmClassName(), + methodName, + signature, + classAndMethod.first.toJvmClassName(), + classAndMethod.second, + ) processor.onMethodOutClassReplace( className, methodName, diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt deleted file mode 100644 index a3f934cacc2c..000000000000 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.hoststubgen.filters - -import com.android.hoststubgen.asm.ClassNodes - -/** - * Filter used by TextFileFilterPolicyParser for "method call relacement". - */ -class TextFilePolicyMethodReplaceFilter( - val spec: List<MethodCallReplaceSpec>, - val classes: ClassNodes, - val fallback: OutputFilter, -) : DelegatingFilter(fallback) { - - data class MethodCallReplaceSpec( - val fromClass: String, - val fromMethod: String, - val fromDescriptor: String, - val toClass: String, - val toMethod: String, - ) - - override fun hasAnyMethodCallReplace(): Boolean { - return true - } - - override fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, - className: String, - methodName: String, - descriptor: String, - ): MethodReplaceTarget? { - // Maybe use 'Tri' if we end up having too many replacements. - spec.forEach { - if (className == it.fromClass && - methodName == it.fromMethod - ) { - if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) { - return MethodReplaceTarget(it.toClass, it.toMethod) - } - } - } - return null - } -} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt deleted file mode 100644 index bc90d1248322..000000000000 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.hoststubgen.filters - -import java.util.regex.Pattern - -/** - * A filter that provides a simple "jarjar" functionality via [mapType] - */ -class TextFilePolicyRemapperFilter( - val typeRenameSpecs: List<TypeRenameSpec>, - fallback: OutputFilter, -) : DelegatingFilter(fallback) { - /** - * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix] - * to it. - */ - data class TypeRenameSpec( - val typeInternalNamePattern: Pattern, - val typeInternalNamePrefix: String, - ) - - override fun remapType(className: String): String? { - typeRenameSpecs.forEach { - if (it.typeInternalNamePattern.matcher(className).matches()) { - return it.typeInternalNamePrefix + className - } - } - return null - } -} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt index 0b17879b862c..d0869929edfb 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt @@ -16,11 +16,16 @@ package com.android.hoststubgen.utils import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists +import com.android.hoststubgen.InputFileNotFoundException +import com.android.hoststubgen.JarResourceNotFoundException import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine -import java.io.BufferedReader +import java.io.File import java.io.FileReader +import java.io.InputStreamReader +import java.io.Reader + +const val JAR_RESOURCE_PREFIX = "jar:" /** * Base class for parsing arguments from commandline. @@ -73,7 +78,7 @@ abstract class BaseOptions { * * Subclasses override/extend this method to support more options. */ - abstract fun parseOption(option: String, ai: ArgIterator): Boolean + abstract fun parseOption(option: String, args: ArgIterator): Boolean abstract fun dumpFields(): String } @@ -112,7 +117,9 @@ class ArgIterator( companion object { fun withAtFiles(args: List<String>): ArgIterator { - return ArgIterator(expandAtFiles(args)) + val expanded = mutableListOf<String>() + expandAtFiles(args.asSequence(), expanded) + return ArgIterator(expanded) } /** @@ -125,34 +132,30 @@ class ArgIterator( * * The file can contain '#' as comments. */ - private fun expandAtFiles(args: List<String>): List<String> { - val ret = mutableListOf<String>() - + private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) { args.forEach { arg -> if (arg.startsWith("@@")) { - ret += arg.substring(1) + out.add(arg.substring(1)) return@forEach } else if (!arg.startsWith('@')) { - ret += arg + out.add(arg) return@forEach } + // Read from the file, and add each line to the result. - val filename = arg.substring(1).ensureFileExists() + val file = FileOrResource(arg.substring(1)) - log.v("Expanding options file $filename") + log.v("Expanding options file ${file.path}") - BufferedReader(FileReader(filename)).use { reader -> - while (true) { - var line = reader.readLine() ?: break // EOF + val fileArgs = file + .open() + .buffered() + .lineSequence() + .map(::normalizeTextLine) + .filter(CharSequence::isNotEmpty) - line = normalizeTextLine(line) - if (line.isNotEmpty()) { - ret += line - } - } - } + expandAtFiles(fileArgs, out) } - return ret } } } @@ -204,3 +207,37 @@ class IntSetOnce(value: Int) : SetOnce<Int>(value) { } } } + +/** + * A path either points to a file in filesystem, or an entry in the JAR. + */ +class FileOrResource(val path: String) { + init { + path.ensureFileExists() + } + + /** + * Either read from filesystem, or read from JAR resources. + */ + fun open(): Reader { + return if (path.startsWith(JAR_RESOURCE_PREFIX)) { + val path = path.removePrefix(JAR_RESOURCE_PREFIX) + InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!) + } else { + FileReader(path) + } + } +} + +fun String.ensureFileExists(): String { + if (this.startsWith(JAR_RESOURCE_PREFIX)) { + val cl = FileOrResource::class.java.classLoader + val path = this.removePrefix(JAR_RESOURCE_PREFIX) + if (cl.getResource(path) == null) { + throw JarResourceNotFoundException(path) + } + } else if (!File(this).exists()) { + throw InputFileNotFoundException(this) + } + return this +} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt index a08d1d605949..769b769d7a20 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -17,7 +17,6 @@ package com.android.hoststubgen.visitors import com.android.hoststubgen.HostStubGenErrors import com.android.hoststubgen.HostStubGenStats -import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor import com.android.hoststubgen.asm.getPackageNameFromFullClassName @@ -26,13 +25,10 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep import com.android.hoststubgen.log -import java.io.PrintWriter import org.objectweb.asm.ClassVisitor import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes -import org.objectweb.asm.commons.ClassRemapper -import org.objectweb.asm.util.TraceClassVisitor const val OPCODE_VERSION = Opcodes.ASM9 @@ -49,8 +45,6 @@ abstract class BaseAdapter( data class Options( val errors: HostStubGenErrors, val stats: HostStubGenStats?, - val enablePreTrace: Boolean, - val enablePostTrace: Boolean, val deleteClassFinals: Boolean, val deleteMethodFinals: Boolean, // We don't remove finals from fields, because final fields have a stronger memory @@ -253,50 +247,4 @@ abstract class BaseAdapter( substituted: Boolean, superVisitor: MethodVisitor?, ): MethodVisitor? - - companion object { - fun getVisitor( - classInternalName: String, - classes: ClassNodes, - nextVisitor: ClassVisitor, - filter: OutputFilter, - packageRedirector: PackageRedirectRemapper, - options: Options, - ): ClassVisitor { - var next = nextVisitor - - val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose)) - - // Inject TraceClassVisitor for debugging. - if (options.enablePostTrace) { - next = TraceClassVisitor(next, verbosePrinter) - } - - // Handle --package-redirect - if (!packageRedirector.isEmpty) { - // Don't apply the remapper on redirect-from classes. - // Otherwise, if the target jar actually contains the "from" classes (which - // may or may not be the case) they'd be renamed. - // But we update all references in other places, so, a method call to a "from" class - // would be replaced with the "to" class. All type references (e.g. variable types) - // will be updated too. - if (!packageRedirector.isTarget(classInternalName)) { - next = ClassRemapper(next, packageRedirector) - } else { - log.v( - "Class $classInternalName is a redirect-from class, not applying" + - " --package-redirect" - ) - } - } - - next = ImplGeneratingAdapter(classes, next, filter, options) - - // Inject TraceClassVisitor for debugging. - if (options.enablePreTrace) { - next = TraceClassVisitor(next, verbosePrinter) - } - return next - } - } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt index b8a357668c2b..617385ad438e 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt @@ -396,7 +396,7 @@ class ImplGeneratingAdapter( } val to = filter.getMethodCallReplaceTo( - currentClassName, callerMethodName, owner, name, descriptor + owner, name, descriptor ) if (to == null diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index 8bb454fa12e7..d9cc54aebf51 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -18,6 +18,7 @@ package com.android.hoststubgen import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.IntSetOnce import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options that can be set from command line arguments. @@ -61,9 +62,9 @@ class HostStubGenOptions( } } - override fun parseOption(option: String, ai: ArgIterator): Boolean { + override fun parseOption(option: String, args: ArgIterator): Boolean { // Define some shorthands... - fun nextArg(): String = ai.nextArgRequired(option) + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help @@ -94,7 +95,7 @@ class HostStubGenOptions( } } - else -> return super.parseOption(option, ai) + else -> return super.parseOption(option, args) } return true diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt index f7fd0804c151..58bd9e987fd1 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenhelper.policytoannot import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options for the "ravenhelper pta" subcommand. @@ -41,8 +41,8 @@ class PtaOptions( var dumpOperations: SetOnce<Boolean> = SetOnce(false), ) : BaseOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt index fd6f732a06ce..5ce9a23e6e05 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt @@ -19,10 +19,10 @@ import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.filters.FilterPolicyWithReason +import com.android.hoststubgen.filters.MethodCallReplaceSpec import com.android.hoststubgen.filters.PolicyFileProcessor import com.android.hoststubgen.filters.SpecialClass import com.android.hoststubgen.filters.TextFileFilterPolicyParser -import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter import com.android.hoststubgen.log import com.android.hoststubgen.utils.ClassPredicate import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler @@ -448,7 +448,7 @@ private class TextPolicyToAnnotationConverter( className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) { // This can't be converted to an annotation. classHasMember = true diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt index 8b95843f08a6..6e0b7b89cf13 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenhelper.sourcemap import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options for the "ravenhelper map" subcommand. @@ -38,8 +38,8 @@ class MapOptions( var text: SetOnce<String?> = SetOnce(null), ) : BaseOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp index 957e20647d44..93cda4e3c4c9 100644 --- a/ravenwood/tools/ravenizer/Android.bp +++ b/ravenwood/tools/ravenizer/Android.bp @@ -15,5 +15,10 @@ java_binary_host { "hoststubgen-lib", "ravenwood-junit-for-ravenizer", ], + java_resources: [ + ":ravenizer-standard-options", + ":ravenwood-standard-annotations", + ":ravenwood-common-policies", + ], visibility: ["//visibility:public"], } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt index e67c730df069..04e3bda2ba27 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt @@ -16,23 +16,22 @@ package com.android.platform.test.ravenwood.ravenizer import com.android.hoststubgen.GeneralUserErrorException +import com.android.hoststubgen.HostStubGenClassProcessor import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.zipEntryNameToClassName import com.android.hoststubgen.executableName import com.android.hoststubgen.log import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.util.CheckClassAdapter import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.util.CheckClassAdapter /** * Various stats on Ravenizer. @@ -41,7 +40,7 @@ data class RavenizerStats( /** Total end-to-end time. */ var totalTime: Double = .0, - /** Time took to build [ClasNodes] */ + /** Time took to build [ClassNodes] */ var loadStructureTime: Double = .0, /** Time took to validate the classes */ @@ -50,14 +49,17 @@ data class RavenizerStats( /** Total real time spent for converting the jar file */ var totalProcessTime: Double = .0, - /** Total real time spent for converting class files (except for I/O time). */ - var totalConversionTime: Double = .0, + /** Total real time spent for ravenizing class files (excluding I/O time). */ + var totalRavenizeTime: Double = .0, + + /** Total real time spent for processing class files HSG style (excluding I/O time). */ + var totalHostStubGenTime: Double = .0, /** Total real time spent for copying class files without modification. */ var totalCopyTime: Double = .0, /** # of entries in the input jar file */ - var totalEntiries: Int = 0, + var totalEntries: Int = 0, /** # of *.class files in the input jar file */ var totalClasses: Int = 0, @@ -67,14 +69,15 @@ data class RavenizerStats( ) { override fun toString(): String { return """ - RavenizerStats{ + RavenizerStats { totalTime=$totalTime, loadStructureTime=$loadStructureTime, validationTime=$validationTime, totalProcessTime=$totalProcessTime, - totalConversionTime=$totalConversionTime, + totalRavenizeTime=$totalRavenizeTime, + totalHostStubGenTime=$totalHostStubGenTime, totalCopyTime=$totalCopyTime, - totalEntiries=$totalEntiries, + totalEntries=$totalEntries, totalClasses=$totalClasses, processedClasses=$processedClasses, } @@ -90,12 +93,18 @@ class Ravenizer { val stats = RavenizerStats() stats.totalTime = log.nTime { + val allClasses = ClassNodes.loadClassStructures(options.inJar.get) { + stats.loadStructureTime = it + } + val processor = HostStubGenClassProcessor(options, allClasses) + process( options.inJar.get, options.outJar.get, options.enableValidation.get, options.fatalValidation.get, options.stripMockito.get, + processor, stats, ) } @@ -108,15 +117,13 @@ class Ravenizer { enableValidation: Boolean, fatalValidation: Boolean, stripMockito: Boolean, + processor: HostStubGenClassProcessor, stats: RavenizerStats, ) { - var allClasses = ClassNodes.loadClassStructures(inJar) { - time -> stats.loadStructureTime = time - } if (enableValidation) { stats.validationTime = log.iTime("Validating classes") { - if (!validateClasses(allClasses)) { - var message = "Invalid test class(es) detected." + + if (!validateClasses(processor.allClasses)) { + val message = "Invalid test class(es) detected." + " See error log for details." if (fatalValidation) { throw RavenizerInvalidTestException(message) @@ -126,7 +133,7 @@ class Ravenizer { } } } - if (includeUnsupportedMockito(allClasses)) { + if (includeUnsupportedMockito(processor.allClasses)) { log.w("Unsupported Mockito detected in $inJar!") } @@ -134,7 +141,7 @@ class Ravenizer { ZipFile(inJar).use { inZip -> val inEntries = inZip.entries() - stats.totalEntiries = inZip.size() + stats.totalEntries = inZip.size() ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip -> while (inEntries.hasMoreElements()) { @@ -159,9 +166,9 @@ class Ravenizer { stats.totalClasses += 1 } - if (className != null && shouldProcessClass(allClasses, className)) { - stats.processedClasses += 1 - processSingleClass(inZip, entry, outZip, allClasses, stats) + if (className != null && + shouldProcessClass(processor.allClasses, className)) { + processSingleClass(inZip, entry, outZip, processor, stats) } else { // Too slow, let's use merge_zips to bring back the original classes. copyZipEntry(inZip, entry, outZip, stats) @@ -201,14 +208,22 @@ class Ravenizer { inZip: ZipFile, entry: ZipEntry, outZip: ZipOutputStream, - allClasses: ClassNodes, + processor: HostStubGenClassProcessor, stats: RavenizerStats, ) { + stats.processedClasses += 1 val newEntry = ZipEntry(entry.name) outZip.putNextEntry(newEntry) BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - processSingleClass(entry, bis, outZip, allClasses, stats) + var classBytes = bis.readBytes() + stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") { + classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses) + } + stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") { + classBytes = processor.processClassBytecode(classBytes) + } + outZip.write(classBytes) } outZip.closeEntry() } @@ -217,41 +232,34 @@ class Ravenizer { * Whether a class needs to be processed. This must be kept in sync with [processSingleClass]. */ private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean { - return !classInternalName.shouldByBypassed() + return !classInternalName.shouldBypass() && RunnerRewritingAdapter.shouldProcess(classes, classInternalName) } - private fun processSingleClass( + private fun ravenizeSingleClass( entry: ZipEntry, - input: InputStream, - output: OutputStream, + input: ByteArray, allClasses: ClassNodes, - stats: RavenizerStats, - ) { - val cr = ClassReader(input) - - lateinit var data: ByteArray - stats.totalConversionTime += log.vTime("Modify ${entry.name}") { + ): ByteArray { + val classInternalName = zipEntryNameToClassName(entry.name) + ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") - val classInternalName = zipEntryNameToClassName(entry.name) - ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") - val flags = ClassWriter.COMPUTE_MAXS - val cw = ClassWriter(flags) - var outVisitor: ClassVisitor = cw + val flags = ClassWriter.COMPUTE_MAXS + val cw = ClassWriter(flags) + var outVisitor: ClassVisitor = cw - val enableChecker = false - if (enableChecker) { - outVisitor = CheckClassAdapter(outVisitor) - } + val enableChecker = false + if (enableChecker) { + outVisitor = CheckClassAdapter(outVisitor) + } - // This must be kept in sync with shouldProcessClass. - outVisitor = RunnerRewritingAdapter.maybeApply( - classInternalName, allClasses, outVisitor) + // This must be kept in sync with shouldProcessClass. + outVisitor = RunnerRewritingAdapter.maybeApply( + classInternalName, allClasses, outVisitor) - cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) + val cr = ClassReader(input) + cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) - data = cw.toByteArray() - } - output.write(data) + return cw.toByteArray() } } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt index 7f4829ec6127..8a09e6d533b8 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt @@ -21,6 +21,7 @@ import com.android.hoststubgen.LogLevel import com.android.hoststubgen.executableName import com.android.hoststubgen.log import com.android.hoststubgen.runMainWithBoilerplate +import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX import java.nio.file.Paths import kotlin.io.path.exists @@ -36,6 +37,10 @@ import kotlin.io.path.exists */ private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe" +/** + * This is the name of the standard option text file embedded inside ravenizer.jar. + */ +private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt" /** * Entry point. @@ -45,12 +50,12 @@ fun main(args: Array<String>) { log.setConsoleLogLevel(LogLevel.Info) runMainWithBoilerplate { - var newArgs = args.asList() + val newArgs = args.toMutableList() + newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS") + if (Paths.get(RAVENIZER_DOTFILE).exists()) { log.i("Reading options from $RAVENIZER_DOTFILE") - newArgs = args.toMutableList().apply { - add(0, "@$RAVENIZER_DOTFILE") - } + newArgs.add(0, "@$RAVENIZER_DOTFILE") } val options = RavenizerOptions().apply { parseArgs(newArgs) } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt index 2c0365404ab6..5d278bb046ae 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenizer import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists +import com.android.hoststubgen.HostStubGenClassProcessorOptions import com.android.hoststubgen.utils.ArgIterator -import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists class RavenizerOptions( /** Input jar file*/ @@ -36,10 +36,10 @@ class RavenizerOptions( /** Whether to remove mockito and dexmaker classes. */ var stripMockito: SetOnce<Boolean> = SetOnce(false), -) : BaseOptions() { +) : HostStubGenClassProcessorOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help @@ -57,7 +57,7 @@ class RavenizerOptions( "--strip-mockito" -> stripMockito.set(true) "--no-strip-mockito" -> stripMockito.set(false) - else -> return false + else -> return super.parseOption(option, args) } return true @@ -79,6 +79,6 @@ class RavenizerOptions( enableValidation=$enableValidation, fatalValidation=$fatalValidation, stripMockito=$stripMockito, - """.trimIndent() + """.trimIndent() + '\n' + super.dumpFields() } } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt index 6092fcc9402d..b394a761c7ae 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt @@ -15,8 +15,8 @@ */ package com.android.platform.test.ravenwood.ravenizer -import android.platform.test.annotations.internal.InnerRunner import android.platform.test.annotations.NoRavenizer +import android.platform.test.annotations.internal.InnerRunner import android.platform.test.ravenwood.RavenwoodAwareTestRunner import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.findAnyAnnotation @@ -85,7 +85,7 @@ fun String.isRavenwoodClass(): Boolean { /** * Classes that should never be modified. */ -fun String.shouldByBypassed(): Boolean { +fun String.shouldBypass(): Boolean { if (this.isRavenwoodClass()) { return true } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt index 61e254b225c3..d252b4dc8ab6 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt @@ -20,8 +20,8 @@ import com.android.hoststubgen.asm.isAbstract import com.android.hoststubgen.asm.startsWithAny import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.log -import org.objectweb.asm.tree.ClassNode import java.util.regex.Pattern +import org.objectweb.asm.tree.ClassNode fun validateClasses(classes: ClassNodes): Boolean { var allOk = true @@ -37,7 +37,7 @@ fun validateClasses(classes: ClassNodes): Boolean { * */ fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean { - if (cn.name.shouldByBypassed()) { + if (cn.name.shouldBypass()) { // Class doesn't need to be checked. return true } @@ -145,4 +145,4 @@ com.android.server.power.stats.BatteryStatsTimerTest private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean { return allowListedLegacyTests.contains(targetClass.name) -}
\ No newline at end of file +} diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index b52b3dabd47d..35db3c6f0a6d 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -260,6 +260,16 @@ flag { } flag { + name: "pointer_up_motion_event_in_touch_exploration" + namespace: "accessibility" + description: "Allows POINTER_UP motionEvents to trigger during touch exploration." + bug: "374930391" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "proxy_use_apps_on_virtual_device_listener" namespace: "accessibility" description: "Fixes race condition described in b/286587811" @@ -336,3 +346,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "hearing_input_change_when_comm_device" + namespace: "accessibility" + description: "Listen to the CommunicationDeviceChanged to show hearing device input notification." + bug: "394070235" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index c49151dd5e30..6c26c1f74002 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -521,15 +521,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Nullable IBinder focusedToken) { return AccessibilityManagerService.this.handleKeyGestureEvent(event); } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - return switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, - KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true; - default -> false; - }; - } }; @VisibleForTesting @@ -5619,7 +5610,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private boolean isValidDisplay(@Nullable Display display) { - if (display == null || display.getType() == Display.TYPE_OVERLAY) { + if (display == null) { return false; } // Private virtual displays are created by the ap and is not allowed to access by other diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java index 10dffb59317e..805d7f820c8d 100644 --- a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java @@ -65,9 +65,9 @@ public class HearingDevicePhoneCallNotificationController { private final Executor mCallbackExecutor; public HearingDevicePhoneCallNotificationController(@NonNull Context context) { - mTelephonyListener = new CallStateListener(context); mTelephonyManager = context.getSystemService(TelephonyManager.class); mCallbackExecutor = Executors.newSingleThreadExecutor(); + mTelephonyListener = new CallStateListener(context, mCallbackExecutor); } @VisibleForTesting @@ -109,14 +109,29 @@ public class HearingDevicePhoneCallNotificationController { AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, ""); private final Context mContext; + private final Executor mCommDeviceChangedExecutor; + private final AudioManager.OnCommunicationDeviceChangedListener mCommDeviceChangedListener; private NotificationManager mNotificationManager; private AudioManager mAudioManager; private BroadcastReceiver mHearingDeviceActionReceiver; private BluetoothDevice mHearingDevice; + private boolean mIsCommDeviceChangedRegistered = false; private boolean mIsNotificationShown = false; - CallStateListener(@NonNull Context context) { + CallStateListener(@NonNull Context context, @NonNull Executor executor) { mContext = context; + mCommDeviceChangedExecutor = executor; + mCommDeviceChangedListener = device -> { + if (device == null) { + return; + } + mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(device)); + if (mHearingDevice != null) { + showNotificationIfNeeded(); + } else { + dismissNotificationIfNeeded(); + } + }; } @Override @@ -134,6 +149,11 @@ public class HearingDevicePhoneCallNotificationController { } if (state == TelephonyManager.CALL_STATE_IDLE) { + if (mIsCommDeviceChangedRegistered) { + mIsCommDeviceChangedRegistered = false; + mAudioManager.removeOnCommunicationDeviceChangedListener( + mCommDeviceChangedListener); + } dismissNotificationIfNeeded(); if (mHearingDevice != null) { @@ -143,10 +163,23 @@ public class HearingDevicePhoneCallNotificationController { mHearingDevice = null; } if (state == TelephonyManager.CALL_STATE_OFFHOOK) { - mHearingDevice = getSupportedInputHearingDeviceInfo( - mAudioManager.getAvailableCommunicationDevices()); - if (mHearingDevice != null) { - showNotificationIfNeeded(); + if (com.android.server.accessibility.Flags.hearingInputChangeWhenCommDevice()) { + AudioDeviceInfo commDevice = mAudioManager.getCommunicationDevice(); + mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(commDevice)); + if (mHearingDevice != null) { + showNotificationIfNeeded(); + } else { + mAudioManager.addOnCommunicationDeviceChangedListener( + mCommDeviceChangedExecutor, + mCommDeviceChangedListener); + mIsCommDeviceChangedRegistered = true; + } + } else { + mHearingDevice = getSupportedInputHearingDeviceInfo( + mAudioManager.getAvailableCommunicationDevices()); + if (mHearingDevice != null) { + showNotificationIfNeeded(); + } } } } @@ -264,6 +297,10 @@ public class HearingDevicePhoneCallNotificationController { PendingIntent.FLAG_IMMUTABLE); } case ACTION_BLUETOOTH_DEVICE_DETAILS -> { + if (mHearingDevice == null) { + return null; + } + Bundle bundle = new Bundle(); bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress()); intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index cc93d0887d89..a71224a68125 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -124,6 +124,22 @@ public class AutoclickController extends BaseEventStreamTransformation { } }; + @VisibleForTesting + final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController = + new AutoclickScrollPanel.ScrollPanelControllerInterface() { + @Override + public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { + // TODO(b/388845721): Perform actual scroll. + } + + @Override + public void exitScrollMode() { + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + } + } + }; + public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) { mTrace = trace; mContext = context; @@ -168,7 +184,8 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager = mContext.getSystemService(WindowManager.class); mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController); - mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager); + mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager, + mScrollPanelController); mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java index 86f79a83ea28..e79be502a6fc 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import android.annotation.IntDef; import android.content.Context; import android.graphics.PixelFormat; import android.view.Gravity; @@ -25,23 +26,97 @@ import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; +import android.widget.ImageButton; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + public class AutoclickScrollPanel { + public static final int DIRECTION_UP = 0; + public static final int DIRECTION_DOWN = 1; + public static final int DIRECTION_LEFT = 2; + public static final int DIRECTION_RIGHT = 3; + + @IntDef({ + DIRECTION_UP, + DIRECTION_DOWN, + DIRECTION_LEFT, + DIRECTION_RIGHT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollDirection {} + private final Context mContext; private final View mContentView; private final WindowManager mWindowManager; + private ScrollPanelControllerInterface mScrollPanelController; + + // Scroll panel buttons. + private final ImageButton mUpButton; + private final ImageButton mDownButton; + private final ImageButton mLeftButton; + private final ImageButton mRightButton; + private final ImageButton mExitButton; + private boolean mInScrollMode = false; - public AutoclickScrollPanel(Context context, WindowManager windowManager) { + /** + * Interface for handling scroll operations. + */ + public interface ScrollPanelControllerInterface { + /** + * Called when a scroll direction is hovered. + * + * @param direction The direction to scroll: one of {@link ScrollDirection} values. + */ + void handleScroll(@ScrollDirection int direction); + + /** + * Called when the exit button is hovered. + */ + void exitScrollMode(); + } + + public AutoclickScrollPanel(Context context, WindowManager windowManager, + ScrollPanelControllerInterface controller) { mContext = context; mWindowManager = windowManager; + mScrollPanelController = controller; mContentView = LayoutInflater.from(context).inflate( R.layout.accessibility_autoclick_scroll_panel, null); + + // Initialize buttons. + mUpButton = mContentView.findViewById(R.id.scroll_up); + mLeftButton = mContentView.findViewById(R.id.scroll_left); + mRightButton = mContentView.findViewById(R.id.scroll_right); + mDownButton = mContentView.findViewById(R.id.scroll_down); + mExitButton = mContentView.findViewById(R.id.scroll_exit); + + initializeButtonState(); + } + + /** + * Sets up hover listeners for scroll panel buttons. + */ + private void initializeButtonState() { + // Set up hover listeners for direction buttons. + setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP); + setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT); + setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT); + setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN); + + // Set up hover listener for exit button. + mExitButton.setOnHoverListener((v, event) -> { + if (mScrollPanelController != null) { + mScrollPanelController.exitScrollMode(); + } + return true; + }); } /** @@ -67,6 +142,19 @@ public class AutoclickScrollPanel { } /** + * Sets up a hover listener for a direction button. + */ + private void setupHoverListenerForDirectionButton(ImageButton button, + @ScrollDirection int direction) { + button.setOnHoverListener((v, event) -> { + if (mScrollPanelController != null) { + mScrollPanelController.handleScroll(direction); + } + return true; + }); + } + + /** * Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window * Manager. */ diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index fb329430acb2..b02fe2752a62 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -653,6 +653,14 @@ public class TouchExplorer extends BaseEventStreamTransformation case ACTION_UP: handleActionUp(event, rawEvent, policyFlags); break; + case ACTION_POINTER_UP: + if (com.android.server.accessibility.Flags + .pointerUpMotionEventInTouchExploration()) { + if (mState.isServiceDetectingGestures()) { + mAms.sendMotionEventToListeningServices(rawEvent); + } + } + break; default: break; } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index cd46b38272c2..568abd196735 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -26,6 +26,8 @@ import android.view.Display; import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.VisibleForTesting; + import com.android.server.accessibility.AccessibilityManagerService; /** @@ -73,7 +75,8 @@ public class TouchState { private int mState = STATE_CLEAR; // Helper class to track received pointers. // Todo: collapse or hide this class so multiple classes don't modify it. - private final ReceivedPointerTracker mReceivedPointerTracker; + @VisibleForTesting + public final ReceivedPointerTracker mReceivedPointerTracker; // The most recently received motion event. private MotionEvent mLastReceivedEvent; // The accompanying raw event without any transformations. @@ -219,8 +222,19 @@ public class TouchState { startTouchInteracting(); break; case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END: - setState(STATE_CLEAR); - // We will clear when we actually handle the next ACTION_DOWN. + // When interaction ends, check if there are still down pointers. + // If there are any down pointers, go directly to TouchExploring instead. + if (com.android.server.accessibility.Flags + .pointerUpMotionEventInTouchExploration()) { + if (mReceivedPointerTracker.mReceivedPointersDown > 0) { + startTouchExploring(); + } else { + setState(STATE_CLEAR); + // We will clear when we actually handle the next ACTION_DOWN. + } + } else { + setState(STATE_CLEAR); + } break; case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: startTouchExploring(); @@ -419,7 +433,8 @@ public class TouchState { private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT]; // Which pointers are down. - private int mReceivedPointersDown; + @VisibleForTesting + public int mReceivedPointersDown; // The edge flags of the last received down event. private int mLastReceivedDownEdgeFlags; diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index 6ccf5e47ca6c..59566677b1fc 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -39,6 +39,14 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED; @@ -157,8 +165,24 @@ public final class PresentationStatsEventLogger { DETECTION_PREFER_PCC }) @Retention(RetentionPolicy.SOURCE) - public @interface DetectionPreference { - } + public @interface DetectionPreference {} + + /** + * The fill dialog not shown reason. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}. + */ + @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = { + FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN, + FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED, + FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD, + FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED, + FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION, + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED, + FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END, + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FillDialogNotShownReason {} public static final int NOT_SHOWN_REASON_ANY_SHOWN = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN; @@ -219,6 +243,25 @@ public final class PresentationStatsEventLogger { public static final int DETECTION_PREFER_PCC = AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC; + // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason + public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY; + + private static final int DEFAULT_VALUE_INT = -1; private final int mSessionId; @@ -871,6 +914,43 @@ public final class PresentationStatsEventLogger { } /** + * Set fill_dialog_not_shown_reason + * @param reason + */ + public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) { + mEventInternal.ifPresent(event -> { + if ((event.mFillDialogNotShownReason + == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END + || event.mFillDialogNotShownReason + == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason + == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) { + event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY; + } else { + event.mFillDialogNotShownReason = reason; + } + }); + } + + /** + * Set fill_dialog_ready_to_show_ms + * @param val + */ + public void maybeSetFillDialogReadyToShowMs(long val) { + mEventInternal.ifPresent(event -> { + event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp); + }); + } + + /** + * Set ime_animation_finish_ms + * @param val + */ + public void maybeSetImeAnimationFinishMs(long val) { + mEventInternal.ifPresent(event -> { + event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp); + }); + } + /** * Set the log contains relayout metrics. * This is being added as a temporary measure to add logging. * In future, when we map Session's old view states to the new autofill id's as part of fixing @@ -959,7 +1039,13 @@ public final class PresentationStatsEventLogger { + " event.notExpiringResponseDuringAuthCount=" + event.mFixExpireResponseDuringAuthCount + " event.notifyViewEnteredIgnoredDuringAuthCount=" - + event.mNotifyViewEnteredIgnoredDuringAuthCount); + + event.mNotifyViewEnteredIgnoredDuringAuthCount + + " event.fillDialogNotShownReason=" + + event.mFillDialogNotShownReason + + " event.fillDialogReadyToShowMs=" + + event.mFillDialogReadyToShowMs + + " event.imeAnimationFinishMs=" + + event.mImeAnimationFinishMs); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -1020,7 +1106,10 @@ public final class PresentationStatsEventLogger { event.mViewFilledSuccessfullyOnRefillCount, event.mViewFailedOnRefillCount, event.mFixExpireResponseDuringAuthCount, - event.mNotifyViewEnteredIgnoredDuringAuthCount); + event.mNotifyViewEnteredIgnoredDuringAuthCount, + event.mFillDialogNotShownReason, + event.mFillDialogReadyToShowMs, + event.mImeAnimationFinishMs); mEventInternal = Optional.empty(); } @@ -1087,6 +1176,9 @@ public final class PresentationStatsEventLogger { // Following are not logged and used only for internal logic boolean shouldResetShownCount = false; boolean mHasRelayoutLog = false; + @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN; + int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT; + int mImeAnimationFinishMs = DEFAULT_VALUE_INT; PresentationStatsEventInternal() {} } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6515b237519a..ff3bf2acb080 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -81,6 +81,13 @@ import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTIC import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS; import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION; import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED; @@ -5622,6 +5629,10 @@ final class Session synchronized (mLock) { final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN); + // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due + // to possible SHOW_FILL_DIALOG_WAIT. + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN); } // Just show fill dialog once per fill request, so disabled after shown. // Note: Cannot disable before requestShowFillDialog() because the method @@ -5725,6 +5736,15 @@ final class Session private boolean isFillDialogUiEnabled() { synchronized (mLock) { + if (mSessionFlags.mFillDialogDisabled) { + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED); + } + if (mSessionFlags.mScreenHasCredmanField) { + // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED". + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD); + } return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField; } } @@ -5789,6 +5809,8 @@ final class Session || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) { // Last fill dialog triggered ids are changed. if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed."); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED); return SHOW_FILL_DIALOG_NO; } @@ -5815,6 +5837,8 @@ final class Session // we need to wait for animation to happen. We can't return from here yet. // This is the situation #2 described above. Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog"); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION); mFillDialogRunnable = createFillDialogEvalRunnable( response, filledId, filterText, flags); return SHOW_FILL_DIALOG_WAIT; @@ -5824,9 +5848,15 @@ final class Session // max of start input time or the ime finish time long effectiveDuration = currentTimestampMs - Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs); + mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs( + currentTimestampMs); + mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs( + Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs)); if (effectiveDuration >= mFillDialogTimeoutMs) { Log.d(TAG, "Fill dialog not shown since IME has been up for more time than " + mFillDialogTimeoutMs + "ms"); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED); return SHOW_FILL_DIALOG_NO; } else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) { // we need to wait for some time after animation ends @@ -5834,6 +5864,8 @@ final class Session response, filledId, filterText, flags); mHandler.postDelayed(runnable, mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END); return SHOW_FILL_DIALOG_WAIT; } } diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index cd9285cdfe91..cbee8391458d 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -58,13 +58,16 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.R; import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.utils.PackageUtils; +import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Class responsible for handling incoming {@link AssociationRequest}s. @@ -130,6 +133,12 @@ public class AssociationRequestsProcessor { private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5; private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min; + // Set of profiles for which the association dialog cannot be skipped. + private static final Set<String> DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION = new ArraySet<>( + Arrays.asList( + AssociationRequest.DEVICE_PROFILE_APP_STREAMING, + AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING)); + private final @NonNull Context mContext; private final @NonNull PackageManagerInternal mPackageManagerInternal; private final @NonNull AssociationStore mAssociationStore; @@ -174,6 +183,7 @@ public class AssociationRequestsProcessor { // 2a. Check if association can be created without launching UI (i.e. CDM needs NEITHER // to perform discovery NOR to collect user consent). if (request.isSelfManaged() && !request.isForceConfirmation() + && !DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION.contains(request.getDeviceProfile()) && !willAddRoleHolder(request, packageName, userId)) { // 2a.1. Create association right away. createAssociationAndNotifyApplication(request, packageName, userId, diff --git a/services/core/Android.bp b/services/core/Android.bp index 14d9d3f0c0a1..decac40d20f8 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -122,10 +122,10 @@ genrule { } genrule { - name: "statslog-mediarouter-java-gen", - tools: ["stats-log-api-gen"], - cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog", - out: ["com/android/server/media/MediaRouterStatsLog.java"], + name: "statslog-mediarouter-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog", + out: ["com/android/server/media/MediaRouterStatsLog.java"], } java_library_static { @@ -138,6 +138,7 @@ java_library_static { "ondeviceintelligence_conditionally", ], srcs: [ + ":android.hardware.audio.effect-V1-java-source", ":android.hardware.tv.hdmi.connection-V1-java-source", ":android.hardware.tv.hdmi.earc-V1-java-source", ":android.hardware.tv.mediaquality-V1-java-source", diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java index 31704c442290..4e1d77c26129 100644 --- a/services/core/java/com/android/server/am/ConnectionRecord.java +++ b/services/core/java/com/android/server/am/ConnectionRecord.java @@ -142,6 +142,10 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS); } + @Override + public boolean transmitsCpuTime() { + return !hasFlag(Context.BIND_ALLOW_FREEZE); + } public long getFlags() { return flags; @@ -273,6 +277,9 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) { sb.append("CAPS "); } + if (hasFlag(Context.BIND_ALLOW_FREEZE)) { + sb.append("!CPU "); + } if (serviceDead) { sb.append("DEAD "); } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 336a35e7a7e3..fa35da30bf4b 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2802,7 +2802,7 @@ public class OomAdjuster { // we check the final procstate, and remove it if the procsate is below BFGS. capability |= getBfslCapabilityFromClient(client); - capability |= getCpuCapabilityFromClient(client); + capability |= getCpuCapabilityFromClient(cr, client); if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) { if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) { @@ -3259,7 +3259,7 @@ public class OomAdjuster { // we check the final procstate, and remove it if the procsate is below BFGS. capability |= getBfslCapabilityFromClient(client); - capability |= getCpuCapabilityFromClient(client); + capability |= getCpuCapabilityFromClient(conn, client); if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) { // If the other app is cached for any reason, for purposes here @@ -3502,10 +3502,13 @@ public class OomAdjuster { /** * @return the CPU capability from a client (of a service binding or provider). */ - private static int getCpuCapabilityFromClient(ProcessRecord client) { - // Just grant CPU capability every time - // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings. - return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME; + private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn, + ProcessRecord client) { + if (conn == null || conn.transmitsCpuTime()) { + return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME; + } else { + return 0; + } } /** diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 1b7e8f0bd244..7e7b5685cf13 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -635,6 +635,15 @@ public class OomAdjusterModernImpl extends OomAdjuster { * Returns true if this connection can propagate capabilities. */ boolean canAffectCapabilities(); + + /** + * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the + * client possesses it. + */ + default boolean transmitsCpuTime() { + // Always lend this capability by default. + return true; + } } /** diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 4b5f06b13885..8ef79a916530 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1472,8 +1472,8 @@ public class AudioDeviceBroker { mAudioService.postAccessoryPlugMediaUnmute(device); } - /*package*/ int getVolumeForDeviceIgnoreMute(int streamType, int device) { - return mAudioService.getVolumeForDeviceIgnoreMute(streamType, device); + /*package*/ int getVssVolumeForDevice(int streamType, int device) { + return mAudioService.getVssVolumeForDevice(streamType, device); } /*package*/ int getMaxVssVolumeForStream(int streamType) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 2e6d98485e85..829d9ea7495f 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -2482,7 +2482,7 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeHearingAidDeviceAvailable( String address, String name, int streamType, String eventSource) { - final int hearingAidVolIndex = mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, + final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); @@ -2672,7 +2672,7 @@ public class AudioDeviceInventory { } final int leAudioVolIndex = (volumeIndex == -1) - ? mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, device) + ? mDeviceBroker.getVssVolumeForDevice(streamType, device) : volumeIndex; final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType); mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a43e4d98c077..766456134b20 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -529,7 +529,7 @@ public class AudioService extends IAudioService.Stub */ private InputDeviceVolumeHelper mInputDeviceVolumeHelper; - /*package*/ int getVolumeForDeviceIgnoreMute(int stream, int device) { + /*package*/ int getVssVolumeForDevice(int stream, int device) { final VolumeStreamState streamState = mStreamStates.get(stream); return streamState != null ? streamState.getIndex(device) : -1; } @@ -4997,6 +4997,8 @@ public class AudioService extends IAudioService.Stub pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL"); + pw.println("\tandroid.media.audio.ringtoneUserUriCheck:" + + android.media.audio.Flags.ringtoneUserUriCheck()); pw.println("\tandroid.media.audio.roForegroundAudioControl:" + roForegroundAudioControl()); pw.println("\tandroid.media.audio.scoManagedByAudio:" @@ -5098,7 +5100,7 @@ public class AudioService extends IAudioService.Stub } final int device = absVolumeDevices.toArray(new Integer[0])[0].intValue(); - final int index = getVolumeForDeviceIgnoreMute(streamType, device); + final int index = getStreamVolume(streamType, device); if (DEBUG_VOL) { Slog.i(TAG, "onUpdateContextualVolumes streamType: " + streamType diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index 67afff79dffd..643f3308d8f5 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -724,7 +724,7 @@ public class SoundDoseHelper { int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC); if (safeDevicesContains(device) && isStreamActive) { scheduleMusicActiveCheck(); - int index = mAudioService.getVolumeForDeviceIgnoreMute(AudioSystem.STREAM_MUSIC, + int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC, device); if (index > safeMediaVolumeIndex(device)) { // Approximate cumulative active music time diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 97f9a7c4f2b0..8f5b831ca0b4 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -219,7 +219,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { && reason != HdmiControlService.INITIATED_BY_BOOT_UP; List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE); - if (bufferedActiveSource.isEmpty()) { + List<HdmiCecMessage> bufferedActiveSourceFromService = mService.getCecMessageWithOpcode( + Constants.MESSAGE_ACTIVE_SOURCE); + if (bufferedActiveSource.isEmpty() && bufferedActiveSourceFromService.isEmpty()) { addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) { diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 6d973ac8d1b5..fdd0ef2f90e1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1593,6 +1593,17 @@ public class HdmiControlService extends SystemService { this.mCecMessageBuffer = cecMessageBuffer; } + List<HdmiCecMessage> getCecMessageWithOpcode(int opcode) { + List<HdmiCecMessage> cecMessagesWithOpcode = new ArrayList<>(); + List<HdmiCecMessage> cecMessages = mCecMessageBuffer.getBuffer(); + for (HdmiCecMessage message: cecMessages) { + if (message.getOpcode() == opcode) { + cecMessagesWithOpcode.add(message); + } + } + return cecMessagesWithOpcode; + } + /** * Returns {@link Looper} of main thread. Use this {@link Looper} instance * for tasks that are running on main service thread. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index c2fecf283a34..d9db178e0dc2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -568,6 +568,7 @@ public class InputManagerService extends IInputManager.Stub } mWindowManagerCallbacks = callbacks; registerLidSwitchCallbackInternal(mWindowManagerCallbacks); + mKeyGestureController.setWindowManagerCallbacks(callbacks); } public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) { @@ -2756,24 +2757,6 @@ public class InputManagerService extends IInputManager.Stub @Nullable IBinder focussedToken) { return InputManagerService.this.handleKeyGestureEvent(event); } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP: - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN: - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS: - return true; - default: - return false; - - } - } }); } @@ -3371,6 +3354,11 @@ public class InputManagerService extends IInputManager.Stub */ @Nullable SurfaceControl createSurfaceForGestureMonitor(String name, int displayId); + + /** + * Provide information on whether the keyguard is currently locked or not. + */ + boolean isKeyguardLocked(int displayId); } /** diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index ef5babf19d83..395c77322c04 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -62,8 +62,10 @@ import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.ViewConfiguration; import com.android.internal.R; +import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; @@ -104,6 +106,7 @@ final class KeyGestureController { private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1; private static final int MSG_PERSIST_CUSTOM_GESTURES = 2; private static final int MSG_LOAD_CUSTOM_GESTURES = 3; + private static final int MSG_ACCESSIBILITY_SHORTCUT = 4; // must match: config_settingsKeyBehavior in config.xml private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0; @@ -122,12 +125,15 @@ final class KeyGestureController { static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2; private final Context mContext; + private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks; private final Handler mHandler; private final Handler mIoHandler; private final int mSystemPid; private final KeyCombinationManager mKeyCombinationManager; private final SettingsObserver mSettingsObserver; private final AppLaunchShortcutManager mAppLaunchShortcutManager; + @VisibleForTesting + final AccessibilityShortcutController mAccessibilityShortcutController; private final InputGestureManager mInputGestureManager; private final DisplayManager mDisplayManager; @GuardedBy("mInputDataStore") @@ -175,8 +181,14 @@ final class KeyGestureController { private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); - KeyGestureController(Context context, Looper looper, Looper ioLooper, + public KeyGestureController(Context context, Looper looper, Looper ioLooper, InputDataStore inputDataStore) { + this(context, looper, ioLooper, inputDataStore, new Injector()); + } + + @VisibleForTesting + KeyGestureController(Context context, Looper looper, Looper ioLooper, + InputDataStore inputDataStore, Injector injector) { mContext = context; mHandler = new Handler(looper, this::handleMessage); mIoHandler = new Handler(ioLooper, this::handleIoMessage); @@ -197,6 +209,8 @@ final class KeyGestureController { mSettingsObserver = new SettingsObserver(mHandler); mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); + mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext, + mHandler); mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class)); mInputDataStore = inputDataStore; mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -295,8 +309,8 @@ final class KeyGestureController { KeyEvent.KEYCODE_VOLUME_UP) { @Override public boolean preCondition() { - return isKeyGestureSupported( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD); + return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( + mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY)); } @Override @@ -376,15 +390,15 @@ final class KeyGestureController { KeyEvent.KEYCODE_DPAD_DOWN) { @Override public boolean preCondition() { - return isKeyGestureSupported( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD); + return mAccessibilityShortcutController + .isAccessibilityShortcutAvailable(false); } @Override public void execute() { handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); } @@ -392,7 +406,7 @@ final class KeyGestureController { public void cancel() { handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, KeyGestureEvent.FLAG_CANCELLED); } @@ -438,6 +452,7 @@ final class KeyGestureController { mSettingsObserver.observe(); mAppLaunchShortcutManager.systemRunning(); mInputGestureManager.systemRunning(); + initKeyGestures(); int userId; synchronized (mUserLock) { @@ -447,6 +462,27 @@ final class KeyGestureController { mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } + @SuppressLint("MissingPermission") + private void initKeyGestures() { + InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + im.registerKeyGestureEventHandler((event, focusedToken) -> { + switch (event.getKeyGestureType()) { + case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: + if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT), + getAccessibilityShortcutTimeout()); + } else { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + } + return true; + default: + return false; + } + }); + } + public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { return false; @@ -971,17 +1007,6 @@ final class KeyGestureController { return false; } - private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - synchronized (mKeyGestureHandlerRecords) { - for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { - if (handler.isKeyGestureSupported(gestureType)) { - return true; - } - } - } - return false; - } - public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally @@ -1019,9 +1044,16 @@ final class KeyGestureController { synchronized (mUserLock) { mCurrentUserId = userId; } + mAccessibilityShortcutController.setCurrentUser(userId); mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } + + public void setWindowManagerCallbacks( + @NonNull InputManagerService.WindowManagerCallbacks callbacks) { + mWindowManagerCallbacks = callbacks; + } + private boolean isDefaultDisplayOn() { Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); if (defaultDisplay == null) { @@ -1068,6 +1100,9 @@ final class KeyGestureController { AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj; notifyKeyGestureEvent(event); break; + case MSG_ACCESSIBILITY_SHORTCUT: + mAccessibilityShortcutController.performAccessibilityShortcut(); + break; } return true; } @@ -1347,17 +1382,6 @@ final class KeyGestureController { } return false; } - - public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - try { - return mKeyGestureHandler.isKeyGestureSupported(gestureType); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to identify if key gesture type is supported by the " - + "process " + mPid + ", assuming it died.", ex); - binderDied(); - } - return false; - } } private class SettingsObserver extends ContentObserver { @@ -1413,6 +1437,25 @@ final class KeyGestureController { return event; } + private long getAccessibilityShortcutTimeout() { + synchronized (mUserLock) { + final ViewConfiguration config = ViewConfiguration.get(mContext); + final boolean hasDialogShown = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0; + final boolean skipTimeoutRestriction = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, + 0, mCurrentUserId) != 0; + + // If users manually set the volume key shortcut for any accessibility service, the + // system would bypass the timeout restriction of the shortcut dialog. + return hasDialogShown || skipTimeoutRestriction + ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation() + : config.getAccessibilityShortcutKeyTimeout(); + } + } + public void dump(IndentingPrintWriter ipw) { ipw.println("KeyGestureController:"); ipw.increaseIndent(); @@ -1459,4 +1502,12 @@ final class KeyGestureController { mAppLaunchShortcutManager.dump(ipw); mInputGestureManager.dump(ipw); } + + @VisibleForTesting + static class Injector { + AccessibilityShortcutController getAccessibilityShortcutController(Context context, + Handler handler) { + return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM); + } + } } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java index 02987a98417f..15f186b047f2 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -132,8 +132,8 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod); + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod); InputBindResult startInputOrWindowGainedFocus( @StartInputReason int startInputReason, IInputMethodClient client, @@ -142,7 +142,7 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher); + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible); void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode); @@ -324,11 +324,11 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { return mCallback.startInputOrWindowGainedFocus( startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId, imeDispatcher); + unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible); } @Override @@ -340,13 +340,13 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { mCallback.startInputOrWindowGainedFocusAsync( startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId, imeDispatcher, startInputSeq, - useAsyncShowHideMethod); + unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible, + startInputSeq, useAsyncShowHideMethod); } @Override diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index 69353becc692..2c07a3179344 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -443,7 +443,8 @@ public final class ImeVisibilityStateComputer { } @GuardedBy("ImfLock.class") - ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) { + ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible, + boolean imeRequestedVisible) { // TODO: Output the request IME visibility state according to the requested window state final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE; // Should we auto-show the IME even if the caller has not @@ -576,7 +577,8 @@ public final class ImeVisibilityStateComputer { SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } } - if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus() + if (!state.hasEditorFocused() && (mInputShown || (Flags.refactorInsetsController() + && imeRequestedVisible)) && state.isStartInputByGainFocus() && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) { // Hide the soft-keyboard when the system do nothing for softInputModeState // of the window being gained focus without an editor. This behavior benefits diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 68ad8f7e9433..23757757e336 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3725,8 +3725,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { // implemented by ZeroJankProxy } @@ -3739,7 +3739,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { if (UserHandle.getCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); @@ -3870,7 +3870,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs); + unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs, + imeRequestedVisible); } finally { Binder.restoreCallingIdentity(ident); } @@ -3899,7 +3900,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @NonNull InputMethodBindingController bindingController, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs, + boolean imeRequestedVisible) { ProtoLog.v(IMMS_DEBUG, "startInputOrWindowGainedFocusInternalLocked: reason=%s" + " client=%s" + " inputContext=%s" @@ -3910,12 +3912,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. + " unverifiedTargetSdkVersion=%s" + " bindingController=%s" + " imeDispatcher=%s" - + " cs=%s", + + " cs=%s" + + " imeRequestedVisible=%s", InputMethodDebug.startInputReasonToString(startInputReason), client.asBinder(), inputContext, editorInfo, InputMethodDebug.startInputFlagsToString(startInputFlags), InputMethodDebug.softInputModeToString(softInputMode), Integer.toHexString(windowFlags), unverifiedTargetSdkVersion, bindingController, - imeDispatcher, cs); + imeDispatcher, cs, imeRequestedVisible); final int userId = bindingController.getUserId(); final var userData = getUserData(userId); @@ -3963,7 +3966,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputBindResult res = null; final ImeVisibilityResult imeVisRes = visibilityStateComputer.computeState(windowState, - isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)); + isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags), + imeRequestedVisible); if (imeVisRes != null) { boolean isShow = false; switch (imeVisRes.getReason()) { diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 72529254545e..12c1d9cbb2a1 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -234,15 +234,15 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { offload(() -> { InputBindResult result = mInner.startInputOrWindowGainedFocus(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, - userId, imeDispatcher); + userId, imeDispatcher, imeRequestedVisible); sendOnStartInputResult(client, result, startInputSeq); // For first-time client bind, MSG_BIND should arrive after MSG_START_INPUT_RESULT. if (result.result == InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION) { @@ -269,7 +269,7 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { // Should never be called when flag is enabled i.e. when this proxy is used. return null; } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index f137de1b3e1d..988924d9f498 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -25,6 +25,7 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; + import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE; @@ -63,6 +64,7 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -76,18 +78,21 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; + import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; + import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -551,6 +556,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithRouter2Locked(router, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithRouter2Locked(router); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + // End of methods that implement MediaRouter2 operations. // Start of methods that implement MediaRouter2Manager operations. @@ -805,6 +840,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithManagerLocked(manager, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithManagerLocked(manager); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public boolean showMediaOutputSwitcherWithProxyRouter( @NonNull IMediaRouter2Manager proxyRouter) { @@ -1582,6 +1647,61 @@ class MediaRouter2ServiceImpl { DUMMY_REQUEST_ID, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown router: %s", router)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | router: %d suggestion: %d", + routerRecord.mPackageName, suggestedDeviceInfo)); + + routerRecord.mUserRecord.updateDeviceSuggestionsLocked( + routerRecord.mPackageName, routerRecord.mPackageName, suggestedDeviceInfo); + routerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + routerRecord.mUserRecord.mHandler, + routerRecord.mPackageName, + routerRecord.mPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown router: %s", router)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestions | router: %d", routerRecord.mPackageName)); + + return routerRecord.mUserRecord.getDeviceSuggestionsLocked(routerRecord.mPackageName); + } + // End of locked methods that are used by MediaRouter2. // Start of locked methods that are used by MediaRouter2Manager. @@ -1972,6 +2092,68 @@ class MediaRouter2ServiceImpl { uniqueRequestId, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown manager: %s", manager)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | manager: %d, suggestingPackageName: %d suggestion:" + + " %d", + managerRecord.mManagerId, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + + managerRecord.mUserRecord.updateDeviceSuggestionsLocked( + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo); + managerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + managerRecord.mUserRecord.mHandler, + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown manager: %s", manager)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestionsWithManagerLocked | manager: %d", + managerRecord.mManagerId)); + + return managerRecord.mUserRecord.getDeviceSuggestionsLocked( + managerRecord.mTargetPackageName); + } + // End of locked methods that are used by MediaRouter2Manager. // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. @@ -2047,6 +2229,11 @@ class MediaRouter2ServiceImpl { //TODO: make records private for thread-safety final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>(); final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>(); + + // @GuardedBy("mLock") + private final Map<String, Map<String, List<SuggestedDeviceInfo>>> mDeviceSuggestions = + new HashMap<>(); + RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY; Set<String> mActivelyScanningPackages = Set.of(); final UserHandler mHandler; @@ -2076,6 +2263,25 @@ class MediaRouter2ServiceImpl { return null; } + // @GuardedBy("mLock") + public void updateDeviceSuggestionsLocked( + String packageName, + String suggestingPackageName, + List<SuggestedDeviceInfo> deviceSuggestions) { + mDeviceSuggestions.putIfAbsent( + packageName, new HashMap<String, List<SuggestedDeviceInfo>>()); + Map<String, List<SuggestedDeviceInfo>> suggestions = + mDeviceSuggestions.get(packageName); + suggestions.put(suggestingPackageName, deviceSuggestions); + } + + // @GuardedBy("mLock") + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsLocked( + String packageName) { + return mDeviceSuggestions.get(packageName); + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "UserRecord"); @@ -2314,6 +2520,15 @@ class MediaRouter2ServiceImpl { } } + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) { + try { + mRouter.notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + logRemoteException("notifyDeviceSuggestionsUpdated", ex); + } + } + /** * Sends the corresponding router a {@link RoutingSessionInfo session} creation request, * with the given {@link MediaRoute2Info} as the initial member. @@ -3556,6 +3771,41 @@ class MediaRouter2ServiceImpl { // need to update routers other than the one making the update. } + private void notifyDeviceSuggestionsUpdatedOnHandler( + String routerPackageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + List<IMediaRouter2Manager> managers = new ArrayList<>(); + synchronized (service.mLock) { + for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { + if (TextUtils.equals(managerRecord.mTargetPackageName, routerPackageName)) { + managers.add(managerRecord.mManager); + } + } + for (IMediaRouter2Manager manager : managers) { + try { + manager.notifyDeviceSuggestionsUpdated( + routerPackageName, suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + Slog.w( + TAG, + "Failed to notify suggesteion changed. Manager probably died.", + ex); + } + } + for (RouterRecord routerRecord : mUserRecord.mRouterRecords) { + if (TextUtils.equals(routerRecord.mPackageName, routerPackageName)) { + routerRecord.notifyDeviceSuggestionsUpdated( + suggestingPackageName, suggestedDeviceInfo); + } + } + } + } + private void updateDiscoveryPreferenceOnHandler() { MediaRouter2ServiceImpl service = mServiceRef.get(); if (service == null) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 35bb19943a24..11f449e790a8 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -49,6 +49,7 @@ import android.media.RemoteDisplayState.RemoteDisplayInfo; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -80,6 +81,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -526,6 +528,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override + public void setDeviceSuggestionsWithRouter2( + IMediaRouter2 router, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithRouter2(router, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router) { + return mService2.getDeviceSuggestionsWithRouter2(router); + } + + // Binder call + @Override public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) { return mService2.getRemoteSessions(manager); } @@ -666,6 +683,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub return mService2.showMediaOutputSwitcherWithProxyRouter(proxyRouter); } + // Binder call + @Override + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithManager(manager, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager) { + return mService2.getDeviceSuggestionsWithManager(manager); + } + void restoreBluetoothA2dp() { try { boolean a2dpOn; diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 9e38435ff7f1..ad108f64ffe3 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -28,6 +28,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.hardware.audio.effect.DefaultExtension; import android.hardware.tv.mediaquality.AmbientBacklightColorFormat; import android.hardware.tv.mediaquality.IMediaQuality; import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener; @@ -57,6 +58,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.os.Parcel; import android.os.PersistableBundle; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -365,13 +367,21 @@ public class MediaQualityService extends SystemService { try { if (mMediaQuality != null) { + PictureParameters pp = new PictureParameters(); PictureParameter[] pictureParameters = MediaQualityUtils .convertPersistableBundleToPictureParameterList(params); - PictureParameters pp = new PictureParameters(); + PersistableBundle vendorPictureParameters = params + .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS); + Parcel parcel = Parcel.obtain(); + if (vendorPictureParameters != null) { + setVendorPictureParameters(pp, parcel, vendorPictureParameters); + } + pp.pictureParameters = pictureParameters; mMediaQuality.sendDefaultPictureParameters(pp); + parcel.recycle(); return true; } } catch (RemoteException e) { @@ -1419,11 +1429,19 @@ public class MediaQualityService extends SystemService { MediaQualityUtils.convertPersistableBundleToPictureParameterList( params); + PersistableBundle vendorPictureParameters = params + .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS); + Parcel parcel = Parcel.obtain(); + if (vendorPictureParameters != null) { + setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters); + } + android.hardware.tv.mediaquality.PictureProfile toReturn = new android.hardware.tv.mediaquality.PictureProfile(); toReturn.pictureProfileId = id; toReturn.parameters = pictureParameters; + parcel.recycle(); return toReturn; } @@ -1729,4 +1747,16 @@ public class MediaQualityService extends SystemService { return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION; } } + + private void setVendorPictureParameters( + PictureParameters pictureParameters, + Parcel parcel, + PersistableBundle vendorPictureParameters) { + vendorPictureParameters.writeToParcel(parcel, 0); + byte[] vendorBundleToByteArray = parcel.marshall(); + DefaultExtension defaultExtension = new DefaultExtension(); + defaultExtension.bytes = Arrays.copyOf( + vendorBundleToByteArray, vendorBundleToByteArray.length); + pictureParameters.vendorPictureParameters.setParcelable(defaultExtension); + } } diff --git a/services/core/java/com/android/server/os/instrumentation/OWNERS b/services/core/java/com/android/server/os/instrumentation/OWNERS new file mode 100644 index 000000000000..2522426d93f8 --- /dev/null +++ b/services/core/java/com/android/server/os/instrumentation/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/UprobeStats:/OWNERS
\ No newline at end of file diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 46dc75817a36..3230e891db55 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4240,66 +4240,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (!useKeyGestureEventHandler()) { return; } - mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() { - @Override - public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, - @Nullable IBinder focusedToken) { - boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, - focusedToken); - if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( - (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { - mPowerKeyHandled = true; - } - return handled; - } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: - case KeyGestureEvent.KEY_GESTURE_TYPE_HOME: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL: - case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT: - case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT: - case KeyGestureEvent.KEY_GESTURE_TYPE_BACK: - case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION: - case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE: - case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT: - case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT: - case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER: - case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP: - case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN: - case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER: - case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT: - case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB: - case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD: - case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD: - case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: - return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: - return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( - isKeyguardLocked()); - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( - false); - default: - return false; - } + mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> { + boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, + focusedToken); + if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( + (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { + mPowerKeyHandled = true; } + return handled; }); } @@ -4457,13 +4405,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { cancelPendingScreenshotChordAction(); } return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: - if (start) { - interceptAccessibilityShortcutChord(); - } else { - cancelPendingAccessibilityShortcutAction(); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD: if (start) { interceptRingerToggleChord(); @@ -4481,14 +4422,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { cancelGlobalActionsAction(); } return true; - // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - if (start) { - interceptAccessibilityGestureTv(); - } else { - cancelAccessibilityGestureTv(); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: if (start) { interceptBugreportGestureTv(); diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 1299a4d86623..f243d4fa825a 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -153,7 +153,7 @@ final class AccessibilityController { final DisplayContent dc = mService.mRoot.getDisplayContent(displayId); if (dc != null) { final Display display = dc.getDisplay(); - if (display != null && display.getType() != Display.TYPE_OVERLAY) { + if (display != null) { final DisplayMagnifier magnifier = new DisplayMagnifier( mService, dc, display, callbacks); magnifier.notifyImeWindowVisibilityChanged( diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 3f24da9d89f2..51025d204b46 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -60,6 +60,7 @@ import com.android.server.wm.ActivityStarter.DefaultFactory; import com.android.server.wm.ActivityStarter.Factory; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; /** @@ -97,6 +98,9 @@ public class ActivityStartController { /** Whether an {@link ActivityStarter} is currently executing (starting an Activity). */ private boolean mInExecution = false; + /** The {@link TaskDisplayArea}s that are currently starting home activity. */ + private ArrayList<TaskDisplayArea> mHomeLaunchingTaskDisplayAreas = new ArrayList<>(); + /** * TODO(b/64750076): Capture information necessary for dump and * {@link #postStartActivityProcessingForLastStarter} rather than keeping the entire object @@ -162,6 +166,11 @@ public class ActivityStartController { void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason, TaskDisplayArea taskDisplayArea) { + if (mHomeLaunchingTaskDisplayAreas.contains(taskDisplayArea)) { + Slog.e(TAG, "Abort starting home on " + taskDisplayArea + " recursively."); + return; + } + final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN); if (!ActivityRecord.isResolverActivity(aInfo.name)) { @@ -186,13 +195,18 @@ public class ActivityStartController { mSupervisor.endDeferResume(); } - mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason) - .setOutActivity(tmpOutRecord) - .setCallingUid(0) - .setActivityInfo(aInfo) - .setActivityOptions(options.toBundle(), - Binder.getCallingPid(), Binder.getCallingUid()) - .execute(); + try { + mHomeLaunchingTaskDisplayAreas.add(taskDisplayArea); + mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason) + .setOutActivity(tmpOutRecord) + .setCallingUid(0) + .setActivityInfo(aInfo) + .setActivityOptions(options.toBundle(), + Binder.getCallingPid(), Binder.getCallingUid()) + .execute(); + } finally { + mHomeLaunchingTaskDisplayAreas.remove(taskDisplayArea); + } mLastHomeActivityStartRecord = tmpOutRecord[0]; if (rootHomeTask.mInResumeTopActivity) { // If we are in resume section already, home activity will be initialized, but not @@ -479,9 +493,9 @@ public class ActivityStartController { } } catch (SecurityException securityException) { ActivityStarter.logAndThrowExceptionForIntentRedirect(mService.mContext, - "Creator URI Grant Caused Exception.", intent, creatorUid, - creatorPackage, filterCallingUid, callingPackage, - securityException); + ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION, + intent, creatorUid, creatorPackage, filterCallingUid, + callingPackage, securityException); } } if ((aInfo.applicationInfo.privateFlags @@ -720,6 +734,12 @@ public class ActivityStartController { } } + if (!mHomeLaunchingTaskDisplayAreas.isEmpty()) { + dumped = true; + pw.print(prefix); + pw.println("mHomeLaunchingTaskDisplayAreas:" + mHomeLaunchingTaskDisplayAreas); + } + if (!dumped) { pw.print(prefix); pw.println("(nothing)"); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 233f91385ca4..a84a008f66eb 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -65,6 +65,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; +import static com.android.internal.util.FrameworkStatsLog.INTENT_REDIRECT_BLOCKED; import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; @@ -140,6 +141,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.ProtoLog; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.UiThread; import com.android.server.am.ActivityManagerService.IntentCreatorToken; import com.android.server.am.PendingIntentRecord; @@ -623,7 +625,7 @@ class ActivityStarter { if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN) != 0) { logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, - "Unparceled intent does not have a creator token set.", intent, + ActivityStarter.INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN, intent, intentCreatorUid, intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage, null); } @@ -659,9 +661,9 @@ class ActivityStarter { } } catch (SecurityException securityException) { logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, - "Creator URI Grant Caused Exception.", intent, intentCreatorUid, - intentCreatorPackage, resolvedCallingUid, - resolvedCallingPackage, securityException); + ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION, + intent, intentCreatorUid, intentCreatorPackage, + resolvedCallingUid, resolvedCallingPackage, securityException); } } } else { @@ -683,9 +685,9 @@ class ActivityStarter { } } catch (SecurityException securityException) { logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, - "Creator URI Grant Caused Exception.", intent, intentCreatorUid, - intentCreatorPackage, resolvedCallingUid, - resolvedCallingPackage, securityException); + ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION, + intent, intentCreatorUid, intentCreatorPackage, + resolvedCallingUid, resolvedCallingPackage, securityException); } } } @@ -1109,8 +1111,11 @@ class ActivityStarter { if (sourceRecord != null) { if (requestCode >= 0 && !sourceRecord.finishing) { resultRecord = sourceRecord; + request.logMessage.append(" (rr="); + } else { + request.logMessage.append(" (sr="); } - request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")"); + request.logMessage.append(System.identityHashCode(sourceRecord) + ")"); } } @@ -1261,27 +1266,27 @@ class ActivityStarter { request.ignoreTargetSecurity, inTask != null, null, resultRecord, resultRootTask)) { abort = logAndAbortForIntentRedirect(mService.mContext, - "Creator checkStartAnyActivityPermission Caused abortion.", + ActivityStarter.INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } } catch (SecurityException e) { logAndThrowExceptionForIntentRedirect(mService.mContext, - "Creator checkStartAnyActivityPermission Caused Exception.", + ActivityStarter.INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage, e); } if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid, 0, resolvedType, aInfo.applicationInfo)) { abort = logAndAbortForIntentRedirect(mService.mContext, - "Creator IntentFirewall.checkStartActivity Caused abortion.", + ActivityStarter.INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } if (!mService.getPermissionPolicyInternal().checkStartActivity(intent, intentCreatorUid, intentCreatorPackage)) { abort = logAndAbortForIntentRedirect(mService.mContext, - "Creator PermissionPolicyService.checkStartActivity Caused abortion.", + ActivityStarter.INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } } @@ -3626,13 +3631,41 @@ class ActivityStarter { pw.println(mInTaskFragment); } + /** + * Error codes for intent redirect. + * + * @hide + */ + @IntDef(prefix = {"INTENT_REDIRECT_"}, value = { + INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN, + INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION, + INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION, + INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION, + INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY, + INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY, + }) + @Retention(RetentionPolicy.SOURCE) + @interface IntentRedirectErrorCode { + } + + /** + * Error codes for intent redirect issues + */ + static final int INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN = 1; + static final int INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION = 2; + static final int INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION = 3; + static final int INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION = 4; + static final int INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY = 5; + static final int INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY = 6; + static void logAndThrowExceptionForIntentRedirect(@NonNull Context context, - @NonNull String message, @NonNull Intent intent, int intentCreatorUid, + @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage, @Nullable SecurityException originalException) { - String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, + String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); Slog.wtf(TAG, msg); + FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode); if (preventIntentRedirectShowToast()) { UiThread.getHandler().post( () -> Toast.makeText(context, @@ -3646,12 +3679,13 @@ class ActivityStarter { } private static boolean logAndAbortForIntentRedirect(@NonNull Context context, - @NonNull String message, @NonNull Intent intent, int intentCreatorUid, + @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage) { - String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, + String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); Slog.wtf(TAG, msg); + FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode); if (preventIntentRedirectShowToast()) { UiThread.getHandler().post( () -> Toast.makeText(context, @@ -3662,11 +3696,38 @@ class ActivityStarter { ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid); } - private static String getIntentRedirectPreventedLogMessage(@NonNull String message, + private static String getIntentRedirectPreventedLogMessage( + @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage) { + String message = getIntentRedirectErrorMessageFromCode(errorCode); return "[IntentRedirect Hardening] " + message + " intentCreatorUid: " + intentCreatorUid + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid + "; callingPackage: " + callingPackage + "; intent: " + intent; } + + private static String getIntentRedirectErrorMessageFromCode( + @IntentRedirectErrorCode int errorCode) { + return switch (errorCode) { + case INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN -> + "INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN" + + " (Unparceled intent does not have a creator token set, throw exception.)"; + case INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION -> + "INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION" + + " (Creator URI permission grant throw exception.)"; + case INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION -> + "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION" + + " (Creator checkStartAnyActivityPermission, throw exception)"; + case INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION -> + "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION" + + " (Creator checkStartAnyActivityPermission, abort)"; + case INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY -> + "INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY" + + " (Creator IntentFirewall.checkStartActivity, abort)"; + case INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY -> + "INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY" + + " (Creator PermissionPolicyService.checkStartActivity, abort)"; + default -> "Unknown error code: " + errorCode; + }; + } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 353ccd5836c8..42b63d125d6b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -7104,6 +7104,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp public void setAnimatingTypes(@InsetsType int animatingTypes) { if (mAnimatingTypes != animatingTypes) { mAnimatingTypes = animatingTypes; + + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + getInsetsStateController().onAnimatingTypesChanged(this); + } } } } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 2cac63c1e5e9..040bbe46c3aa 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -263,8 +263,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { boolean oldVisibility = mSource.isVisible(); super.updateVisibility(); if (Flags.refactorInsetsController()) { - if (mSource.isVisible() && !oldVisibility && mImeRequester != null) { - reportImeDrawnForOrganizerIfNeeded(mImeRequester); + if (mSource.isVisible() && !oldVisibility && mControlTarget != null) { + reportImeDrawnForOrganizerIfNeeded(mControlTarget); } } onSourceChanged(); @@ -288,11 +288,15 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // If insets target is not available (e.g. RemoteInsetsControlTarget), use current // IME input target to update IME request state. For example, switch from a task // with showing IME to a split-screen task without showing IME. - InsetsTarget insetsTarget = target.getWindow(); - if (insetsTarget == null && mServerVisible) { - insetsTarget = mDisplayContent.getImeInputTarget(); + InputTarget imeInputTarget = mDisplayContent.getImeInputTarget(); + if (imeInputTarget != target && imeInputTarget != null) { + // The controlTarget should be updated with the visibility of the + // current IME input target. + reportImeInputTargetStateToControlTarget(imeInputTarget, target, + statsToken); + } else { + invokeOnImeRequestedChangedListener(target, statsToken); } - invokeOnImeRequestedChangedListener(insetsTarget, statsToken); } } } @@ -328,8 +332,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { if (changed) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_UPDATE_CLIENT_VISIBILITY); - invokeOnImeRequestedChangedListener(mDisplayContent.getImeInputTarget(), - statsToken); + invokeOnImeRequestedChangedListener(controlTarget, statsToken); } else { // TODO(b/353463205) check cancelled / failed ImeTracker.forLogging().onCancelled(statsToken, @@ -383,7 +386,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // not all virtual displays have an ImeInsetsSourceProvider, so it is not // guaranteed that the IME will be started when the control target reports its // requested visibility back. Thus, invoking the listener here. - invokeOnImeRequestedChangedListener(imeInsetsTarget, statsToken); + invokeOnImeRequestedChangedListener((InsetsControlTarget) imeInsetsTarget, + statsToken); } else { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY); @@ -392,18 +396,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - private void invokeOnImeRequestedChangedListener(InsetsTarget insetsTarget, + private void invokeOnImeRequestedChangedListener(InsetsControlTarget controlTarget, @Nullable ImeTracker.Token statsToken) { final var imeListener = mDisplayContent.mWmService.mOnImeRequestedChangedListener; if (imeListener != null) { - if (insetsTarget != null) { + if (controlTarget != null) { + final boolean imeAnimating = Flags.reportAnimatingInsetsTypes() + && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0; ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY); mDisplayContent.mWmService.mH.post(() -> { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER); - imeListener.onImeRequestedChanged(insetsTarget.getWindowToken(), - insetsTarget.isRequestedVisible(WindowInsets.Type.ime()), statsToken); + imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), + controlTarget.isRequestedVisible(WindowInsets.Type.ime()) + || imeAnimating, statsToken); }); } else { ImeTracker.forLogging().onFailed(statsToken, @@ -416,6 +423,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { } } + @Override + void onAnimatingTypesChanged(InsetsControlTarget caller) { + if (Flags.reportAnimatingInsetsTypes()) { + final InsetsControlTarget controlTarget = getControlTarget(); + // If the IME is not being requested anymore and the animation is finished, we need to + // invoke the listener, to let IMS eventually know + if (caller != null && caller == controlTarget && !caller.isRequestedVisible( + WindowInsets.Type.ime()) + && (caller.getAnimatingTypes() & WindowInsets.Type.ime()) == 0) { + // TODO(b/353463205) check statsToken + invokeOnImeRequestedChangedListener(caller, null); + } + } + } + private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) { final WindowState callerWindow = caller.getWindow(); if (callerWindow == null) { diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index 7751ac3f9fc6..a4bc5cbcb5d3 100644 --- a/services/core/java/com/android/server/wm/InputManagerCallback.java +++ b/services/core/java/com/android/server/wm/InputManagerCallback.java @@ -343,6 +343,13 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal } } + @Override + public boolean isKeyguardLocked(int displayId) { + synchronized (mService.mGlobalLock) { + return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId); + } + } + /** Waits until the built-in input devices have been configured. */ public boolean waitForInputDevicesReady(long timeoutMillis) { synchronized (mInputDevicesReadyMonitor) { diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index b7489029768a..1b693fc05b21 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -673,6 +673,9 @@ class InsetsSourceProvider { mServerVisible, mClientVisible); } + void onAnimatingTypesChanged(InsetsControlTarget caller) { + } + protected boolean isLeashReadyForDispatching() { return isLeashInitialized(); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 5e0395f70e65..810e48f492e1 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -393,6 +393,13 @@ class InsetsStateController { } } + void onAnimatingTypesChanged(InsetsControlTarget target) { + for (int i = mProviders.size() - 1; i >= 0; i--) { + final InsetsSourceProvider provider = mProviders.valueAt(i); + provider.onAnimatingTypesChanged(target); + } + } + private void notifyPendingInsetsControlChanged() { if (mPendingTargetProvidersMap.isEmpty()) { return; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index e98b2b749af8..8587b5a9c7ca 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3831,10 +3831,11 @@ class Task extends TaskFragment { pw.print(ActivityInfo.resizeModeToString(mResizeMode)); pw.print(" mSupportsPictureInPicture="); pw.print(mSupportsPictureInPicture); pw.print(" isResizeable="); pw.println(isResizeable()); - pw.print(" isPerceptible="); pw.println(mIsPerceptible); + pw.print(prefix); pw.print("isPerceptible="); pw.println(mIsPerceptible); pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime); pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)"); - pw.print(prefix); pw.println(" isTrimmable=" + mIsTrimmableFromRecents); + pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents); + pw.print(" isForceHidden="); pw.println(isForceHidden()); if (mLaunchAdjacentDisabled) { pw.println(prefix + "mLaunchAdjacentDisabled=true"); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 2dabb253744a..960f5beae2b3 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -24,7 +24,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; @@ -2453,7 +2452,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { inOutConfig.windowConfiguration.setAppBounds(mTmpFullBounds); outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); - if (!customContainerPolicy && windowingMode != WINDOWING_MODE_FREEFORM) { + // Floating tasks shouldn't be restricted by containing app bounds. + if (!customContainerPolicy && !isFloating(windowingMode)) { final Rect containingAppBounds; if (insideParentBounds) { containingAppBounds = useOverrideInsetsForConfig diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 1022d18ac0e9..ce91fc5baba1 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -862,6 +862,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mWmService.scheduleAnimationLocked(); mAnimatingTypes = animatingTypes; + + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + final InsetsStateController insetsStateController = + getDisplayContent().getInsetsStateController(); + insetsStateController.onAnimatingTypesChanged(this); + } } } diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS new file mode 100644 index 000000000000..2522426d93f8 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/UprobeStats:/OWNERS
\ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index ae9a34efc222..c1d8382fcd0e 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -73,10 +73,7 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "framework", - "ravenwood-runtime", - "ravenwood-utils", - "services", + "services.core", ], libs: [ "android.test.base.stubs.system", diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index 05615f68427d..2339a940e2d0 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -287,6 +287,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */, mTargetSdkVersion /* unverifiedTargetSdkVersion */, mUserId /* userId */, - mMockImeOnBackInvokedDispatcher /* imeDispatcher */); + mMockImeOnBackInvokedDispatcher /* imeDispatcher */, + true /* imeRequestedVisible */); } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java index 70eeae648dd0..aa779197f301 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java @@ -267,7 +267,8 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes // visibility state will be preserved to the current window state. final ImeTargetWindowState stateWithUnChangedFlag = initImeTargetWindowState( mWindowToken); - mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */); + mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */, + true /* imeRequestedVisible */); assertThat(stateWithUnChangedFlag.isRequestedImeVisible()).isEqualTo( lastState.isRequestedImeVisible()); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index 11abc9469c82..b81b570389da 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -320,7 +320,8 @@ public class InputMethodManagerServiceWindowGainedFocusTest mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */, mTargetSdkVersion /* unverifiedTargetSdkVersion */, mUserId /* userId */, - mMockImeOnBackInvokedDispatcher /* imeDispatcher */); + mMockImeOnBackInvokedDispatcher /* imeDispatcher */, + true /* imeRequestedVisible */); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 5d8f57866f7d..e094111c327a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -746,6 +746,36 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + WindowProcessController wpc = app.getWindowProcessController(); + doReturn(true).when(wpc).hasVisibleActivities(); + + final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID, + MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false)); + + // App with a visible activity binds to app2 without any special flag. + bindService(app2, app, null, null, 0, mock(IBinder.class)); + + final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID, + MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false)); + + // App with a visible activity binds to app3 with ALLOW_FREEZE. + bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class)); + + setProcessesToLru(app, app2, app3); + + updateOomAdj(app); + + assertCpuTime(app); + assertCpuTime(app2); + assertNoCpuTime(app3); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) @DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING) public void testUpdateOomAdjFreezeState_bindingFromFgs() { final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java index efea21428937..63c572af37b2 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java @@ -171,7 +171,7 @@ public class HearingDevicePhoneCallNotificationControllerTest { HearingDevicePhoneCallNotificationController.CallStateListener { TestCallStateListener(@NonNull Context context) { - super(context); + super(context, context.getMainExecutor()); } @Override diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java index f445b50c7d9c..02361ff259c2 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java @@ -28,7 +28,12 @@ import android.content.Context; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; +import android.view.MotionEvent; +import android.view.View; import android.view.WindowManager; +import android.widget.ImageButton; + +import com.android.internal.R; import org.junit.Before; import org.junit.Rule; @@ -49,12 +54,31 @@ public class AutoclickScrollPanelTest { new TestableContext(getInstrumentation().getContext()); @Mock private WindowManager mMockWindowManager; + @Mock private AutoclickScrollPanel.ScrollPanelControllerInterface mMockScrollPanelController; + private AutoclickScrollPanel mScrollPanel; + // Scroll panel buttons. + private ImageButton mUpButton; + private ImageButton mDownButton; + private ImageButton mLeftButton; + private ImageButton mRightButton; + private ImageButton mExitButton; + @Before public void setUp() { mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); - mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager); + mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager, + mMockScrollPanelController); + + View contentView = mScrollPanel.getContentViewForTesting(); + + // Initialize buttons. + mUpButton = contentView.findViewById(R.id.scroll_up); + mDownButton = contentView.findViewById(R.id.scroll_down); + mLeftButton = contentView.findViewById(R.id.scroll_left); + mRightButton = contentView.findViewById(R.id.scroll_right); + mExitButton = contentView.findViewById(R.id.scroll_exit); } @Test @@ -89,4 +113,55 @@ public class AutoclickScrollPanelTest { // Verify scroll panel is hidden. assertThat(mScrollPanel.isVisible()).isFalse(); } + + @Test + public void initialState_correctButtonVisibility() { + // Verify all expected buttons exist in the view. + assertThat(mUpButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mDownButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mLeftButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mRightButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mExitButton.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void directionButtons_onHover_callsHandleScroll() { + // Test up button. + triggerHoverEvent(mUpButton); + verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP); + + // Test down button. + triggerHoverEvent(mDownButton); + verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN); + + // Test left button. + triggerHoverEvent(mLeftButton); + verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT); + + // Test right button. + triggerHoverEvent(mRightButton); + verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT); + } + + @Test + public void exitButton_onHover_callsExitScrollMode() { + // Test exit button. + triggerHoverEvent(mExitButton); + verify(mMockScrollPanelController).exitScrollMode(); + } + + // Helper method to simulate a hover event on a view. + private void triggerHoverEvent(View view) { + MotionEvent event = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ MotionEvent.ACTION_HOVER_ENTER, + /* x= */ 0, + /* y= */ 0, + /* metaState= */ 0); + + // Dispatch the event to the view's OnHoverListener. + view.dispatchGenericMotionEvent(event); + event.recycle(); + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java index 1af59daa9c78..5922b12edc1e 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java @@ -37,6 +37,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,6 +47,7 @@ import android.content.Context; import android.graphics.PointF; import android.os.Looper; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.DexmakerShareClassLoaderRule; @@ -504,6 +506,36 @@ public class TouchExplorerTest { assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId); } + @Test + @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() { + mTouchExplorer.getState().setServiceDetectsGestures(true); + mTouchExplorer.getState().clear(); + + mLastEvent = pointerDownEvent(); + mTouchExplorer.getState().startTouchExploring(); + MotionEvent event = fromTouchscreen(pointerUpEvent()); + + mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0); + + verify(mMockAms, never()).sendMotionEventToListeningServices(event); + } + + @Test + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() { + mTouchExplorer.getState().setServiceDetectsGestures(true); + mTouchExplorer.getState().clear(); + + mLastEvent = pointerDownEvent(); + mTouchExplorer.getState().startTouchExploring(); + MotionEvent event = fromTouchscreen(pointerUpEvent()); + + mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0); + + verify(mMockAms).sendMotionEventToListeningServices(event); + } + /** * Used to play back event data of a gesture by parsing the log into MotionEvents and sending * them to TouchExplorer. diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java new file mode 100644 index 000000000000..3e7d9fd05327 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java @@ -0,0 +1,77 @@ +/* + * 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.accessibility.gestures; + +import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END; + +import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR; +import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.accessibility.AccessibilityManagerService; +import com.android.server.accessibility.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class TouchStateTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private TouchState mTouchState; + @Mock private AccessibilityManagerService mMockAms; + + @Before + public void setup() { + mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms); + } + + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() { + mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1; + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING); + } + + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_pointerUp_clears() { + mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0; + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR); + } + + @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_clears() { + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR); + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index b0ffebb973a1..aa1d5835bfc8 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -2295,6 +2295,38 @@ public class HdmiCecLocalDeviceTvTest { .hasSize(1); } + @Test + public void onOneTouchPlay_wakeUp_exist_device() { + HdmiCecMessage requestActiveSource = + HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV); + + // Go to standby to trigger RequestActiveSourceAction for playback_1 + mHdmiControlService.onStandby(STANDBY_SCREEN_OFF); + mTestLooper.dispatchAll(); + + mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS); + mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + + // Skip the LauncherX API timeout. + mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource); + mNativeWrapper.clearResultMessages(); + + // turn off TV and wake up with one touch play + mHdmiControlService.onStandby(STANDBY_SCREEN_OFF); + mTestLooper.dispatchAll(); + + // FakePowerManagerWrapper#wakeUp() doesn't broadcast Intent.ACTION_SCREEN_ON + // manually trigger onWakeUp to mock OTP + mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource); + } + @Test public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() { diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 3ca019728c2b..fcdf88f16550 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -605,29 +605,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test - public void testKeyGestureAccessibilityShortcutChord() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.moveTimeForward(5000); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordCalled(); - } - - @Test - public void testKeyGestureAccessibilityShortcutChordCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordNotCalled(); - } - - @Test public void testKeyGestureRingerToggleChord() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE); Assert.assertTrue( @@ -670,29 +647,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test - public void testKeyGestureAccessibilityTvShortcutChord() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.moveTimeForward(5000); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordCalled(); - } - - @Test - public void testKeyGestureAccessibilityTvShortcutChordCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordNotCalled(); - } - - @Test public void testKeyGestureTvTriggerBugReport() { Assert.assertTrue( sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index f88492477487..e56fd3c6272d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -750,11 +750,6 @@ class TestPhoneWindowManager { verify(mAccessibilityShortcutController).performAccessibilityShortcut(); } - void assertAccessibilityKeychordNotCalled() { - mTestLooper.dispatchAll(); - verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut(); - } - void assertCloseAllDialogs() { verify(mContext).closeSystemDialogs(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java index 7d59f4872d37..eb6d5cf8bb14 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java @@ -21,10 +21,18 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.graphics.PixelFormat; +import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.view.WindowInsets; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; @@ -211,4 +219,29 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { mImeProvider.setFrozen(false); assertFalse(mImeProvider.getSource().isVisible()); } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) + public void testUpdateControlForTarget_remoteInsetsControlTarget() throws RemoteException { + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); + makeWindowVisibleAndDrawn(ime); + mImeProvider.setWindowContainer(ime, null, null); + mImeProvider.setServerVisible(true); + mImeProvider.setClientVisible(true); + final WindowState inputTarget = newWindowBuilder("app", TYPE_APPLICATION).build(); + final var displayWindowInsetsController = spy(createDisplayWindowInsetsController()); + mDisplayContent.setRemoteInsetsController(displayWindowInsetsController); + final var controlTarget = mDisplayContent.mRemoteInsetsControlTarget; + + inputTarget.setRequestedVisibleTypes( + WindowInsets.Type.defaultVisible() | WindowInsets.Type.ime()); + mDisplayContent.setImeInputTarget(inputTarget); + mDisplayContent.setImeControlTarget(controlTarget); + + assertTrue(inputTarget.isRequestedVisible(WindowInsets.Type.ime())); + assertFalse(controlTarget.isRequestedVisible(WindowInsets.Type.ime())); + mImeProvider.updateControlForTarget(controlTarget, true /* force */, null /* statsToken */); + verify(displayWindowInsetsController, times(1)).setImeInputTargetRequestedVisibility( + eq(true), any()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index ae6144713a1d..5401a44d7016 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -910,7 +910,7 @@ public class WindowOrganizerTests extends WindowTestsBase { final RunningTaskInfo info2 = task2.getTaskInfo(); WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setAdjacentRoots(info1.token, info2.token); + wct.setAdjacentRootSet(info1.token, info2.token); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); assertTrue(task1.isAdjacentTo(task2)); assertTrue(task2.isAdjacentTo(task1)); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index fbba999bfb36..14d567d141cb 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -3371,14 +3371,13 @@ public class TelephonyManager { return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(), getAttributionTag()); } else { - // This can happen when the ITelephony interface is not up yet. + Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet"); return NETWORK_TYPE_UNKNOWN; } - } catch(RemoteException ex) { - // This shouldn't happen in the normal case - return NETWORK_TYPE_UNKNOWN; - } catch (NullPointerException ex) { - // This could happen before phone restarts due to crashing + } catch (RemoteException // Shouldn't happen in the normal case + | NullPointerException ex // Could happen before phone restarts due to crashing + ) { + Log.e(TAG, "getDataNetworkType: " + ex.getMessage()); return NETWORK_TYPE_UNKNOWN; } } diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 685ae9a5fef2..c5e7188e6efe 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 5f92d7fe830b..22bd458e2751 100644 --- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 1b90e99a8ba2..541ce26d1435 100644 --- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index ffdbb02984a7..d2e02193f0fb 100644 --- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml +++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index ac704e5e7c39..e112c82f0661 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -49,6 +49,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index e2ac5a9579ae..e5700c03cf77 100644 --- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 1a4feb6e9eca..4c41a4c01180 100644 --- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 1b2007deae27..27fc249e36b9 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt index e99c81493394..794fd0255726 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -214,9 +214,5 @@ class KeyGestureEventHandlerTest { ): Boolean { return handler(event, focusedToken) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return true - } } } diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 2799f6c79215..4f1fb6487b19 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -32,6 +32,7 @@ import android.hardware.input.InputGestureData import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.hardware.input.KeyGestureEvent +import android.os.Handler import android.os.IBinder import android.os.Process import android.os.SystemClock @@ -48,9 +49,11 @@ import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE import androidx.test.core.app.ApplicationProvider import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.R +import com.android.internal.accessibility.AccessibilityShortcutController import com.android.internal.annotations.Keep import com.android.internal.util.FrameworkStatsLog import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.server.input.InputManagerService.WindowManagerCallbacks import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -67,6 +70,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito +import org.mockito.kotlin.never +import org.mockito.kotlin.times /** * Tests for {@link KeyGestureController}. @@ -102,6 +107,7 @@ class KeyGestureControllerTests { const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0 const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1 const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2 + const val TEST_PID = 10 } @JvmField @@ -116,11 +122,10 @@ class KeyGestureControllerTests { @Rule val rule = SetFlagsRule() - @Mock - private lateinit var iInputManager: IInputManager - - @Mock - private lateinit var packageManager: PackageManager + @Mock private lateinit var iInputManager: IInputManager + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var wmCallbacks: WindowManagerCallbacks + @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController private var currentPid = 0 private lateinit var context: Context @@ -207,8 +212,34 @@ class KeyGestureControllerTests { private fun setupKeyGestureController() { keyGestureController = - KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore) - Mockito.`when`(iInputManager.getAppLaunchBookmarks()) + KeyGestureController( + context, + testLooper.looper, + testLooper.looper, + inputDataStore, + object : KeyGestureController.Injector() { + override fun getAccessibilityShortcutController( + context: Context?, + handler: Handler? + ): AccessibilityShortcutController { + return accessibilityShortcutController + } + }) + Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any())) + .thenAnswer { + val args = it.arguments + if (args[0] != null) { + keyGestureController.registerKeyGestureHandler( + args[0] as IKeyGestureHandler, + TEST_PID + ) + } + } + keyGestureController.setWindowManagerCallbacks(wmCallbacks) + Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false) + Mockito.`when`(accessibilityShortcutController + .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true) + Mockito.`when`(iInputManager.appLaunchBookmarks) .thenReturn(keyGestureController.appLaunchBookmarks) keyGestureController.systemRunning() testLooper.dispatchAll() @@ -1270,9 +1301,9 @@ class KeyGestureControllerTests { ) ), TestData( - "BACK + DPAD_DOWN -> TV Accessibility Chord", + "BACK + DPAD_DOWN -> Accessibility Chord(for TV)", intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), 0, intArrayOf( @@ -1622,6 +1653,52 @@ class KeyGestureControllerTests { ) } + @Test + fun testAccessibilityShortcutChordPressed() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), + // Assuming this value is always greater than the accessibility shortcut timeout, which + // currently defaults to 3000ms + timeDelayMs = 10000 + ) + Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityTvShortcutChordPressed() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + timeDelayMs = 10000 + ) + Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityShortcutChordPressedForLessThanTimeout() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), + timeDelayMs = 0 + ) + Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + timeDelayMs = 0 + ) + Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() + } + private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> @@ -1683,7 +1760,11 @@ class KeyGestureControllerTests { assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size) } - private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) { + private fun sendKeys( + testKeys: IntArray, + assertNotSentToApps: Boolean = false, + timeDelayMs: Long = 0 + ) { var metaState = 0 val now = SystemClock.uptimeMillis() for (key in testKeys) { @@ -1699,6 +1780,11 @@ class KeyGestureControllerTests { testLooper.dispatchAll() } + if (timeDelayMs > 0) { + testLooper.moveTimeForward(timeDelayMs) + testLooper.dispatchAll() + } + for (key in testKeys.reversed()) { val upEvent = KeyEvent( now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState, @@ -1742,9 +1828,5 @@ class KeyGestureControllerTests { override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean { return handler(event, token) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return true - } } } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 8cd89ce89e83..649241aaaa8c 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -75,16 +75,7 @@ public class TestableLooper { * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. */ private static boolean isAtLeastBaklava() { - Method[] methods = TestLooperManager.class.getMethods(); - for (Method method : methods) { - if (method.getName().equals("peekWhen")) { - return true; - } - } - return false; - // TODO(shayba): delete the above, uncomment the below. - // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. - // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; } static { diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 4d379e45a81a..bb54a26036db 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -68,16 +68,7 @@ public class TestLooper { * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. */ private static boolean isAtLeastBaklava() { - Method[] methods = TestLooperManager.class.getMethods(); - for (Method method : methods) { - if (method.getName().equals("peekWhen")) { - return true; - } - } - return false; - // TODO(shayba): delete the above, uncomment the below. - // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. - // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; } static { diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 0d261abd728d..e51477c668dd 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -249,6 +249,8 @@ struct ResourceFile { // Flag std::optional<FeatureFlagAttribute> flag; + + bool uses_readwrite_feature_flags = false; }; /** diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 5435cba290fc..db7dddc49a99 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -664,6 +664,7 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) if (!config_value->value) { // Resource does not exist, add it now. config_value->value = std::move(res.value); + config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; } else { // When validation is enabled, ensure that a resource cannot have multiple values defined for // the same configuration unless protected by flags. @@ -681,12 +682,14 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) ConfigKey{&res.config, res.product}, lt_config_key_ref()), util::make_unique<ResourceConfigValue>(res.config, res.product)); (*it)->value = std::move(res.value); + (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; break; } case CollisionResult::kTakeNew: // Take the incoming value. config_value->value = std::move(res.value); + config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; break; case CollisionResult::kConflict: @@ -843,6 +846,12 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { return *this; } +NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags( + bool uses_readwrite_feature_flags) { + res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags; + return *this; +} + NewResource NewResourceBuilder::Build() { return std::move(res_); } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index b0e185536d16..778b43adb50b 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -104,6 +104,9 @@ class ResourceConfigValue { // The actual Value. std::unique_ptr<Value> value; + // Whether the value uses read/write feature flags + bool uses_readwrite_feature_flags = false; + ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product) : config(config), product(product) { } @@ -284,6 +287,7 @@ struct NewResource { std::optional<AllowNew> allow_new; std::optional<StagedId> staged_id; bool allow_mangled = false; + bool uses_readwrite_feature_flags = false; }; struct NewResourceBuilder { @@ -297,6 +301,7 @@ struct NewResourceBuilder { NewResourceBuilder& SetAllowNew(AllowNew allow_new); NewResourceBuilder& SetStagedId(StagedId id); NewResourceBuilder& SetAllowMangled(bool allow_mangled); + NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags); NewResource Build(); private: diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 2a7921600477..755dbb6f8e42 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -673,11 +673,13 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv // Update the output format of this XML file. file_ref->type = XmlFileTypeForOutputFormat(options_.output_format); - bool result = table->AddResource(NewResourceBuilder(file.name) - .SetValue(std::move(file_ref), file.config) - .SetAllowMangled(true) - .Build(), - context_->GetDiagnostics()); + bool result = table->AddResource( + NewResourceBuilder(file.name) + .SetValue(std::move(file_ref), file.config) + .SetAllowMangled(true) + .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags) + .Build(), + context_->GetDiagnostics()); if (!result) { return false; } diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index 2e20e8175213..bac871b8bdc3 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -414,6 +414,8 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, .SetId(res_id, OnIdConflict::CREATE_ENTRY) .SetAllowMangled(true); + res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags()); + if (entry->flags() & ResTable_entry::FLAG_PUBLIC) { Visibility visibility{Visibility::Level::kPublic}; diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp index 9dc205f4c1ba..0be392164453 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.cpp +++ b/tools/aapt2/format/binary/ResEntryWriter.cpp @@ -199,6 +199,10 @@ void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) { flags |= ResTable_entry::FLAG_WEAK; } + if (entry->uses_readwrite_feature_flags) { + flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS; + } + if constexpr (std::is_same_v<ResTable_entry_ext, T>) { flags |= ResTable_entry::FLAG_COMPLEX; } diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h index c11598ec12f7..f54b29aa8f2a 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.h +++ b/tools/aapt2/format/binary/ResEntryWriter.h @@ -38,6 +38,8 @@ struct FlatEntry { // The entry string pool index to the entry's name. uint32_t entry_key; + + bool uses_readwrite_feature_flags; }; // Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer. diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..50144ae816b6 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -502,7 +502,8 @@ class PackageFlattener { // Group values by configuration. for (auto& config_value : entry.values) { config_to_entry_list_map[config_value->config].push_back( - FlatEntry{&entry, config_value->value.get(), local_key_index}); + FlatEntry{&entry, config_value->value.get(), local_key_index, + config_value->uses_readwrite_feature_flags}); } } diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..9156b96b67ec 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -1069,4 +1069,23 @@ TEST_F(TableFlattenerTest, FlattenTypeEntryWithNameCollapseInExemption) { testing::IsTrue()); } +TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .Add(NewResourceBuilder("com.app.a:color/foo") + .SetValue(util::make_unique<BinaryPrimitive>( + uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc)) + .SetUsesReadWriteFeatureFlags(true) + .SetId(0x7f020000) + .Build()) + .Build(); + ResTable res_table; + TableFlattenerOptions options; + ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table)); + + uint32_t flags; + ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags)); + ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS); +} + } // namespace aapt diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index dbef77615515..47a71fe36e9f 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include <regex> +#include <string> + #include "LoadedApk.h" #include "cmd/Dump.h" #include "io/StringStream.h" @@ -183,4 +186,49 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInPathFails) { "Only read only flags may be used with resources: test.package.rwFlag")); } +TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpChunksToString(loaded_apk.get(), &output); + + // The actual line looks something like: + // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010 + // + // This regex matches that line and captures the name and the flag value for checking. + std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})"); + std::smatch match; + + std::stringstream ss(output); + std::string line; + bool found = false; + int fields_flagged = 0; + while (std::getline(ss, line)) { + bool first_line = false; + if (line.contains("config: v36")) { + std::getline(ss, line); + first_line = true; + } + if (!line.contains("flags")) { + continue; + } + if (std::regex_search(line, match, regex) && (match.size() == 3)) { + unsigned int hex_value; + std::stringstream hex_ss; + hex_ss << std::hex << match[2]; + hex_ss >> hex_value; + if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) { + fields_flagged++; + if (first_line && match[1] == "layout1") { + found = true; + } + } + } + } + ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set"; + // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1 + ASSERT_EQ(fields_flagged, 1); +} + } // namespace aapt diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp index 75c6f17dcb51..8a3337c446cb 100644 --- a/tools/aapt2/link/FlaggedXmlVersioner.cpp +++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp @@ -66,6 +66,28 @@ class AllDisabledFlagsVisitor : public xml::Visitor { bool had_flags_ = false; }; +// An xml visitor that goes through the a doc and determines if any elements are behind a flag. +class FindFlagsVisitor : public xml::Visitor { + public: + void Visit(xml::Element* node) override { + if (had_flags_) { + return; + } + auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); + if (attr != nullptr) { + had_flags_ = true; + return; + } + VisitChildren(node); + } + + bool HadFlags() const { + return had_flags_; + } + + bool had_flags_ = false; +}; + std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context, xml::XmlResource* doc) { std::vector<std::unique_ptr<xml::XmlResource>> docs; @@ -74,15 +96,20 @@ std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAap // Support for read/write flags was added in baklava so if the doc will only get used on // baklava or later we can just return the original doc. docs.push_back(doc->Clone()); + FindFlagsVisitor visitor; + doc->root->Accept(&visitor); + docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags(); } else { auto preBaklavaVersion = doc->Clone(); AllDisabledFlagsVisitor visitor; preBaklavaVersion->root->Accept(&visitor); + preBaklavaVersion->file.uses_readwrite_feature_flags = false; docs.push_back(std::move(preBaklavaVersion)); if (visitor.HadFlags()) { auto baklavaVersion = doc->Clone(); baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA; + baklavaVersion->file.uses_readwrite_feature_flags = true; docs.push_back(std::move(baklavaVersion)); } } |