diff options
173 files changed, 4518 insertions, 1810 deletions
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index 696bc82a9ffc..ae150aece432 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -1309,6 +1309,8 @@ public class Bmgr { return "AGENT_FAILURE_DURING_RESTORE"; case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT: return "FAILED_TO_READ_DATA_FROM_TRANSPORT"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN: + return "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN"; default: return "UNKNOWN_ID"; } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index b38f5da6b638..54ab3b8f185b 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -55,9 +55,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; -import android.graphics.Rect; import android.graphics.drawable.Icon; -import android.hardware.HardwareBuffer; import android.os.BatteryStats; import android.os.Binder; import android.os.Build; @@ -86,7 +84,6 @@ import android.util.Log; import android.util.Singleton; import android.util.Size; import android.view.WindowInsetsController.Appearance; -import android.window.TaskSnapshot; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.LocalePicker; @@ -5420,10 +5417,11 @@ public class ActivityManager { * * @hide */ + @Nullable @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @Nullable String getSwitchingFromUserMessage() { + public String getSwitchingFromUserMessage(@UserIdInt int userId) { try { - return getService().getSwitchingFromUserMessage(); + return getService().getSwitchingFromUserMessage(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -5434,10 +5432,11 @@ public class ActivityManager { * * @hide */ + @Nullable @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @Nullable String getSwitchingToUserMessage() { + public String getSwitchingToUserMessage(@UserIdInt int userId) { try { - return getService().getSwitchingToUserMessage(); + return getService().getSwitchingToUserMessage(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index a12c0674998e..e5f7889859c1 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -292,14 +292,14 @@ public abstract class ActivityManagerInternal { public abstract boolean canStartMoreUsers(); /** - * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}. + * Sets the user switcher message for switching from a user. */ - public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage); + public abstract void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message); /** - * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}. + * Sets the user switcher message for switching to a user. */ - public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage); + public abstract void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message); /** * Returns maximum number of users that can run simultaneously. diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 599f1a8be233..ea4646aa9eb9 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -102,6 +102,8 @@ public class AppCompatTaskInfo implements Parcelable { private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8; /** Top activity flag for whether min aspect ratio of the activity has been overridden.*/ public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9; + /** Top activity flag for whether restart menu is shown due to display move. */ + private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { @@ -115,7 +117,8 @@ public class AppCompatTaskInfo implements Parcelable { FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON, FLAG_FULLSCREEN_OVERRIDE_SYSTEM, FLAG_FULLSCREEN_OVERRIDE_USER, - FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE + FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE, + FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE }) public @interface TopActivityFlag {} @@ -133,7 +136,8 @@ public class AppCompatTaskInfo implements Parcelable { @TopActivityFlag private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED - | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED; + | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED + | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE; private AppCompatTaskInfo() { // Do nothing @@ -300,6 +304,21 @@ public class AppCompatTaskInfo implements Parcelable { } /** + * @return {@code true} if the restart menu is enabled for the top activity due to display move. + */ + public boolean isRestartMenuEnabledForDisplayMove() { + return isTopActivityFlagEnabled(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE); + } + + /** + * Sets the top activity flag for whether the restart menu is enabled for the top activity due + * to display move. + */ + public void setRestartMenuEnabledForDisplayMove(boolean enable) { + setTopActivityFlag(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, enable); + } + + /** * @return {@code true} if the top activity bounds are letterboxed. */ public boolean isTopActivityLetterboxed() { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index ad01ad57b2d8..6cdfb97520ae 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -403,8 +403,8 @@ interface IActivityManager { void setPackageScreenCompatMode(in String packageName, int mode); @UnsupportedAppUsage boolean switchUser(int userid); - String getSwitchingFromUserMessage(); - String getSwitchingToUserMessage(); + String getSwitchingFromUserMessage(int userId); + String getSwitchingToUserMessage(int userId); @UnsupportedAppUsage void setStopUserOnSwitch(int value); boolean removeTask(int taskId); diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 6936ddc5eeac..59247934ba46 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -655,8 +655,10 @@ public class TaskInfo { + " effectiveUid=" + effectiveUid + " displayId=" + displayId + " isRunning=" + isRunning - + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity - + " topActivity=" + topActivity + " origActivity=" + origActivity + + " baseIntent=" + baseIntent + + " baseActivity=" + baseActivity + + " topActivity=" + topActivity + + " origActivity=" + origActivity + " realActivity=" + realActivity + " numActivities=" + numActivities + " lastActiveTime=" + lastActiveTime diff --git a/core/java/android/app/backup/BackupManagerMonitor.java b/core/java/android/app/backup/BackupManagerMonitor.java index e741bc2bf608..19c24cd8a14a 100644 --- a/core/java/android/app/backup/BackupManagerMonitor.java +++ b/core/java/android/app/backup/BackupManagerMonitor.java @@ -297,6 +297,9 @@ public class BackupManagerMonitor { @hide */ public static final int LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT = 81; + /** The pipe between the BackupAgent and the framework was broken during full backup. @hide */ + public static final int LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN = 82; + /** * This method will be called each time something important happens on BackupManager. * diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 74da62c85ed2..10d3051cff6f 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -166,7 +166,15 @@ public abstract class RegisteredServicesCache<V> { @UnsupportedAppUsage public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, String attributeName, XmlSerializerAndParser<V> serializerAndParser) { - mContext = context; + this(new Injector<V>(context), interfaceName, metaDataName, attributeName, + serializerAndParser); + } + + /** Provides the basic functionality for unit tests. */ + @VisibleForTesting + public RegisteredServicesCache(Injector<V> injector, String interfaceName, String metaDataName, + String attributeName, XmlSerializerAndParser<V> serializerAndParser) { + mContext = injector.getContext(); mInterfaceName = interfaceName; mMetaDataName = metaDataName; mAttributesName = attributeName; @@ -184,7 +192,7 @@ public abstract class RegisteredServicesCache<V> { if (isCore) { intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); } - mBackgroundHandler = BackgroundThread.getHandler(); + mBackgroundHandler = injector.getBackgroundHandler(); mContext.registerReceiverAsUser( mPackageReceiver, UserHandle.ALL, intentFilter, null, mBackgroundHandler); @@ -918,4 +926,25 @@ public abstract class RegisteredServicesCache<V> { return null; } } + + /** + * Point of injection for test dependencies. + * @param <V> The type of the value. + */ + @VisibleForTesting + public static class Injector<V> { + private final Context mContext; + + public Injector(Context context) { + mContext = context; + } + + public Context getContext() { + return mContext; + } + + public Handler getBackgroundHandler() { + return BackgroundThread.getHandler(); + } + } } diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 414f27498414..176c0c8ab966 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -165,3 +165,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "haptics" + name: "remove_hidl_support" + description: "Remove framework code to support HIDL vibrator HALs." + bug: "308452413" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index f026614f2a35..ce31e1ea7e38 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -455,7 +455,7 @@ public class DreamService extends Service implements Window.Callback { // Simply wake up in the case the device is not locked. if (!keyguardManager.isKeyguardLocked()) { - wakeUp(false); + wakeUp(); return true; } @@ -477,11 +477,11 @@ public class DreamService extends Service implements Window.Callback { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on keyEvent"); - wakeUp(false); + wakeUp(); return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (mDebug) Slog.v(mTag, "Waking up on back key"); - wakeUp(false); + wakeUp(); return true; } return mWindow.superDispatchKeyEvent(event); @@ -492,7 +492,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchKeyShortcutEvent(KeyEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on keyShortcutEvent"); - wakeUp(false); + wakeUp(); return true; } return mWindow.superDispatchKeyShortcutEvent(event); @@ -505,7 +505,7 @@ public class DreamService extends Service implements Window.Callback { // but finish()es on any other kind of activity if (!mInteractive && event.getActionMasked() == MotionEvent.ACTION_UP) { if (mDebug) Slog.v(mTag, "Waking up on touchEvent"); - wakeUp(false); + wakeUp(); return true; } return mWindow.superDispatchTouchEvent(event); @@ -516,7 +516,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchTrackballEvent(MotionEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on trackballEvent"); - wakeUp(false); + wakeUp(); return true; } return mWindow.superDispatchTrackballEvent(event); @@ -527,7 +527,7 @@ public class DreamService extends Service implements Window.Callback { public boolean dispatchGenericMotionEvent(MotionEvent event) { if (!mInteractive) { if (mDebug) Slog.v(mTag, "Waking up on genericMotionEvent"); - wakeUp(false); + wakeUp(); return true; } return mWindow.superDispatchGenericMotionEvent(event); @@ -925,37 +925,32 @@ public class DreamService extends Service implements Window.Callback { } } - /** - * Updates doze state. Note that this must be called on the mHandler. - */ - private void updateDoze() { - mHandler.post(() -> { - if (mDreamToken == null) { - Slog.w(mTag, "Updating doze without a dream token."); - return; - } + private synchronized void updateDoze() { + if (mDreamToken == null) { + Slog.w(mTag, "Updating doze without a dream token."); + return; + } - if (mDozing) { - try { - Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState - + " mDozeScreenBrightness=" + mDozeScreenBrightness - + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat); - if (startAndStopDozingInBackground()) { - mDreamManager.startDozingOneway( - mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightnessFloat, mDozeScreenBrightness, - mUseNormalBrightnessForDoze); - } else { - mDreamManager.startDozing( - mDreamToken, mDozeScreenState, mDozeScreenStateReason, - mDozeScreenBrightnessFloat, mDozeScreenBrightness, - mUseNormalBrightnessForDoze); - } - } catch (RemoteException ex) { - // system server died + if (mDozing) { + try { + Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState + + " mDozeScreenBrightness=" + mDozeScreenBrightness + + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat); + if (startAndStopDozingInBackground()) { + mDreamManager.startDozingOneway( + mDreamToken, mDozeScreenState, mDozeScreenStateReason, + mDozeScreenBrightnessFloat, mDozeScreenBrightness, + mUseNormalBrightnessForDoze); + } else { + mDreamManager.startDozing( + mDreamToken, mDozeScreenState, mDozeScreenStateReason, + mDozeScreenBrightnessFloat, mDozeScreenBrightness, + mUseNormalBrightnessForDoze); } + } catch (RemoteException ex) { + // system server died } - }); + } } /** @@ -971,16 +966,14 @@ public class DreamService extends Service implements Window.Callback { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public void stopDozing() { - mHandler.post(() -> { - if (mDozing) { - mDozing = false; - try { - mDreamManager.stopDozing(mDreamToken); - } catch (RemoteException ex) { - // system server died - } + if (mDozing) { + mDozing = false; + try { + mDreamManager.stopDozing(mDreamToken); + } catch (RemoteException ex) { + // system server died } - }); + } } /** @@ -1208,7 +1201,7 @@ public class DreamService extends Service implements Window.Callback { @Override public void onExitRequested() { // Simply finish dream when exit is requested. - mHandler.post(() -> finishInternal()); + mHandler.post(() -> finish()); } @Override @@ -1306,13 +1299,9 @@ public class DreamService extends Service implements Window.Callback { * </p> */ public final void finish() { - mHandler.post(this::finishInternal); - } - - private void finishInternal() { // If there is an active overlay connection, signal that the dream is ending before - // continuing. Note that the overlay cannot rely on the unbound state, since another - // dream might have bound to it in the meantime. + // continuing. Note that the overlay cannot rely on the unbound state, since another dream + // might have bound to it in the meantime. if (mOverlayConnection != null) { mOverlayConnection.addConsumer(overlay -> { try { @@ -1368,7 +1357,7 @@ public class DreamService extends Service implements Window.Callback { * </p> */ public final void wakeUp() { - mHandler.post(()-> wakeUp(false)); + wakeUp(false); } /** @@ -1570,7 +1559,7 @@ public class DreamService extends Service implements Window.Callback { if (mActivity != null && !mActivity.isFinishing()) { mActivity.finishAndRemoveTask(); } else { - finishInternal(); + finish(); } mDreamToken = null; @@ -1730,7 +1719,7 @@ public class DreamService extends Service implements Window.Callback { // the window reference in order to fully release the DreamActivity. mWindow = null; mActivity = null; - finishInternal(); + finish(); } if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) { diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index aad3bf2679d9..901fc02f0040 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -516,8 +516,9 @@ public abstract class LayoutInflater { mConstructorArgs[0] = inflaterContext; View result = root; - if (root != null && root.getViewRootImpl() != null) { - root.getViewRootImpl().notifyRendererOfExpensiveFrame(); + ViewRootImpl viewRootImpl = root != null ? root.getViewRootImpl() : null; + if (viewRootImpl != null) { + viewRootImpl.notifyRendererOfExpensiveFrame(); } try { diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index 0b52687ebeb1..4f89c831a05f 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -47,17 +47,17 @@ public enum DesktopExperienceFlags { com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true), CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true), DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true), - ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false), + 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_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false), + ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true), ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT( com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement, true), - ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false), - ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false), + ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, true), + ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true), ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true), - ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false), + ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true), ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false), ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false), ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS( @@ -67,9 +67,9 @@ public enum DesktopExperienceFlags { ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false), ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false), - ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false), + ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, true), ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays, - false), + true), FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH(Flags::formFactorBasedDesktopFirstSwitch, false), REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true) // go/keep-sorted end diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 1156503cf8e8..ea345a5ef46a 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -688,12 +688,6 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public WindowContainerTransaction setAdjacentRoots( @NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - mHierarchyOps.add(HierarchyOp.createForAdjacentRoots( - root1.asBinder(), - root2.asBinder())); - return this; - } return setAdjacentRootSet(root1, root2); } @@ -714,10 +708,6 @@ public final class WindowContainerTransaction implements Parcelable { */ @NonNull public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalArgumentException("allowMultipleAdjacentTaskFragments is not enabled." - + " Use #setAdjacentRoots instead."); - } if (roots.length < 2) { throw new IllegalArgumentException("setAdjacentRootSet must have size >= 2"); } @@ -1973,13 +1963,6 @@ public final class WindowContainerTransaction implements Parcelable { return mContainers; } - /** @deprecated b/373709676 replace with {@link #getContainers()}. */ - @Deprecated - @NonNull - public IBinder getAdjacentRoot() { - return mReparent; - } - public boolean getToTop() { return mToTop; } @@ -2127,17 +2110,12 @@ public final class WindowContainerTransaction implements Parcelable { sb.append(mContainer).append(" to ").append(mToTop ? "top" : "bottom"); break; case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: - if (Flags.allowMultipleAdjacentTaskFragments()) { - for (IBinder container : mContainers) { - if (container == mContainers[0]) { - sb.append("adjacentRoots=").append(container); - } else { - sb.append(", ").append(container); - } + for (IBinder container : mContainers) { + if (container == mContainers[0]) { + sb.append("adjacentRoots=").append(container); + } else { + sb.append(", ").append(container); } - } else { - sb.append("container=").append(mContainer) - .append(" adjacentRoot=").append(mReparent); } break; case HIERARCHY_OP_TYPE_LAUNCH_TASK: diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java index ee4761b9d024..dccbf4036b3e 100644 --- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java +++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java @@ -245,6 +245,12 @@ public class RegisteredServicesCacheTest extends AndroidTestCase { SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, new TestSerializer()); } + TestServicesCache(Injector<TestServiceType> injector, + XmlSerializerAndParser<TestServiceType> serializerAndParser) { + super(injector, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, + serializerAndParser); + } + @Override public TestServiceType parseServiceAttributes(Resources res, String packageName, AttributeSet attrs) { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java index 01d2201a5a0c..8bcbd2a3fc9f 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java @@ -22,4 +22,11 @@ package com.android.wm.shell.shared; public class ShellSharedConstants { public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION = "extra_shell_can_hand_off_animation"; + + /** + * Defines the max screen width or height in dp for a device to be considered a small tablet. + * + * @see android.view.WindowManager#LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP + */ + public static final int SMALL_TABLET_MAX_EDGE_DP = 960; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt index 1b7c9c282304..ad2671b8135d 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt @@ -26,6 +26,8 @@ import android.view.WindowInsets import android.view.WindowManager import kotlin.math.max +import com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP + /** Contains device configuration used for positioning bubbles on the screen. */ data class DeviceConfig( val isLargeScreen: Boolean, @@ -38,7 +40,6 @@ data class DeviceConfig( companion object { private const val LARGE_SCREEN_MIN_EDGE_DP = 600 - private const val SMALL_TABLET_MAX_EDGE_DP = 960 @JvmStatic fun create(context: Context, windowManager: WindowManager): DeviceConfig { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 50e2f4d52bf2..3e95a0b1100f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1604,7 +1604,7 @@ public class BubbleController implements ConfigurationChangeListener, @Nullable BubbleTransitions.DragData dragData) { if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow - ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - taskId=%s", taskInfo.taskId); BubbleBarLocation location = null; if (dragData != null) { location = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 9b11e4ab16fa..4413c8715c0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -18,6 +18,8 @@ package com.android.wm.shell.compatui; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; @@ -53,7 +55,9 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.api.CompatUIEvent; import com.android.wm.shell.compatui.api.CompatUIHandler; import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.api.CompatUIRequest; import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked; +import com.android.wm.shell.compatui.impl.CompatUIRequests; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.KeyguardChangeListener; @@ -245,6 +249,21 @@ public class CompatUIController implements OnDisplaysChangedListener, mCallback = callback; } + @Override + public void sendCompatUIRequest(CompatUIRequest compatUIRequest) { + switch(compatUIRequest.getRequestId()) { + case DISPLAY_COMPAT_SHOW_RESTART_DIALOG: + handleDisplayCompatShowRestartDialog(compatUIRequest.asType()); + break; + default: + } + } + + private void handleDisplayCompatShowRestartDialog( + CompatUIRequests.DisplayCompatShowRestartDialog request) { + onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener())); + } + /** * Called when the Task info changed. Creates and updates the compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. @@ -254,13 +273,17 @@ public class CompatUIController implements OnDisplaysChangedListener, public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); - if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { + final boolean isInDisplayCompatMode = + taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); + if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat() + && !isInDisplayCompatMode) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); } mIsInDesktopMode = isInDesktopMode(taskInfo); // We close all the Compat UI educations in case TaskInfo has no configuration or // TaskListener or in desktop mode. - if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) { + if (taskInfo.configuration == null || taskListener == null + || (mIsInDesktopMode && !isInDisplayCompatMode)) { // Null token means the current foreground activity is not in compatibility mode. removeLayouts(taskInfo.taskId); return; @@ -552,8 +575,11 @@ public class CompatUIController implements OnDisplaysChangedListener, @Nullable ShellTaskOrganizer.TaskListener taskListener) { RestartDialogWindowManager layout = mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); + final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode + && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); if (layout != null) { - if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) { + if (layout.needsToBeRecreated(taskInfo, taskListener) + || isInNonDisplayCompatDesktopMode) { mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); layout.release(); } else { @@ -568,8 +594,9 @@ public class CompatUIController implements OnDisplaysChangedListener, return; } } - if (mIsInDesktopMode) { - // Return if in desktop mode. + if (isInNonDisplayCompatDesktopMode) { + // No restart dialog can be shown in desktop mode unless the task is in display compat + // mode. return; } // Create a new UI layout. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt index 817e554b550e..f71f8099f29f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt @@ -28,6 +28,11 @@ interface CompatUIHandler { fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) /** + * Invoked when another component in Shell requests a CompatUI state change. + */ + fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) + + /** * Optional reference to the object responsible to send {@link CompatUIEvent} */ fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt new file mode 100644 index 000000000000..069fd9b062a6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.api + +/** + * Abstraction for all the possible Compat UI Component requests. + */ +interface CompatUIRequest { + /** + * Unique request identifier + */ + val requestId: Int + + @Suppress("UNCHECKED_CAST") + fun <T : CompatUIRequest> asType(): T? = this as? T + + fun <T : CompatUIRequest> asType(clazz: Class<T>): T? { + return if (clazz.isInstance(this)) clazz.cast(this) else null + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt new file mode 100644 index 000000000000..da4fc99491dc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.impl + +import android.app.TaskInfo +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.compatui.api.CompatUIRequest + +internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0 + +/** + * All the {@link CompatUIRequest} the Compat UI Framework can handle + */ +sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest { + /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */ + data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo, + val taskListener: ShellTaskOrganizer.TaskListener) : + CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt index 02db85a4f99d..7dcb16c10097 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt @@ -23,6 +23,7 @@ import com.android.wm.shell.compatui.api.CompatUIEvent import com.android.wm.shell.compatui.api.CompatUIHandler import com.android.wm.shell.compatui.api.CompatUIInfo import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUIRequest import com.android.wm.shell.compatui.api.CompatUIState import java.util.function.Consumer @@ -102,4 +103,6 @@ class DefaultCompatUIHandler( override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) { this.compatUIEventSender = compatUIEventSender } + + override fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) {} } 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 73be2d7ac49d..478fd6d83661 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 @@ -1247,7 +1247,7 @@ public abstract class WMShellModule { @DynamicOverride DesktopUserRepositories desktopUserRepositories, @NonNull DesksOrganizer desksOrganizer ) { - if (DesktopModeStatus.canEnterDesktopMode(context)) { + if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.of( new DesksTransitionObserver(desktopUserRepositories, desksOrganizer)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 1f7edb413908..4646662073e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -452,6 +452,11 @@ class DesktopMixedTransitionHandler( private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } + private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? = + info.changes.firstOrNull { change -> + change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform + } + private fun findDesktopTaskLaunchChange( info: TransitionInfo, launchTaskId: Int?, @@ -459,14 +464,18 @@ class DesktopMixedTransitionHandler( return if (launchTaskId != null) { // Launching a known task (probably from background or moving to front), so // specifically look for it. - findTaskChange(info, launchTaskId) + val launchChange = findTaskChange(info, launchTaskId) + if ( + DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue && + launchChange == null + ) { + findLaunchChange(info) + } else { + launchChange + } } else { // Launching a new task, so the first opening freeform task. - info.changes.firstOrNull { change -> - change.mode == TRANSIT_OPEN && - change.taskInfo != null && - change.taskInfo!!.isFreeform - } + findLaunchChange(info) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index c5ee3137e5ba..fa98d0339a65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -22,6 +22,13 @@ import android.annotation.DimenRes import android.app.ActivityManager.RunningTaskInfo import android.app.TaskInfo import android.content.Context +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED import android.content.pm.ActivityInfo.isFixedOrientationLandscape import android.content.pm.ActivityInfo.isFixedOrientationPortrait @@ -30,7 +37,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect import android.os.SystemProperties import android.util.Size +import android.window.DesktopModeFlags import com.android.wm.shell.R +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import kotlin.math.ceil @@ -264,6 +273,58 @@ fun getAppHeaderHeight(context: Context): Int = @DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height /** + * Returns the task bounds a launching task should inherit from an existing running instance. + * Returns null if there are no bounds to inherit. + */ +fun getInheritedExistingTaskBounds( + taskRepository: DesktopRepository, + shellTaskOrganizer: ShellTaskOrganizer, + task: RunningTaskInfo, + deskId: Int, +): Rect? { + if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null + val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull() + if (activeTask == null) return null + val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask) + val lastTaskTopActivity = lastTask?.topActivity + val currentTaskTopActivity = task.topActivity + val intentFlags = task.baseIntent.flags + val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE + return when { + // No running task activity to inherit bounds from. + lastTaskTopActivity == null -> null + // No current top activity to set bounds for. + currentTaskTopActivity == null -> null + // Top task is not an instance of the launching activity, do not inherit its bounds. + lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null + // Top task is an instance of launching activity. Activity will be launching in a new + // task with the existing task also being closed. Inherit existing task bounds to + // prevent new task jumping. + (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) -> + lastTask.configuration.windowConfiguration.bounds + else -> null + } +} + +/** + * Returns true if the launch mode or intent will result in a new task being created for the + * activity. + */ +private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) = + launchMode == LAUNCH_SINGLE_TASK || + launchMode == LAUNCH_SINGLE_INSTANCE || + launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK || + (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0 + +/** + * Returns true if the intent will result in an existing task instance being closed if a new one + * appears. + */ +private fun isClosingExitingInstance(intentFlags: Int) = + (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 || + (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0 + +/** * Calculates the desired initial bounds for applications in desktop windowing. This is done as a * scale of the screen bounds. */ 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 99606d04ff3b..dbc599be57af 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 @@ -32,6 +32,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -238,6 +240,10 @@ class DesktopTasksController( removeVisualIndicator() } + override fun onTransitionInterrupted() { + removeVisualIndicator() + } + private fun removeVisualIndicator() { visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } @@ -2280,11 +2286,19 @@ class DesktopTasksController( wct.reorder(task.token, true) return wct } + val inheritedTaskBounds = + getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) + if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) { + // Inherit bounds from closing task instance to prevent application jumping different + // cascading positions. + wct.setBounds(task.token, inheritedTaskBounds) + } // TODO(b/365723620): Handle non running tasks that were launched after reboot. // If task is already visible, it must have been handled already and added to desktop mode. - // Cascade task only if it's not visible yet. + // Cascade task only if it's not visible yet and has no inherited bounds. if ( - DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() && + inheritedTaskBounds == null && + DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() && !taskRepository.isVisibleTask(task.taskId) ) { val displayLayout = displayController.getDisplayLayout(task.displayId) @@ -2520,9 +2534,17 @@ class DesktopTasksController( ) { val targetDisplayId = taskRepository.getDisplayForDesk(deskId) val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return - val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId) - if (canChangeTaskPosition(task)) { - wct.setBounds(task.token, initialBounds) + val inheritedTaskBounds = + getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) + if (inheritedTaskBounds != null) { + // Inherit bounds from closing task instance to prevent application jumping different + // cascading positions. + wct.setBounds(task.token, inheritedTaskBounds) + } else { + val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId) + if (canChangeTaskPosition(task)) { + wct.setBounds(task.token, initialBounds) + } } if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 4f511a901756..24b2e4879546 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -24,8 +24,10 @@ import android.os.SystemClock import android.os.SystemProperties import android.os.UserHandle import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CLOSE import android.window.DesktopModeFlags +import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo @@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler( */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { + logV("finishDragToDesktop: not in progress, returning") // Don't attempt to finish a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully // started. return null } - if (requireTransitionState().startAborted) { + val state = requireTransitionState() + if (state.startAborted) { + logV("finishDragToDesktop: start was aborted, clearing state") // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. clearState() return null } - return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + if (state.startInterrupted) { + logV("finishDragToDesktop: start was interrupted, returning") + // We should only have interrupted the start transition after receiving a cancel/end + // request, let that existing request play out and just return here. + return null + } + state.endTransitionToken = + transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + return state.endTransitionToken } /** @@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler( clearState() return } + if (state.startInterrupted) { + // We should only have interrupted the start transition after receiving a cancel/end + // request, let that existing request play out and just return here. + return + } state.cancelState = cancelState if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { @@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler( // transient to start and merge. Animate the cancellation (scale back to original // bounds) first before actually starting the cancel transition so that the wallpaper // is visible behind the animating task. - startCancelAnimation() + state.activeCancelAnimation = startCancelAnimation() } else if ( state.draggedTaskChange != null && (cancelState == CancelState.CANCEL_SPLIT_LEFT || @@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler( ) { if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { // TODO(b/388853233): add support for dragging split task to bubble - startCancelAnimation() + state.activeCancelAnimation = startCancelAnimation() } else { // Animation is handled by BubbleController val wct = WindowContainerTransaction() @@ -357,6 +375,19 @@ sealed class DragToDesktopTransitionHandler( ): Boolean { val state = requireTransitionState() + if ( + handleCancelOrExitAfterInterrupt( + transition, + info, + startTransaction, + finishTransaction, + finishCallback, + state, + ) + ) { + return true + } + val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && transition == state.startTransitionToken @@ -539,6 +570,58 @@ sealed class DragToDesktopTransitionHandler( } } + private fun handleCancelOrExitAfterInterrupt( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: Transitions.TransitionFinishCallback, + state: TransitionState, + ): Boolean { + if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { + return false + } + val isCancelDragToDesktop = + info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && + transition == state.cancelTransitionToken + val isEndDragToDesktop = + info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && + transition == state.endTransitionToken + // We should only receive cancel or end transitions through startAnimation() if the + // start transition was interrupted while a cancel- or end-transition had already + // been requested. Finish the cancel/end transition to avoid having to deal with more + // incoming transitions, and clear the state for the next start-drag transition. + if (!isCancelDragToDesktop && !isEndDragToDesktop) { + return false + } + if (!state.startInterrupted) { + logW( + "Not interrupted, but received startAnimation for cancel/end drag." + + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" + ) + return false + } + logV( + "startAnimation: interrupted -> " + + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" + ) + if (isEndDragToDesktop) { + setupEndDragToDesktop(info, startTransaction, finishTransaction) + animateEndDragToDesktop(startTransaction = startTransaction, finishCallback) + } else { // isCancelDragToDesktop + // Similar to when we merge the cancel transition: ensure all tasks involved in the + // cancel transition are shown, and finish the transition immediately. + info.changes.forEach { change -> + startTransaction.show(change.leash) + finishTransaction.show(change.leash) + } + } + startTransaction.apply() + finishCallback.onTransitionFinished(/* wct= */ null) + clearState() + return true + } + /** * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is @@ -590,6 +673,7 @@ sealed class DragToDesktopTransitionHandler( ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { logV("mergeAnimation: end-transition, target=$mergeTarget") + state.mergedEndTransition = true setupEndDragToDesktop( info, startTransaction = startT, @@ -617,6 +701,41 @@ sealed class DragToDesktopTransitionHandler( return } logW("unhandled merge transition: transitionInfo=$info") + // Handle unknown incoming transitions by finishing the start transition. For now, only do + // this if we've already requested a cancel- or end transition. If we've already merged the + // end-transition, or if the end-transition is running on its own, then just wait until that + // finishes instead. If we've merged the cancel-transition we've finished the + // start-transition and won't reach this code. + if ( + mergeTarget == state.startTransitionToken && + isCancelOrEndTransitionRequested(state) && + !state.mergedEndTransition + ) { + interruptStartTransition(state) + } + } + + private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean = + state.cancelTransitionToken != null || state.endTransitionToken != null + + private fun interruptStartTransition(state: TransitionState) { + if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { + return + } + logV("interruptStartTransition") + state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) + state.dragAnimator.cancelAnimator() + state.activeCancelAnimation?.removeAllListeners() + state.activeCancelAnimation?.cancel() + state.activeCancelAnimation = null + // Keep the transition state so we can deal with Cancel/End properly in #startAnimation. + state.startInterrupted = true + dragToDesktopStateListener?.onTransitionInterrupted() + // Cancel CUJs here as they won't be accurate now that an incoming transition is playing. + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) + LatencyTracker.getInstance(context) + .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) } protected open fun setupEndDragToDesktop( @@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler( } ?: false } - private fun startCancelAnimation() { + private fun startCancelAnimation(): Animator { val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator @@ -800,7 +919,7 @@ sealed class DragToDesktopTransitionHandler( val dx = targetX - x val dy = targetY - y val tx: SurfaceControl.Transaction = transactionSupplier.get() - ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) + return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) .apply { addUpdateListener { animator -> @@ -818,6 +937,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { + state.activeCancelAnimation = null dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() @@ -910,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler( val dragLayer: Int, ) + /** Listener for various events happening during the DragToDesktop transition. */ interface DragToDesktopStateListener { + /** Indicates that the animation into Desktop has started. */ fun onCommitToDesktopAnimationStart() + /** Called when the animation to cancel the desktop-drag has finished. */ fun onCancelToDesktopAnimationEnd() + + /** Indicates that the drag-to-desktop transition has been interrupted. */ + fun onTransitionInterrupted() } sealed class TransitionState { @@ -930,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler( abstract var cancelState: CancelState abstract var startAborted: Boolean abstract val visualIndicator: DesktopModeVisualIndicator? + abstract var startInterrupted: Boolean + abstract var endTransitionToken: IBinder? + abstract var mergedEndTransition: Boolean + abstract var activeCancelAnimation: Animator? data class FromFullscreen( override val draggedTaskId: Int, @@ -945,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, + override var startInterrupted: Boolean = false, + override var endTransitionToken: IBinder? = null, + override var mergedEndTransition: Boolean = false, + override var activeCancelAnimation: Animator? = null, var otherRootChanges: MutableList<Change> = mutableListOf(), ) : TransitionState() @@ -962,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, + override var startInterrupted: Boolean = false, + override var endTransitionToken: IBinder? = null, + override var mergedEndTransition: Boolean = false, + override var activeCancelAnimation: Animator? = null, var splitRootChange: Change? = null, var otherSplitTask: Int, ) : TransitionState() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 7baef2b2dc97..bde46a1bc375 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -361,6 +361,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = rootView; final boolean fontScaleChanged = mWindowDecorConfig != null && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale; + final boolean localeListChanged = mWindowDecorConfig != null + && !mWindowDecorConfig.getLocales() + .equals(mTaskInfo.getConfiguration().getLocales()); final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -376,7 +379,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode || mDecorWindowContext == null - || fontScaleChanged) { + || fontScaleChanged + || localeListChanged) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt index 801048adda4d..957898fd0088 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Bitmap +import android.os.LocaleList import android.os.UserHandle import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting @@ -80,6 +81,13 @@ class WindowDecorTaskResourceLoader( */ private val existingTasks = mutableSetOf<Int>() + /** + * A map of task -> localeList to keep track of the language of app name that's currently + * cached in |taskToResourceCache|. + */ + @VisibleForTesting + val localeListOnCache = ConcurrentHashMap<Int, LocaleList>() + init { shellInit.addInitCallback(this::onInit, this) } @@ -99,11 +107,14 @@ class WindowDecorTaskResourceLoader( fun getName(taskInfo: RunningTaskInfo): CharSequence { checkWindowDecorExists(taskInfo) val cachedResources = taskToResourceCache[taskInfo.taskId] - if (cachedResources != null) { + val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId] + if (cachedResources != null && + taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) { return cachedResources.appName } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.appName } @@ -117,6 +128,7 @@ class WindowDecorTaskResourceLoader( } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.appIcon } @@ -130,6 +142,7 @@ class WindowDecorTaskResourceLoader( } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.veilIcon } @@ -142,6 +155,7 @@ class WindowDecorTaskResourceLoader( fun onWindowDecorClosed(taskInfo: RunningTaskInfo) { existingTasks.remove(taskInfo.taskId) taskToResourceCache.remove(taskInfo.taskId) + localeListOnCache.remove(taskInfo.taskId) } private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 598a101b8bcd..597e4a55ed0e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -59,6 +59,7 @@ import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.impl.CompatUIRequests; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.sysui.ShellController; @@ -738,6 +739,22 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mController, never()).removeLayouts(taskInfo.taskId); } + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + public void testSendCompatUIRequest_createRestartDialog() { + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false); + doReturn(true).when(mMockRestartDialogLayout) + .needsToBeRecreated(any(TaskInfo.class), + any(ShellTaskOrganizer.TaskListener.class)); + doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled(); + doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo)); + + mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog( + taskInfo, mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + } + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, /* isFocused */ false, /* isTopActivityTransparent */ false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index e9f92cfd7c56..0c585b3e843a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -431,6 +431,38 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX, + ) + fun startAndAnimateLaunchTransition_withMinimizeChange_wrongTaskId_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask, mode = TRANSIT_OPEN) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = Int.MAX_VALUE, + minimizingTaskId = minimizingTask.taskId, + ) + mixedHandler.startAnimation( + transition, + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) {} + + verify(rootTaskDisplayAreaOrganizer) + .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any()) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() { val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index d093629000f3..75308442d76a 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 @@ -31,6 +31,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE @@ -1133,6 +1134,54 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES) + fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() { + // Setup existing task. + val existingTask = setUpFreeformTask(active = true) + val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class") + existingTask.topActivity = testComponent + existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) + // Set up new instance of already existing task. + val launchingTask = setUpFullscreenTask() + launchingTask.topActivity = testComponent + launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + + // Move new instance to desktop. By default multi instance is not supported so first + // instance will close. + val wct = WindowContainerTransaction() + controller.addMoveToDeskTaskChanges(wct, launchingTask, deskId = 0) + + // New instance should inherit task bounds of old instance. + assertThat(findBoundsChange(wct, launchingTask)) + .isEqualTo(existingTask.configuration.windowConfiguration.bounds) + } + + @Test + @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES) + fun handleRequest_newTaskInstance_inheritsClosingInstanceBounds() { + setUpLandscapeDisplay() + // Setup existing task. + val existingTask = setUpFreeformTask(active = true) + val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class") + existingTask.topActivity = testComponent + existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) + // Set up new instance of already existing task. + val launchingTask = setUpFreeformTask(active = false) + taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId) + launchingTask.topActivity = testComponent + launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + + // Move new instance to desktop. By default multi instance is not supported so first + // instance will close. + val wct = controller.handleRequest(Binder(), createTransition(launchingTask)) + + assertNotNull(wct, "should handle request") + val finalBounds = findBoundsChange(wct, launchingTask) + // New instance should inherit task bounds of old instance. + assertThat(finalBounds).isEqualTo(existingTask.configuration.windowConfiguration.bounds) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { setUpLandscapeDisplay() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 0871d38ceb46..6e7adf368155 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder @@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP +import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT @@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .mockStatic(SystemProperties::class.java) .startMocking() + whenever( + transitions.startTransition( + eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), + /* wct= */ any(), + eq(defaultHandler), + ) + ) + .thenReturn(mock<IBinder>()) } @After @@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startTransition = startDrag(defaultHandler, task) val endTransition = mock<IBinder>() defaultHandler.onTaskResizeAnimationListener = mock() - defaultHandler.mergeAnimation( + mergeAnimation( transition = endTransition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task, - ), - startT = mock<SurfaceControl.Transaction>(), - finishT = mock<SurfaceControl.Transaction>(), + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + task = task, mergeTarget = startTransition, - finishCallback = mock<Transitions.TransitionFinishCallback>(), ) defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) @@ -701,6 +706,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() { + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + defaultHandler.onTaskResizeAnimationListener = mock() + val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback) + + mergeInterruptingTransition(mergeTarget = startTransition) + + verify(finishCallback, never()).onTransitionFinished(anyOrNull()) + verify(dragAnimator, never()).cancelAnimator() + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() { + val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(defaultHandler, task, finishCallback = startDragFinishCallback) + defaultHandler.onTaskResizeAnimationListener = mock() + mergeAnimation( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + task = task, + mergeTarget = startTransition, + ) + + mergeInterruptingTransition(mergeTarget = startTransition) + + verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() { + val task1 = createTask() + val startTransition = startDrag(defaultHandler, task1) + val endTransition = + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) + val startTransaction = mock<SurfaceControl.Transaction>() + val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>() + defaultHandler.onTaskResizeAnimationListener = mock() + mergeInterruptingTransition(mergeTarget = startTransition) + + val didAnimate = + defaultHandler.startAnimation( + transition = requireNotNull(endTransition), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task1, + ), + startTransaction = startTransaction, + finishTransaction = mock(), + finishCallback = endDragFinishCallback, + ) + + assertThat(didAnimate).isTrue() + verify(startTransaction).apply() + verify(endDragFinishCallback).onTransitionFinished(anyOrNull()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() { + val task1 = createTask() + val startTransition = startDrag(defaultHandler, task1) + verify(dragAnimator).startAnimation() + val endTransition = + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.onTaskResizeAnimationListener = mock() + mergeInterruptingTransition(mergeTarget = startTransition) + defaultHandler.startAnimation( + transition = requireNotNull(endTransition), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task1, + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = mock(), + ) + + startDrag(defaultHandler, createTask()) + + verify(dragAnimator, times(2)).startAnimation() + } + + private fun mergeInterruptingTransition(mergeTarget: IBinder) { + defaultHandler.mergeAnimation( + transition = mock<IBinder>(), + info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()), + startT = mock(), + finishT = mock(), + mergeTarget = mergeTarget, + finishCallback = mock(), + ) + } + + private fun mergeAnimation( + transition: IBinder = mock(), + type: Int, + mergeTarget: IBinder, + task: RunningTaskInfo, + ) { + defaultHandler.mergeAnimation( + transition = transition, + info = createTransitionInfo(type = type, draggedTask = task), + startT = mock(), + finishT = mock(), + mergeTarget = mergeTarget, + finishCallback = mock(), + ) + } + + @Test fun getAnimationFraction_returnsFraction() { val fraction = SpringDragToDesktopTransitionHandler.getAnimationFraction( @@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { finishTransaction: SurfaceControl.Transaction = mock(), homeChange: TransitionInfo.Change? = createHomeChange(), transitionRootLeash: SurfaceControl = mock(), + finishCallback: Transitions.TransitionFinishCallback = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ), startTransaction = startTransaction, finishTransaction = finishTransaction, - finishCallback = {}, + finishCallback = finishCallback, ) return transition } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt index 6ecebd76a951..75f6bda4d750 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt @@ -203,7 +203,7 @@ class GroupedTaskInfoTest : ShellTestCase() { assertThat(taskInfoFromParcel.taskInfoList).hasSize(3) // Only compare task ids val taskIdComparator = Correspondence.transforming<TaskInfo, Int>( - { it?.taskId }, "has taskId of" + { it.taskId }, "has taskId of" ) assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator) .containsExactly(1, 2, 3).inOrder() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index a2927fa3527b..9a2e2fad50be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -60,6 +60,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; +import android.os.LocaleList; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; @@ -97,6 +98,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.function.Supplier; /** @@ -475,6 +477,50 @@ public class WindowDecorationTests extends ShellTestCase { } @Test + public void testReinflateViewsOnLocaleListChange() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US)); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + + final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo2.configuration.setLocales(new LocaleList(Locale.US, Locale.FRANCE)); + windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since the locale list has changed. + verify(windowDecor, times(1)).releaseViews(any()); + } + + @Test + public void testViewNotReinflatedWhenLocaleListNotChanged() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US)); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should not be called since nothing has changed. + verify(windowDecor, never()).releaseViews(any()); + } + + @Test public void testLayoutResultCalculation_fullWidthCaption() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt index c61e0eb3b5af..c8ccac35d4c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -23,6 +23,7 @@ import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable +import android.os.LocaleList import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableContext @@ -39,6 +40,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources import com.google.common.truth.Truth.assertThat +import java.util.Locale import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test @@ -116,8 +118,10 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { @Test fun testGetName_cached_returnsFromCache() { val task = createTaskInfo(context.userId) + task.configuration.setLocales(LocaleList(Locale.US)) loader.onWindowDecorCreated(task) loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + loader.localeListOnCache[task.taskId] = LocaleList(Locale.US) loader.getName(task) @@ -130,6 +134,19 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { } @Test + fun testGetName_cached_localesChanged_loadsResourceAndCaches() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + loader.localeListOnCache[task.taskId] = LocaleList(Locale.US, Locale.FRANCE) + task.configuration.setLocales(LocaleList(Locale.FRANCE, Locale.US)) + doReturn("App Name but in French").whenever(mockPackageManager).getApplicationLabel(any()) + + assertThat(loader.getName(task)).isEqualTo("App Name but in French") + assertThat(loader.taskToResourceCache[task.taskId]?.appName).isEqualTo("App Name but in French") + } + + @Test fun testGetHeaderIcon_notCached_loadsResourceAndCaches() { val task = createTaskInfo(context.userId) loader.onWindowDecorCreated(task) diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp index 7e14b88a47fa..ef5406250251 100644 --- a/libs/hostgraphics/HostBufferQueue.cpp +++ b/libs/hostgraphics/HostBufferQueue.cpp @@ -29,6 +29,7 @@ public: } virtual status_t detachBuffer(int slot) { + mBuffer.clear(); return OK; } diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 4e86eacea404..f3b21bfdaa3c 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -3836,6 +3836,151 @@ public final class MediaCodecInfo { maxBlocks, maxBlocksPerSecond, blockSize, blockSize, 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (GetFlag(() -> android.media.codec.Flags.apvSupport()) + && mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_APV)) { + maxBlocksPerSecond = 11880; + maxBps = 7000000; + + // Sample rate, and Bit rate for APV Codec, + // corresponding to the definitions in + // "10.1.4. Levels and bands" + // found at https://www.ietf.org/archive/id/draft-lim-apv-03.html + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int BR = 0; // bit rate bps + switch (profileLevel.level) { + case CodecProfileLevel.APVLevel1Band0: + SR = 3041280; BR = 7000000; break; + case CodecProfileLevel.APVLevel1Band1: + SR = 3041280; BR = 11000000; break; + case CodecProfileLevel.APVLevel1Band2: + SR = 3041280; BR = 14000000; break; + case CodecProfileLevel.APVLevel1Band3: + SR = 3041280; BR = 21000000; break; + case CodecProfileLevel.APVLevel11Band0: + SR = 6082560; BR = 14000000; break; + case CodecProfileLevel.APVLevel11Band1: + SR = 6082560; BR = 21000000; break; + case CodecProfileLevel.APVLevel11Band2: + SR = 6082560; BR = 28000000; break; + case CodecProfileLevel.APVLevel11Band3: + SR = 6082560; BR = 42000000; break; + case CodecProfileLevel.APVLevel2Band0: + SR = 15667200; BR = 36000000; break; + case CodecProfileLevel.APVLevel2Band1: + SR = 15667200; BR = 53000000; break; + case CodecProfileLevel.APVLevel2Band2: + SR = 15667200; BR = 71000000; break; + case CodecProfileLevel.APVLevel2Band3: + SR = 15667200; BR = 106000000; break; + case CodecProfileLevel.APVLevel21Band0: + SR = 31334400; BR = 71000000; break; + case CodecProfileLevel.APVLevel21Band1: + SR = 31334400; BR = 106000000; break; + case CodecProfileLevel.APVLevel21Band2: + SR = 31334400; BR = 141000000; break; + case CodecProfileLevel.APVLevel21Band3: + SR = 31334400; BR = 212000000; break; + case CodecProfileLevel.APVLevel3Band0: + SR = 66846720; BR = 101000000; break; + case CodecProfileLevel.APVLevel3Band1: + SR = 66846720; BR = 151000000; break; + case CodecProfileLevel.APVLevel3Band2: + SR = 66846720; BR = 201000000; break; + case CodecProfileLevel.APVLevel3Band3: + SR = 66846720; BR = 301000000; break; + case CodecProfileLevel.APVLevel31Band0: + SR = 133693440; BR = 201000000; break; + case CodecProfileLevel.APVLevel31Band1: + SR = 133693440; BR = 301000000; break; + case CodecProfileLevel.APVLevel31Band2: + SR = 133693440; BR = 401000000; break; + case CodecProfileLevel.APVLevel31Band3: + SR = 133693440; BR = 602000000; break; + case CodecProfileLevel.APVLevel4Band0: + SR = 265420800; BR = 401000000; break; + case CodecProfileLevel.APVLevel4Band1: + SR = 265420800; BR = 602000000; break; + case CodecProfileLevel.APVLevel4Band2: + SR = 265420800; BR = 780000000; break; + case CodecProfileLevel.APVLevel4Band3: + SR = 265420800; BR = 1170000000; break; + case CodecProfileLevel.APVLevel41Band0: + SR = 530841600; BR = 780000000; break; + case CodecProfileLevel.APVLevel41Band1: + SR = 530841600; BR = 1170000000; break; + case CodecProfileLevel.APVLevel41Band2: + SR = 530841600; BR = 1560000000; break; + case CodecProfileLevel.APVLevel41Band3: + // Current API allows bitrates only up to Max Integer + // Hence we are limiting internal limits to Integer.MAX_VALUE + // even when actual Level/Band limits are higher + SR = 530841600; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band0: + SR = 1061683200; BR = 1560000000; break; + case CodecProfileLevel.APVLevel5Band1: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band2: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band3: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel51Band0: + case CodecProfileLevel.APVLevel51Band1: + case CodecProfileLevel.APVLevel51Band2: + case CodecProfileLevel.APVLevel51Band3: + SR = 2123366400; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel6Band0: + case CodecProfileLevel.APVLevel6Band1: + case CodecProfileLevel.APVLevel6Band2: + case CodecProfileLevel.APVLevel6Band3: + SR = 4777574400L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel61Band0: + case CodecProfileLevel.APVLevel61Band1: + case CodecProfileLevel.APVLevel61Band2: + case CodecProfileLevel.APVLevel61Band3: + SR = 8493465600L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel7Band0: + case CodecProfileLevel.APVLevel7Band1: + case CodecProfileLevel.APVLevel7Band2: + case CodecProfileLevel.APVLevel7Band3: + SR = 16986931200L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel71Band0: + case CodecProfileLevel.APVLevel71Band1: + case CodecProfileLevel.APVLevel71Band2: + case CodecProfileLevel.APVLevel71Band3: + SR = 33973862400L; BR = Integer.MAX_VALUE; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.APVProfile422_10: + case CodecProfileLevel.APVProfile422_10HDR10: + case CodecProfileLevel.APVProfile422_10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBps = Math.max(BR, maxBps); + } + + final int blockSize = 16; + maxBlocks = Integer.MAX_VALUE; + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + maxBlocks = (int) Math.min((long) maxBlocks, maxBlocksPerSecond); + // Max frame size in APV is 2^24 + int maxLengthInBlocks = Utils.divUp((int) Math.pow(2, 24), blockSize); + maxLengthInBlocks = Math.min(maxLengthInBlocks, maxBlocks); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 2 /* widthAlignment */, 1 /* heightAlignment */); } else { Log.w(TAG, "Unsupported mime " + mime); // using minimal bitrate here. should be overriden by 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 0c64fa7d34dd..d55bbb3bbdbf 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -447,6 +447,10 @@ public class IllustrationPreference extends Preference implements GroupSectionDi illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio)); } + public boolean isAnimatable() { + return mIsAnimatable; + } + private void startAnimation(Drawable drawable) { if (!(drawable instanceof Animatable)) { return; diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 42f576a9a151..3c86f28cccb3 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2121,3 +2121,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/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt index d08d859ec0d7..fc4d53af4b53 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -32,7 +31,6 @@ import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.modifier.ModifierLocalModifierNode import androidx.compose.ui.node.DrawModifierNode @@ -50,11 +48,7 @@ import androidx.compose.ui.util.fastForEach * The elements redirected to this container will be drawn above the content of this composable. */ fun Modifier.container(state: ContainerState): Modifier { - return onPlaced { state.lastOffsetInWindow = it.positionInWindow() } - .drawWithContent { - drawContent() - state.drawInOverlay(this) - } + return this then ContainerElement(state) } /** @@ -105,6 +99,30 @@ internal interface LayerRenderer { fun drawInOverlay(drawScope: DrawScope) } +private data class ContainerElement(private val state: ContainerState) : + ModifierNodeElement<ContainerNode>() { + override fun create(): ContainerNode { + return ContainerNode(state) + } + + override fun update(node: ContainerNode) { + node.state = state + } +} + +/** A node implementing [container] that can be delegated to. */ +class ContainerNode(var state: ContainerState) : + Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode { + override fun onPlaced(coordinates: LayoutCoordinates) { + state.lastOffsetInWindow = coordinates.positionInWindow() + } + + override fun ContentDrawScope.draw() { + drawContent() + state.drawInOverlay(this) + } +} + private data class DrawInContainerElement( var state: ContainerState, var enabled: () -> Boolean, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 061fdd99eb1b..0a711487ccb1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene( modifier = Modifier.padding(horizontal = 16.dp), ) } - else -> CollapsedShadeHeader(viewModel = headerViewModel) + else -> + CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 542f081fe469..8f0fb20cef36 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -258,15 +258,18 @@ fun ContentScope.QuickSettingsLayout( modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding), ) - BrightnessSliderContainer( - viewModel = viewModel.brightnessSliderViewModel, - containerColors = ContainerColors.singleColor(OverlayShade.Colors.PanelBackground), - modifier = - Modifier.systemGestureExclusionInShade( - enabled = { layoutState.transitionState is TransitionState.Idle } - ) - .fillMaxWidth(), - ) + Box( + Modifier.systemGestureExclusionInShade( + enabled = { layoutState.transitionState is TransitionState.Idle } + ) + ) { + BrightnessSliderContainer( + viewModel = viewModel.brightnessSliderViewModel, + containerColors = + ContainerColors.singleColor(OverlayShade.Colors.PanelBackground), + modifier = Modifier.fillMaxWidth(), + ) + } Box { GridAnchor() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 23baeacd76ec..86c8fc34a63c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -127,6 +127,7 @@ object ShadeHeader { @Composable fun ContentScope.CollapsedShadeHeader( viewModel: ShadeHeaderViewModel, + isSplitShade: Boolean, modifier: Modifier = Modifier, ) { val cutoutLocation = LocalDisplayCutout.current.location @@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader( } } - val isShadeLayoutWide = viewModel.isShadeLayoutWide - val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. @@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { - Clock(scale = 1f, onClick = viewModel::onClockClicked) + Clock(onClick = viewModel::onClockClicked) VariableDayDate( longerDateText = viewModel.longerDateText, shorterDateText = viewModel.shorterDateText, @@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader( Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) .padding(horizontal = horizontalPadding), ) { - if (isShadeLayoutWide) { + if (isSplitShade) { ShadeCarrierGroup(viewModel = viewModel) } SystemIconChip( - onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } + onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade } ) { StatusIcons( viewModel = viewModel, @@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader( .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight), ) { Box(modifier = Modifier.fillMaxWidth()) { - Box { - Clock( - scale = 2.57f, - onClick = viewModel::onClockClicked, - modifier = Modifier.align(Alignment.CenterStart), - ) - } + Clock( + onClick = viewModel::onClockClicked, + modifier = Modifier.align(Alignment.CenterStart), + scale = 2.57f, + ) Box( modifier = Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth() @@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) - val isShadeLayoutWide = viewModel.isShadeLayoutWide - val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. @@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader( startContent = { Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { val chipHighlight = viewModel.notificationsChipHighlight - if (isShadeLayoutWide) { + if (viewModel.showClock) { Clock( - scale = 1f, onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) - Spacer(modifier = Modifier.width(5.dp)) } NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, @@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader( } @Composable -private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun ContentScope.Clock( + onClick: () -> Unit, + modifier: Modifier = Modifier, + scale: Float = 1f, +) { val layoutDirection = LocalLayoutDirection.current ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 5040490da8f6..885d34fb95c9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey @@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateContentDpAsState +import com.android.compose.animation.scene.animateContentFloatAsState import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.padding @@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene( viewModel = viewModel, headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qqsMediaHost, modifier = modifier, @@ -253,9 +251,6 @@ private fun ContentScope.SingleShade( viewModel: ShadeSceneContentViewModel, headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, @@ -340,6 +335,7 @@ private fun ContentScope.SingleShade( content = { CollapsedShadeHeader( viewModel = headerViewModel, + isSplitShade = false, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) @@ -434,15 +430,13 @@ private fun ContentScope.SplitShade( val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by - animateSceneFloatAsState( + animateContentFloatAsState( value = 1f, key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false, ) val unfoldTranslationXForStartSide by viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f) - val unfoldTranslationXForEndSide by - viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f) val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings) val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() @@ -512,6 +506,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( viewModel = headerViewModel, + isSplitShade = true, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 72ee75ad2d47..90bf92ae1dd0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -32,12 +32,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ApproachLayoutModifierNode +import androidx.compose.ui.layout.ApproachMeasureScope import androidx.compose.ui.layout.LookaheadScope -import androidx.compose.ui.layout.approachLayout -import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.zIndex import com.android.compose.animation.scene.Ancestor import com.android.compose.animation.scene.AnimatedState import com.android.compose.animation.scene.ContentKey @@ -68,8 +73,8 @@ import com.android.compose.gesture.NestedScrollControlState import com.android.compose.gesture.NestedScrollableBound import com.android.compose.gesture.nestedScrollController import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.ContainerNode import com.android.compose.ui.graphics.ContainerState -import com.android.compose.ui.graphics.container import kotlin.math.pow /** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */ @@ -158,24 +163,14 @@ internal sealed class Content( fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) { // If this content has a custom factory, provide it to the content so that the factory is // automatically used when calling rememberOverscrollEffect(). + val isElevationPossible = + layoutImpl.state.isElevationPossible(content = key, element = null) Box( - modifier - .thenIf(isInvisible) { InvisibleModifier } - .zIndex(zIndex) - .approachLayout( - isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() } - ) { measurable, constraints -> - // TODO(b/353679003): Use the ModifierNode API to set this *before* the - // approach - // pass is started. - targetSize = lookaheadSize - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { placeable.place(0, 0) } - } - .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) { - Modifier.container(containerState) - } - .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) } + modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf( + layoutImpl.implicitTestTags + ) { + Modifier.testTag(key.testTag) + } ) { CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) { scope.content() @@ -194,6 +189,72 @@ internal sealed class Content( } } +private data class ContentElement( + private val content: Content, + private val isElevationPossible: Boolean, + private val isInvisible: Boolean, +) : ModifierNodeElement<ContentNode>() { + override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible) + + override fun update(node: ContentNode) { + node.update(content, isElevationPossible, isInvisible) + } +} + +private class ContentNode( + private var content: Content, + private var isElevationPossible: Boolean, + private var isInvisible: Boolean, +) : DelegatingNode(), ApproachLayoutModifierNode { + private var containerDelegate = containerDelegate(isElevationPossible) + + private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? { + return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null + } + + fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) { + if (content != this.content || isElevationPossible != this.isElevationPossible) { + this.content = content + this.isElevationPossible = isElevationPossible + + containerDelegate?.let { undelegate(it) } + containerDelegate = containerDelegate(isElevationPossible) + } + + this.isInvisible = isInvisible + } + + override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + check(isLookingAhead) + return measurable.measure(constraints).run { + content.targetSize = IntSize(width, height) + layout(width, height) { + if (!isInvisible) { + place(0, 0, zIndex = content.zIndex) + } + } + } + } + + override fun ApproachMeasureScope.approachMeasure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + return measurable.measure(constraints).run { + layout(width, height) { + if (!isInvisible) { + place(0, 0, zIndex = content.zIndex) + } + } + } + } +} + internal class ContentEffects(factory: OverscrollFactory) { val overscrollEffect = factory.createOverscrollEffect() val gestureEffect = GestureEffect(overscrollEffect) @@ -307,8 +368,3 @@ internal class ContentScopeImpl( ) } } - -private val InvisibleModifier = - Modifier.layout { measurable, constraints -> - measurable.measure(constraints).run { layout(width, height) {} } - } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json index 0fcdfa3e1b53..57f67665242c 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json @@ -60,7 +60,9 @@ 912, 928, 944, - 960 + 960, + 976, + 992 ], "features": [ { @@ -310,6 +312,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -491,67 +501,75 @@ }, { "width": 170.4, - "height": 39.2 + "height": 28.8 }, { "width": 166.8, - "height": 36 + "height": 15.2 }, { "width": 164, - "height": 31.6 + "height": 6.4 }, { "width": 162.4, - "height": 26.8 + "height": 0.8 }, { "width": 161.2, - "height": 22 + "height": 0 }, { "width": 160.4, - "height": 17.6 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, - "height": 14 + "height": 0 }, { "width": 160, - "height": 10.8 + "height": 0 }, { "width": 160, - "height": 8 + "height": 0 }, { "width": 160, - "height": 6 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 2.8 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 }, { "width": 160, @@ -627,6 +645,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json index 3196334c5314..01bc852cf7f4 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json @@ -59,7 +59,9 @@ 896, 912, 928, - 944 + 944, + 960, + 976 ], "features": [ { @@ -305,6 +307,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -482,71 +492,79 @@ }, { "width": 171.6, - "height": 39.6 + "height": 32 }, { "width": 167.6, - "height": 36.8 + "height": 18 }, { "width": 164.8, - "height": 32.4 + "height": 8.8 }, { "width": 162.8, - "height": 27.6 + "height": 2.8 }, { "width": 161.6, - "height": 22.8 + "height": 0 }, { "width": 160.8, - "height": 18.4 + "height": 0 }, { "width": 160.4, - "height": 14.4 + "height": 0 }, { "width": 160, - "height": 11.2 + "height": 0 }, { "width": 160, - "height": 8.4 + "height": 0 }, { "width": 160, - "height": 6.4 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 3.2 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.6 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -617,6 +635,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json index 4b0306853903..b6e423afc6c4 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json @@ -336,55 +336,55 @@ }, { "width": 188, - "height": 24.8 + "height": 25.6 }, { "width": 188, - "height": 32.8 + "height": 36.4 }, { "width": 188, - "height": 44 + "height": 45.6 }, { "width": 188, - "height": 57.2 + "height": 59.2 }, { "width": 188, - "height": 70.8 + "height": 72.8 }, { "width": 188, - "height": 78 + "height": 79.6 }, { "width": 188, - "height": 91.2 + "height": 92.8 }, { "width": 188, - "height": 103.2 + "height": 104.4 }, { "width": 188, - "height": 114.4 + "height": 115.2 }, { "width": 188, - "height": 124.4 + "height": 125.2 }, { "width": 188, - "height": 134 + "height": 134.8 }, { "width": 188, - "height": 142.8 + "height": 143.2 }, { "width": 188, - "height": 150.8 + "height": 151.2 }, { "width": 188, @@ -392,7 +392,7 @@ }, { "width": 188, - "height": 159.6 + "height": 160 }, { "width": 188, @@ -400,7 +400,7 @@ }, { "width": 188, - "height": 174 + "height": 174.4 }, { "width": 188, @@ -412,7 +412,7 @@ }, { "width": 188, - "height": 187.6 + "height": 188 }, { "width": 188, diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json index 10a9ba7e2760..a82db346ed58 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json @@ -39,7 +39,9 @@ 576, 592, 608, - 624 + 624, + 640, + 656 ], "features": [ { @@ -205,6 +207,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -302,67 +312,75 @@ }, { "width": 170.8, - "height": 39.6 + "height": 29.6 }, { "width": 166.8, - "height": 36.8 + "height": 12.8 }, { "width": 164, - "height": 32.4 + "height": 2.4 }, { "width": 162, - "height": 27.6 + "height": 0 }, { "width": 160.8, - "height": 22.8 + "height": 0 }, { "width": 160.4, - "height": 18.4 + "height": 0 }, { "width": 160, - "height": 14.4 + "height": 0 }, { "width": 160, - "height": 11.2 + "height": 0 }, { "width": 160, - "height": 8.4 + "height": 0 }, { "width": 160, - "height": 6 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 3.2 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -417,6 +435,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json index d8bf48d32d20..6dc5a0e79e81 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json @@ -32,8 +32,7 @@ 464, 480, 496, - 512, - 528 + 512 ], "features": [ { @@ -163,10 +162,6 @@ { "x": 50, "y": 50 - }, - { - "x": 50, - "y": 50 } ] }, @@ -228,63 +223,59 @@ }, { "width": 188, - "height": 42.4 + "height": 44.4 }, { "width": 188, - "height": 100 + "height": 103.6 }, { "width": 188, - "height": 161.6 + "height": 166 }, { "width": 188, - "height": 218 + "height": 222.4 }, { "width": 188, - "height": 265.6 + "height": 270 }, { "width": 188, - "height": 303.6 + "height": 307.2 }, { "width": 188, - "height": 332.4 + "height": 335.6 }, { "width": 188, - "height": 354 + "height": 356.4 }, { "width": 188, - "height": 369.2 + "height": 371.2 }, { "width": 188, - "height": 380 + "height": 381.6 }, { "width": 188, - "height": 387.2 + "height": 388.8 }, { "width": 188, - "height": 392 + "height": 393.2 }, { "width": 188, - "height": 395.2 + "height": 396 }, { "width": 188, - "height": 397.6 - }, - { - "width": 188, - "height": 398.4 + "height": 398 }, { "width": 188, @@ -356,7 +347,6 @@ 1, 1, 1, - 1, 1 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json index 57bdf3e1ecab..1cd971aa2898 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json @@ -69,9 +69,7 @@ 1056, 1072, 1088, - 1104, - 1120, - 1136 + 1104 ], "features": [ { @@ -353,14 +351,6 @@ { "x": 64, "y": 50 - }, - { - "x": 64, - "y": 50 - }, - { - "x": 64, - "y": 50 } ] }, @@ -466,43 +456,43 @@ }, { "width": 188, - "height": 24 + "height": 24.8 }, { "width": 188, - "height": 29.6 + "height": 30 }, { "width": 188, - "height": 37.2 + "height": 38 }, { "width": 188, - "height": 45.6 + "height": 46 }, { "width": 188, - "height": 53.6 + "height": 54 }, { "width": 188, - "height": 60.4 + "height": 61.2 }, { "width": 188, - "height": 66.4 + "height": 66.8 }, { "width": 188, - "height": 71.2 + "height": 71.6 }, { "width": 188, - "height": 75.2 + "height": 75.6 }, { "width": 188, - "height": 77.6 + "height": 78 }, { "width": 188, @@ -510,7 +500,7 @@ }, { "width": 188, - "height": 80.4 + "height": 80.8 }, { "width": 188, @@ -522,7 +512,7 @@ }, { "width": 188, - "height": 79.2 + "height": 79.6 }, { "width": 187.6, @@ -530,7 +520,7 @@ }, { "width": 186.8, - "height": 76 + "height": 76.4 }, { "width": 186, @@ -578,43 +568,39 @@ }, { "width": 172.4, - "height": 39.2 + "height": 37.6 }, { "width": 170.8, - "height": 38.4 + "height": 38 }, { "width": 169.2, - "height": 34.8 + "height": 30.4 }, { "width": 167.6, - "height": 30 + "height": 25.2 }, { "width": 166, - "height": 25.2 + "height": 20.4 }, { "width": 164, - "height": 20.4 + "height": 16 }, { "width": 162.4, - "height": 16.4 + "height": 12.4 }, { "width": 160.8, - "height": 12.8 - }, - { - "width": 160, - "height": 9.6 + "height": 9.2 }, { "width": 160, - "height": 7.2 + "height": 6.8 }, { "width": 160, @@ -626,7 +612,7 @@ }, { "width": 160, - "height": 2.8 + "height": 2.4 }, { "width": 160, @@ -634,10 +620,6 @@ }, { "width": 160, - "height": 1.2 - }, - { - "width": 160, "height": 0.8 }, { @@ -646,7 +628,7 @@ }, { "width": 160, - "height": 0 + "height": 0.4 }, { "width": 160, @@ -735,8 +717,6 @@ 0.03147719, 0.019312752, 0.011740655, - 0, - 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json index 9aa91c2d5e17..1030455e873f 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json @@ -27,7 +27,9 @@ 384, 400, 416, - 432 + 432, + 448, + 464 ], "features": [ { @@ -145,6 +147,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -194,67 +204,75 @@ }, { "width": 168.8, - "height": 38.4 + "height": 22.4 }, { "width": 165.2, - "height": 34.8 + "height": 10 }, { "width": 162.8, - "height": 30 + "height": 2.4 }, { "width": 161.2, - "height": 25.2 + "height": 0 }, { "width": 160.4, - "height": 20.4 + "height": 0 }, { "width": 160, - "height": 16.4 + "height": 0 }, { "width": 160, - "height": 12.8 + "height": 0 }, { "width": 160, - "height": 9.6 + "height": 0 }, { "width": 160, - "height": 7.2 + "height": 0 }, { "width": 160, - "height": 5.2 + "height": 0 }, { "width": 160, - "height": 3.6 + "height": 0 }, { "width": 160, - "height": 2.8 + "height": 0 }, { "width": 160, - "height": 1.6 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -297,6 +315,8 @@ 0, 0, 0, + 0, + 0, 0 ] } 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/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt index 684af6fc8040..efc68f3b6884 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt @@ -64,10 +64,10 @@ class KeyEventInteractorTest : SysuiTestCase() { val isPowerButtonLongPressed by collectLastValue( underTest.isPowerButtonLongPressed) - repository.setPowerButtonBeingLongPressed(false) + repository.setPowerButtonLongPressed(false) assertThat(isPowerButtonLongPressed).isFalse() - repository.setPowerButtonBeingLongPressed(true) + repository.setPowerButtonLongPressed(true) assertThat(isPowerButtonLongPressed).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index a832f486ef32..04eb709b8894 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -94,6 +94,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test + fun showClock_wideLayout_returnsTrue() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = true) + + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) + assertThat(underTest.showClock).isTrue() + + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) + assertThat(underTest.showClock).isTrue() + } + + @Test + fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) + + assertThat(underTest.showClock).isFalse() + } + + @Test + fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) + + assertThat(underTest.showClock).isTrue() + } + + @Test fun onShadeCarrierGroupClicked_launchesNetworkSettings() = testScope.runTest { val activityStarter = kosmos.activityStarter 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/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..54e9f5fcde26 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) @@ -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/coordinator/BundleCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt index 8a9720ea3cb0..732180810880 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt @@ -34,6 +34,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals @SmallTest @RunWith(AndroidJUnit4::class) @@ -87,6 +88,18 @@ class BundleCoordinatorTest : SysuiTestCase() { isFalse() } + @Test + fun testBundler_getBundleIdOrNull_returnBundleId() { + val classifiedEntry = makeEntryOfChannelType(PROMOTIONS_ID) + assertEquals(coordinator.bundler.getBundleIdOrNull(classifiedEntry), PROMOTIONS_ID) + } + + @Test + fun testBundler_getBundleIdOrNull_returnNull() { + val unclassifiedEntry = makeEntryOfChannelType("not system channel") + assertEquals(coordinator.bundler.getBundleIdOrNull(unclassifiedEntry), null) + } + private fun makeEntryOfChannelType( type: String, buildBlock: NotificationEntryBuilder.() -> Unit = {} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt index a90539413adb..e28e587d2cdc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -280,6 +281,8 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() val listEntryList = listOf(group, solo1, solo2) val notificationEntryList = listOf(solo1, solo2, parent, child1, child2) + val bundle = BundleEntry("bundleKey") + val bundleList = listOf(bundle) runCoordinatorTest { // All entries are added (and now unseen) @@ -300,6 +303,11 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { assertThatTopOngoingKey().isEqualTo(null) assertThatTopUnseenKey().isEqualTo(solo1.key) + // TEST: bundle is not picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(bundleList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen solo1.setColorizedFgs(true) onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) 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/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt new file mode 100644 index 000000000000..fb19a884a210 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt @@ -0,0 +1,64 @@ +/* + * 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 + +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.collectLastValue +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SqueezeEffectInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { + SqueezeEffectInteractor( + squeezeEffectRepository = fakeSqueezeEffectRepository + ) + } + + @Test + fun testIsSqueezeEffectDisabled_whenDisabledInRepository() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @Test + fun testIsSqueezeEffectEnabled_whenEnabledInRepository() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt new file mode 100644 index 000000000000..5d8d3f90e13c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt @@ -0,0 +1,96 @@ +/* + * 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 + +import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS +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.collectLastValue +import com.android.systemui.kosmos.runTest +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 +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SqueezeEffectRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val globalSettings = FakeGlobalSettings(StandardTestDispatcher()) + + @Mock + private lateinit var bgHandler: Handler + + private val Kosmos.underTest by Kosmos.Fixture { + SqueezeEffectRepositoryImpl( + bgHandler = bgHandler, + bgCoroutineContext = testScope.testScheduler, + globalSettings = globalSettings + ) + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @DisableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectDisabled_WhenFlagDisabled() = + kosmos.runTest { + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectDisabled_WhenFlagEnabled_GlobalSettingsDisabled() = + kosmos.runTest { + globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0) + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectEnabled_WhenFlagEnabled_GlobalSettingEnabled() = + kosmos.runTest { + globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5) + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt new file mode 100644 index 000000000000..83dc45c8c511 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt @@ -0,0 +1,102 @@ +/* + * 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 + +import android.view.View +import android.view.WindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +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.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +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.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TopLevelWindowEffectsTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + @Mock + private lateinit var windowManager: WindowManager + + @Mock + private lateinit var viewCapture: Lazy<ViewCapture> + + private val Kosmos.underTest by Kosmos.Fixture { + TopLevelWindowEffects( + context = mContext, + applicationScope = testScope.backgroundScope, + windowManager = ViewCaptureAwareWindowManager( + windowManager = windowManager, + lazyViewCapture = viewCapture, + isViewCaptureEnabled = false + ), + squeezeEffectInteractor = SqueezeEffectInteractor( + squeezeEffectRepository = fakeSqueezeEffectRepository + ) + ) + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + doNothing().whenever(windowManager).addView(any<View>(), any<WindowManager.LayoutParams>()) + doNothing().whenever(windowManager).removeView(any<View>()) + doNothing().whenever(windowManager).removeView(any<EffectsWindowRoot>()) + } + + @Test + fun noWindowWhenSqueezeEffectDisabled() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false + + underTest.start() + + verify(windowManager, never()).addView(any<View>(), any<WindowManager.LayoutParams>()) + } + + @Test + fun addViewToWindowWhenSqueezeEffectEnabled() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true + + underTest.start() + + verify(windowManager, times(1)).addView(any<View>(), + any<WindowManager.LayoutParams>()) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml new file mode 100644 index 000000000000..d398f60ddc3c --- /dev/null +++ b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/notification_ripple_untinted_color"> + <item> + <inset + android:insetLeft="0dp" + android:insetTop="8dp" + android:insetRight="0dp" + android:insetBottom="8dp"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/notification_2025_smart_reply_button_corner_radius" /> + <stroke android:width="@dimen/smart_reply_button_stroke_width" + android:color="@color/smart_reply_button_stroke" /> + <solid android:color="@color/smart_reply_button_background"/> + </shape> + </inset> + </item> +</ripple> diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 915563b1ae20..c7add163dffa 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -85,6 +85,7 @@ android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" + android:importantForAccessibility="no" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" @@ -176,6 +177,8 @@ app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="4dp" android:layout_marginBottom="2dp" + android:importantForAccessibility="yes" + android:contentDescription="@string/clipboard_overlay_window_name" android:background="@drawable/clipboard_minimized_background_inset"> <ImageView android:src="@drawable/ic_content_paste" diff --git a/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml new file mode 100644 index 000000000000..ed905885a76f --- /dev/null +++ b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml @@ -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 + --> + +<!-- android:paddingHorizontal is set dynamically in SmartReplyView. --> +<Button xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/Widget.Material.Button" + android:stateListAnimator="@null" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="0dp" + android:minHeight="@dimen/notification_2025_smart_reply_button_min_height" + android:paddingVertical="@dimen/smart_reply_button_padding_vertical" + android:background="@drawable/notification_2025_smart_reply_button_background" + android:gravity="center" + android:fontFamily="google-sans-flex" + android:textSize="@dimen/smart_reply_button_font_size" + android:textColor="@color/smart_reply_button_text" + android:paddingStart="@dimen/smart_reply_button_action_padding_left" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" + android:drawablePadding="@dimen/smart_action_button_icon_padding" + android:textStyle="normal" + android:ellipsize="none"/> diff --git a/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml new file mode 100644 index 000000000000..4f543e5099bf --- /dev/null +++ b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<!-- android:paddingHorizontal is set dynamically in SmartReplyView. --> +<Button xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/Widget.Material.Button" + android:stateListAnimator="@null" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="0dp" + android:minHeight="@dimen/notification_2025_smart_reply_button_min_height" + android:paddingVertical="@dimen/smart_reply_button_padding_vertical" + android:background="@drawable/notification_2025_smart_reply_button_background" + android:gravity="center" + android:fontFamily="google-sans-flex" + android:textSize="@dimen/smart_reply_button_font_size" + android:textColor="@color/smart_reply_button_text" + android:paddingStart="@dimen/smart_reply_button_padding_horizontal" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" + android:textStyle="normal" + android:ellipsize="none"/> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d547bd0d05f3..d0ae307b6919 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1157,6 +1157,8 @@ <dimen name="smart_action_button_icon_size">18dp</dimen> <dimen name="smart_action_button_icon_padding">8dp</dimen> <dimen name="smart_action_button_outline_stroke_width">2dp</dimen> + <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen> + <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen> <!-- Magic Action params. --> <!-- Corner radius = half of min_height to create rounded sides. --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c06c17a0844f..8c1fd65d96d4 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2357,8 +2357,8 @@ <string name="system_multitasking_lhs">Use split screen with app on the left</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> <string name="system_multitasking_full_screen">Use full screen</string> - <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] --> - <string name="system_multitasking_desktop_view">Use desktop view</string> + <!-- User visible title for the keyboard shortcut that switches to desktop windowing [CHAR LIMIT=70] --> + <string name="system_multitasking_desktop_view">Use desktop windowing</string> <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> diff --git a/packages/SystemUI/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/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt index 083191c8ecde..a56710ee3772 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt @@ -32,9 +32,6 @@ class FakePerDisplayRepository<T> : PerDisplayRepository<T> { return instances[displayId] } - override val displayIds: Set<Int> - get() = instances.keys - override val debugName: String get() = "FakePerDisplayRepository" } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt index d27e33e53dbb..d1d013542fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -86,9 +86,6 @@ interface PerDisplayRepository<T> { /** Gets the cached instance or create a new one for a given display. */ operator fun get(displayId: Int): T? - /** List of display ids for which this repository has an instance. */ - val displayIds: Set<Int> - /** Debug name for this repository, mainly for tracing and logging. */ val debugName: String } @@ -122,9 +119,6 @@ constructor( backgroundApplicationScope.launch("$debugName#start") { start() } } - override val displayIds: Set<Int> - get() = perDisplayInstances.keys - private suspend fun start() { dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this) displayRepository.displayIds.collectLatest { displayIds -> @@ -199,8 +193,6 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( private val lazyDefaultDisplayInstance by lazy { instanceProvider.createInstance(Display.DEFAULT_DISPLAY) } - override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) - override fun get(displayId: Int): T? = lazyDefaultDisplayInstance } @@ -214,7 +206,5 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( */ class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) : PerDisplayRepository<T> { - override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) - override fun get(displayId: Int): T? = instance } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt index 741b149f29da..92b9da6790d1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt @@ -31,6 +31,37 @@ import com.android.systemui.plugins.clocks.ClockPreviewConfig object KeyguardPreviewSmartspaceViewBinder { @JvmStatic + fun bind(parentView: View, viewModel: KeyguardPreviewSmartspaceViewModel) { + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + val largeDateView = + parentView.findViewById<View>( + com.android.systemui.shared.R.id.date_smartspace_view_large + ) + val smallDateView = + parentView.findViewById<View>(com.android.systemui.shared.R.id.date_smartspace_view) + parentView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch("$TAG#viewModel.selectedClockSize") { + viewModel.previewingClockSize.collect { + when (it) { + ClockSizeSetting.DYNAMIC -> { + smallDateView?.visibility = View.GONE + largeDateView?.visibility = View.VISIBLE + } + + ClockSizeSetting.SMALL -> { + smallDateView?.visibility = View.VISIBLE + largeDateView?.visibility = View.GONE + } + } + } + } + } + } + } + } + + @JvmStatic fun bind( smartspace: View, viewModel: KeyguardPreviewSmartspaceViewModel, @@ -44,6 +75,7 @@ object KeyguardPreviewSmartspaceViewBinder { when (it) { ClockSizeSetting.DYNAMIC -> viewModel.getLargeClockSmartspaceTopPadding(clockPreviewConfig) + ClockSizeSetting.SMALL -> viewModel.getSmallClockSmartspaceTopPadding(clockPreviewConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 242926b3e1d1..d749e3c11378 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -68,6 +68,7 @@ import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockPreviewConfig +import com.android.systemui.plugins.clocks.ContextExt.getId import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.res.R @@ -126,6 +127,7 @@ constructor( private val displayId = bundle.getInt(KEY_DISPLAY_ID, DEFAULT_DISPLAY) private val display: Display? = displayManager.getDisplay(displayId) + /** * Returns a key that should make the KeyguardPreviewRenderer unique and if two of them have the * same key they will be treated as the same KeyguardPreviewRenderer. Primary this is used to @@ -144,6 +146,8 @@ constructor( get() = checkNotNull(host.surfacePackage) private var smartSpaceView: View? = null + private var largeDateView: View? = null + private var smallDateView: View? = null private val disposables = DisposableHandles() private var isDestroyed = false @@ -181,7 +185,7 @@ constructor( ContextThemeWrapper(context.createDisplayContext(it), context.getTheme()) } ?: context - val rootView = FrameLayout(previewContext) + val rootView = ConstraintLayout(previewContext) setupKeyguardRootView(previewContext, rootView) @@ -252,6 +256,24 @@ constructor( fun onClockSizeSelected(clockSize: ClockSizeSetting) { smartspaceViewModel.setOverrideClockSize(clockSize) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + when (clockSize) { + ClockSizeSetting.DYNAMIC -> { + largeDateView?.post { + smallDateView?.visibility = View.GONE + largeDateView?.visibility = View.VISIBLE + } + } + + ClockSizeSetting.SMALL -> { + largeDateView?.post { + smallDateView?.visibility = View.VISIBLE + largeDateView?.visibility = View.GONE + } + } + } + smartSpaceView?.post { smartSpaceView?.visibility = View.GONE } + } } fun destroy() { @@ -280,7 +302,7 @@ constructor( * * The end padding is as follows: Below clock padding end */ - private fun setUpSmartspace(previewContext: Context, parentView: ViewGroup) { + private fun setUpSmartspace(previewContext: Context, parentView: ConstraintLayout) { if ( !lockscreenSmartspaceController.isEnabled || !lockscreenSmartspaceController.isDateWeatherDecoupled @@ -292,40 +314,90 @@ constructor( parentView.removeView(smartSpaceView) } - smartSpaceView = - lockscreenSmartspaceController.buildAndConnectDateView( - parent = parentView, - isLargeClock = false, - ) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + val cs = ConstraintSet() + cs.clone(parentView) + cs.apply { + val largeClockViewId = previewContext.getId("lockscreen_clock_view_large") + val smallClockViewId = previewContext.getId("lockscreen_clock_view") + largeDateView = + lockscreenSmartspaceController + .buildAndConnectDateView(parentView, true) + ?.also { view -> + constrainWidth(view.id, ConstraintSet.WRAP_CONTENT) + constrainHeight(view.id, ConstraintSet.WRAP_CONTENT) + connect(view.id, START, largeClockViewId, START) + connect(view.id, ConstraintSet.END, largeClockViewId, ConstraintSet.END) + connect( + view.id, + TOP, + largeClockViewId, + ConstraintSet.BOTTOM, + smartspaceViewModel.getDateWeatherEndPadding(previewContext), + ) + } + smallDateView = + lockscreenSmartspaceController + .buildAndConnectDateView(parentView, false) + ?.also { view -> + constrainWidth(view.id, ConstraintSet.WRAP_CONTENT) + constrainHeight(view.id, ConstraintSet.WRAP_CONTENT) + connect( + view.id, + START, + smallClockViewId, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + R.dimen.smartspace_padding_horizontal + ), + ) + connect(view.id, TOP, smallClockViewId, TOP) + connect( + view.id, + ConstraintSet.BOTTOM, + smallClockViewId, + ConstraintSet.BOTTOM, + ) + } + parentView.addView(largeDateView) + parentView.addView(smallDateView) + } + cs.applyTo(parentView) + } else { + smartSpaceView = + lockscreenSmartspaceController.buildAndConnectDateView( + parent = parentView, + isLargeClock = false, + ) - val topPadding: Int = - smartspaceViewModel.getLargeClockSmartspaceTopPadding( - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, + val topPadding: Int = + smartspaceViewModel.getLargeClockSmartspaceTopPadding( + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + ) ) - ) - val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext) - val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext) - - smartSpaceView?.let { - it.setPaddingRelative(startPadding, topPadding, endPadding, 0) - it.isClickable = false - it.isInvisible = true - parentView.addView( - it, - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - ), - ) + val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext) + val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext) + + smartSpaceView?.let { + it.setPaddingRelative(startPadding, topPadding, endPadding, 0) + it.isClickable = false + it.isInvisible = true + parentView.addView( + it, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + } + smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } - - smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } - private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { + private fun setupKeyguardRootView(previewContext: Context, rootView: ConstraintLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) rootView.addView( keyguardRootView, @@ -341,6 +413,13 @@ constructor( if (!shouldHideClock) { setUpClock(previewContext, rootView) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + setUpSmartspace(previewContext, keyguardRootView) + KeyguardPreviewSmartspaceViewBinder.bind( + keyguardRootView, + smartspaceViewModel, + ) + } KeyguardPreviewClockViewBinder.bind( keyguardRootView, clockViewModel, @@ -354,19 +433,22 @@ constructor( ) } - setUpSmartspace(previewContext, rootView) - - smartSpaceView?.let { - KeyguardPreviewSmartspaceViewBinder.bind( - it, - smartspaceViewModel, - clockPreviewConfig = - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, - ), - ) + if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + setUpSmartspace(previewContext, keyguardRootView) + smartSpaceView?.let { + KeyguardPreviewSmartspaceViewBinder.bind( + it, + smartspaceViewModel, + clockPreviewConfig = + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + lockId = null, + udfpsTop = null, + ), + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index ef57978584b9..8920c86282da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -747,22 +747,24 @@ constructor( val BrightnessSlider = @Composable { AlwaysDarkMode { - BrightnessSliderContainer( - viewModel = containerViewModel.brightnessSliderViewModel, - containerColors = - ContainerColors( - Color.Transparent, - ContainerColors.defaultContainerColor, - ), - modifier = - Modifier.systemGestureExclusionInShade( - enabled = { - layoutState.transitionState is - TransitionState.Idle - } - ) - .fillMaxWidth(), - ) + Box( + Modifier.systemGestureExclusionInShade( + enabled = { + layoutState.transitionState is TransitionState.Idle + } + ) + ) { + BrightnessSliderContainer( + viewModel = + containerViewModel.brightnessSliderViewModel, + containerColors = + ContainerColors( + Color.Transparent, + ContainerColors.defaultContainerColor, + ), + modifier = Modifier.fillMaxWidth(), + ) + } } } val TileGrid = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 153238fc91c9..a66b51f6fe50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -59,12 +59,14 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset @@ -149,16 +151,14 @@ fun InteractiveTileContainer( onClick = onClick, ) ) { + val size = with(LocalDensity.current) { BadgeIconSize.toDp() } Icon( Icons.Default.Remove, contentDescription = null, modifier = - Modifier.size( - width = { decorationSize.width.roundToInt() }, - height = { decorationSize.height.roundToInt() }, - ) - .align(Alignment.Center) - .graphicsLayer { this.alpha = badgeIconAlpha }, + Modifier.size(size).align(Alignment.Center).graphicsLayer { + this.alpha = badgeIconAlpha + }, ) } } @@ -219,12 +219,13 @@ fun StaticTileBadge( } ) { val secondaryColor = MaterialTheme.colorScheme.secondary + val size = with(LocalDensity.current) { BadgeIconSize.toDp() } Icon( icon, contentDescription = contentDescription, modifier = - Modifier.size(BadgeSize).align(Alignment.Center).drawBehind { - drawCircle(secondaryColor) + Modifier.size(size).align(Alignment.Center).drawBehind { + drawCircle(secondaryColor, radius = BadgeSize.toPx() / 2) }, ) } @@ -338,6 +339,7 @@ private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset private object SelectionDefaults { val SelectedBorderWidth = 2.dp val BadgeSize = 24.dp + val BadgeIconSize = 16.sp val BadgeXOffset = -4.dp val BadgeYOffset = 4.dp val ResizingPillWidth = 8.dp diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 20b44d73e097..5609326362fc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.animation.scene.OverlayKey import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -86,6 +87,22 @@ constructor( (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = batteryMeterViewControllerFactory::create + val showClock: Boolean by + hydrator.hydratedStateOf( + traceName = "showClock", + initialValue = + shouldShowClock( + isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, + overlays = sceneInteractor.currentOverlays.value, + ), + source = + combine( + shadeInteractor.isShadeLayoutWide, + sceneInteractor.currentOverlays, + ::shouldShowClock, + ), + ) + val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( traceName = "notificationsChipHighlight", @@ -114,13 +131,6 @@ constructor( }, ) - val isShadeLayoutWide: Boolean by - hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier @@ -271,6 +281,11 @@ constructor( } } + private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean { + // Notifications shade on narrow layout renders its own clock. Hide the header clock. + return isShadeLayoutWide || Overlays.NotificationsShade !in overlays + } + private fun getFormatFromPattern(pattern: String?): DateFormat { val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) 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/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 41353b9921bd..4d68f2e6ef1b 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 @@ -63,17 +63,6 @@ public class BundleEntry extends PipelineEntry { @Nullable @Override - public NotifSection getSection() { - return null; - } - - @Override - public int getSectionIndex() { - return 0; - } - - @Nullable - @Override public PipelineEntry getParent() { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index 4a1b9568c714..04dc7d5ed3ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -31,8 +31,8 @@ data class ListAttachState private constructor( var parent: PipelineEntry?, /** - * The section that this ListEntry was sorted into. If the child of the group, this will be the - * parent's section. Null if not attached to the list. + * The section that this PipelineEntry was sorted into. If the child of the group, this will be + * the parent's section. Null if not attached to the list. */ var section: NotifSection?, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java index 697d0a06cf9d..caa7abb1aa7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java @@ -66,10 +66,6 @@ public abstract class ListEntry extends PipelineEntry { return mPreviousAttachState.getParent(); } - public int getSectionIndex() { - return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1; - } - /** * Stores the current attach state into {@link #getPreviousAttachState()}} and then starts a * fresh attach state (all entries will be null/default-initialized). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt index f662a040fae6..0fc0e9c5eab8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter @@ -169,6 +170,14 @@ class NotifPipeline @Inject constructor( } /** + * NotifBundler that is used to determine whether a notification should be bundled according to + * classification. + */ + fun setNotifBundler(bundler: NotifBundler) { + mShadeListBuilder.setBundler(bundler) + } + + /** * StabilityManager that is used to determine whether to suppress group and section changes. * This should only be set once. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java index 84de77bac352..872cd68e1b21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java @@ -70,7 +70,9 @@ public abstract class PipelineEntry { /** * @return Index of section assigned to this entry. */ - public abstract int getSectionIndex(); + public int getSectionIndex() { + return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1; + } /** * @return Parent PipelineEntry 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 bb84ab8f421a..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 @@ -48,6 +48,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.NotificationInteractionTracker; import com.android.systemui.statusbar.notification.NotifPipelineFlags; +import com.android.systemui.statusbar.notification.collection.coordinator.BundleCoordinator; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; @@ -58,8 +59,10 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.SemiSt import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifBundler; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; @@ -78,6 +81,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -122,7 +126,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final List<NotifComparator> mNotifComparators = new ArrayList<>(); private final List<NotifSection> mNotifSections = new ArrayList<>(); private NotifStabilityManager mNotifStabilityManager; - + private NotifBundler mNotifBundler; + private Map<String, BundleEntry> mIdToBundleEntry = new HashMap<>(); private final NamedListenerSet<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners = new NamedListenerSet<>(); private final NamedListenerSet<OnBeforeSortListener> @@ -273,6 +278,21 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { } } + void setBundler(NotifBundler bundler) { + Assert.isMainThread(); + mPipelineState.requireState(STATE_IDLE); + + mNotifBundler = bundler; + if (mNotifBundler == null) { + throw new IllegalStateException(TAG + ".setBundler: null"); + } + + mIdToBundleEntry.clear(); + for (String id: mNotifBundler.getBundleIds()) { + mIdToBundleEntry.put(id, new BundleEntry(id)); + } + } + void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) { Assert.isMainThread(); mPipelineState.requireState(STATE_IDLE); @@ -297,6 +317,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return mNotifStabilityManager; } + @NonNull + private NotifBundler getNotifBundler() { + if (mNotifBundler == null) { + return DefaultNotifBundler.INSTANCE; + } + return mNotifBundler; + } + void setComparators(List<NotifComparator> comparators) { Assert.isMainThread(); mPipelineState.requireState(STATE_IDLE); @@ -651,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 @@ -659,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/BundleCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt index e6d5f4120a20..8833ff1ce20c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt @@ -20,15 +20,19 @@ import android.app.NotificationChannel.NEWS_ID import android.app.NotificationChannel.PROMOTIONS_ID import android.app.NotificationChannel.RECS_ID import android.app.NotificationChannel.SOCIAL_MEDIA_ID -import com.android.systemui.statusbar.notification.collection.PipelineEntry +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.NewsHeader import com.android.systemui.statusbar.notification.dagger.PromoHeader import com.android.systemui.statusbar.notification.dagger.RecsHeader import com.android.systemui.statusbar.notification.dagger.SocialHeader +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_NEWS import com.android.systemui.statusbar.notification.stack.BUCKET_PROMO import com.android.systemui.statusbar.notification.stack.BUCKET_RECS @@ -90,6 +94,20 @@ class BundleCoordinator @Inject constructor( } } + val bundler = + object : NotifBundler("NotifBundler") { + + // Use list instead of set to keep fixed order + override val bundleIds: List<String> = SYSTEM_RESERVED_IDS + + override fun getBundleIdOrNull(entry: NotificationEntry?): String? { + return entry?.representativeEntry?.channel?.id?.takeIf { it in this.bundleIds } + } + } + override fun attach(pipeline: NotifPipeline) { + if (NotificationBundleUi.isEnabled) { + pipeline.setNotifBundler(bundler) + } } } 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..5d981b34edd3 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 @@ -423,6 +424,7 @@ constructor( map[child.key] = GroupLocation.Child } } + is BundleEntry -> map[topLevelEntry.key] = GroupLocation.Bundle else -> error("unhandled type $topLevelEntry") } } @@ -950,6 +952,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/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt index 56deb18df9ab..d542e67e665a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -26,6 +26,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -193,6 +194,7 @@ constructor( when (it) { is NotificationEntry -> listOfNotNull(it) is GroupEntry -> it.children + is BundleEntry -> emptyList() else -> error("unhandled type of $it") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index df0cde51e8ba..818ef6b335c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection @@ -113,11 +114,12 @@ constructor( mCoordinators.add(remoteInputCoordinator) mCoordinators.add(dismissibilityCoordinator) mCoordinators.add(automaticPromotionCoordinator) - + if (NotificationBundleUi.isEnabled) { + mCoordinators.add(bundleCoordinator) + } if (NotificationsLiveDataStoreRefactor.isEnabled) { mCoordinators.add(statsLoggerCoordinator) } - // Manually add Ordered Sections if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing @@ -135,7 +137,7 @@ constructor( mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent } mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting - if (NotificationClassificationFlag.isEnabled) { + if (NotificationClassificationFlag.isEnabled && !NotificationBundleUi.isEnabled) { mOrderedSections.add(bundleCoordinator.newsSectioner) mOrderedSections.add(bundleCoordinator.socialSectioner) mOrderedSections.add(bundleCoordinator.recsSectioner) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 20c6736b74c8..b54f21b23bba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -34,6 +34,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.statusbar.notification.collection.BundleEntry; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -54,6 +55,7 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -306,7 +308,9 @@ public class PreparationCoordinator implements Coordinator { private void inflateAllRequiredViews(List<PipelineEntry> entries) { for (int i = 0, size = entries.size(); i < size; i++) { PipelineEntry entry = entries.get(i); - if (entry instanceof GroupEntry) { + if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry) { + // TODO(b/399738511) Inflate bundle views. + } else if (entry instanceof GroupEntry) { GroupEntry groupEntry = (GroupEntry) entry; inflateRequiredGroupViews(groupEntry); } else { 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 d1063d95a305..3fad8f0510a1 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 @@ -132,7 +132,12 @@ public class RankingCoordinator implements Coordinator { public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) { mHasSilentEntries = false; for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { + NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry(); + if (notifEntry == null) { + // TODO(b/395698521) Handle BundleEntry + continue; + } + if (notifEntry.getSbn().isClearable()) { mHasSilentEntries = true; break; } @@ -147,6 +152,7 @@ public class RankingCoordinator implements Coordinator { @Override public boolean isInSection(PipelineEntry entry) { return !mHighPriorityProvider.isHighPriority(entry) + && entry.getRepresentativeEntry() != null && entry.getRepresentativeEntry().isAmbient(); } @@ -161,7 +167,12 @@ public class RankingCoordinator implements Coordinator { public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) { mHasMinimizedEntries = false; for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { + NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry(); + if (notifEntry == null) { + // TODO(b/395698521) Handle BundleEntry + continue; + } + if (notifEntry.getSbn().isClearable()) { mHasMinimizedEntries = true; break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt new file mode 100644 index 000000000000..14a9113f7eb4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.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.statusbar.notification.collection.listbuilder.pluggable + +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Pluggable for bundling notifications according to classification. */ +abstract class NotifBundler protected constructor(name: String?) : Pluggable<NotifBundler?>(name) { + abstract val bundleIds: List<String> + + abstract fun getBundleIdOrNull(entry: NotificationEntry?): String? +} + +/** The default, no-op instance of NotifBundler which does not bundle anything. */ +object DefaultNotifBundler : NotifBundler("DefaultNotifBundler") { + override val bundleIds: List<String> + get() = listOf() + + override fun getBundleIdOrNull(entry: NotificationEntry?): String? { + return null + } +} 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 c877beeda152..b481455699ef 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 @@ -68,6 +68,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewStub; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.Chronometer; @@ -114,7 +115,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter; import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -123,6 +123,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; @@ -185,7 +186,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowSnooze = false; private boolean mIsFaded; - private boolean mIsPromotedOngoing = false; private boolean mHasStatusBarChipDuringHeadsUpAnimation = false; @Nullable @@ -871,7 +871,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void updateLimitsForView(NotificationContentView layout) { final int maxExpandedHeight; - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing; } else { maxExpandedHeight = mMaxExpandedHeight; @@ -1350,7 +1350,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return getMaxExpandHeight(); } if (mExpandedWhenPinned) { @@ -2939,7 +2939,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return false; } return mEnableNonGroupedNotificationExpand && mExpandable; @@ -2950,17 +2950,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.updateExpandButtons(isExpandable()); } - /** - * Set this notification to be promoted ongoing - */ - public void setPromotedOngoing(boolean promotedOngoing) { - if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) { - return; - } - - mIsPromotedOngoing = promotedOngoing; - setExpandable(!mIsPromotedOngoing); - } /** * Sets whether the status bar is showing a chip corresponding to this notification. @@ -3061,7 +3050,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setUserLocked(boolean userLocked) { - if (isPromotedOngoing()) return; + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return; mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); @@ -3247,7 +3236,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isPromotedOngoing() { - return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing; + if (!PromotedNotificationUi.isEnabled()) { + return false; + } + + final NotificationEntry entry = mEntry; + if (entry == null) { + return false; + } + + return entry.isPromotedOngoing(); } private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) { @@ -3309,7 +3307,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return isPromotedNotificationExpanded(allowOnKeyguard); } @@ -4038,10 +4036,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void notifyAccessibilityContentExpansionChanged() { - AccessibilityEvent event = AccessibilityEvent.obtain(TYPE_WINDOW_CONTENT_CHANGED); - onPopulateAccessibilityEvent(event); - event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED); - sendAccessibilityEventUnchecked(event); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(TYPE_WINDOW_CONTENT_CHANGED); + event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED); + sendAccessibilityEventUnchecked(event); + } } public void setOnExpansionChangedListener(@Nullable OnExpansionChangedListener listener) { @@ -4396,9 +4396,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView + (!shouldShowPublic() && mIsSummaryWithChildren)); pw.print(", mShowNoBackground: " + mShowNoBackground); pw.print(", clipBounds: " + getClipBounds()); - if (PromotedNotificationUiForceExpanded.isEnabled()) { - pw.print(", isPromotedOngoing: " + isPromotedOngoing()); - } + pw.print(", isPromotedOngoing: " + isPromotedOngoing()); if (notificationRowAccessibilityExpanded()) { pw.print(", isShowingExpanded: " + isShowingExpanded()); pw.print(", isAccessibilityExpandable: " + isAccessibilityExpandable()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 51569c1596ef..3ffc052b5acc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -59,7 +59,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; @@ -1006,10 +1005,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry.setPromotedNotificationContentModel(result.mPromotedContent); } - if (PromotedNotificationUiForceExpanded.isEnabled()) { - row.setPromotedOngoing(entry.isOngoingPromoted()); - } - boolean setRepliesAndActions = true; if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { if (result.inflatedContentView != null) { 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 482b315aa14d..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 @@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED @@ -1293,6 +1292,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view synchronously", @@ -1318,6 +1318,7 @@ constructor( runningInflations, InflationException(invalidReason), row, + entry, callback, logger, "applied invalid view", @@ -1377,6 +1378,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view", @@ -1480,6 +1482,7 @@ constructor( runningInflations: HashMap<Int, CancellationSignal>, e: Exception, notification: ExpandableNotificationRow?, + entry: NotificationEntry, callback: InflationCallback?, logger: NotificationRowContentBinderLogger, logContext: String, @@ -1487,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) } /** @@ -1520,10 +1523,6 @@ constructor( entry.promotedNotificationContentModel = result.promotedContent } - if (PromotedNotificationUiForceExpanded.isEnabled) { - row.setPromotedOngoing(entry.isOngoingPromoted()) - } - result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } setContentViewsFromRemoteViews( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index d7dd7ec08583..53728c7da62d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.shared import android.app.PendingIntent import android.graphics.drawable.Icon +import android.util.Log import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.stack.PriorityBucket @@ -85,6 +86,15 @@ data class ActiveNotificationModel( */ val promotedContent: PromotedNotificationContentModel?, ) : ActiveNotificationEntryModel() { + init { + if (!PromotedNotificationContentModel.featureFlagEnabled()) { + if (promotedContent != null) { + // TODO(b/401018545): convert to Log.wtf and fix tests (see: ag/32114199) + Log.e(TAG, "passing non-null promoted content without feature flag enabled") + } + } + } + companion object { private const val TAG = "ActiveNotificationEntryModel" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index d3af1e5b65fe..6d959be1c5f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy import android.app.ActivityOptions +import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY import android.app.PendingIntent @@ -53,7 +54,6 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.logging.NotificationLogger -import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions @@ -397,16 +397,21 @@ constructor( delayOnClickListener: Boolean, packageContext: Context, ): Button { - val isMagicAction = Flags.notificationMagicActionsTreatment() && + val isMagicAction = + Flags.notificationMagicActionsTreatment() && smartActions.fromAssistant && action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) - val layoutRes = if (isMagicAction) { - R.layout.magic_action_button - } else { - R.layout.smart_action_button - } - return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) - as Button) + val layoutRes = + if (isMagicAction) { + R.layout.magic_action_button + } else { + if (notificationsRedesignTemplates()) { + R.layout.notification_2025_smart_action_button + } else { + R.layout.smart_action_button + } + } + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { text = action.title @@ -435,7 +440,6 @@ constructor( // Mark this as an Action button (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION } - } private fun onSmartActionClick( @@ -499,9 +503,11 @@ constructor( replyIndex: Int, choice: CharSequence, delayOnClickListener: Boolean, - ): Button = - (LayoutInflater.from(parent.context).inflate(R.layout.smart_reply_button, parent, false) - as Button) + ): Button { + val layoutRes = + if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button + else R.layout.smart_reply_button + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { text = choice val onClickListener = @@ -531,6 +537,7 @@ constructor( // Mark this as a Reply button (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY } + } private fun onSmartReplyClick( entry: NotificationEntry, 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 cd2ea7d25699..4315c0f638ac 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 @@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; @@ -936,11 +937,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setSensitive(/* sensitive= */true, /* hideSensitive= */false); row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true); @@ -949,11 +952,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(false); // THEN @@ -961,11 +966,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); // THEN @@ -973,12 +980,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setIgnoreLockscreenConstraints(true); @@ -987,12 +996,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(true); @@ -1001,12 +1012,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(false); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt index 807bc82cbf4e..c9e25c31faa0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt @@ -34,7 +34,7 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository { _isPowerButtonDown.value = isDown } - fun setPowerButtonBeingLongPressed(isLongPressed: Boolean) { + fun setPowerButtonLongPressed(isLongPressed: Boolean) { _isPowerButtonLongPressed.value = isLongPressed } } 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/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt new file mode 100644 index 000000000000..2d2a81586515 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.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.topwindoweffects.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeSqueezeEffectRepository : SqueezeEffectRepository { + override val isSqueezeEffectEnabled = MutableStateFlow(false) +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt new file mode 100644 index 000000000000..aa8bb6b1e104 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeSqueezeEffectRepository by Kosmos.Fixture { FakeSqueezeEffectRepository() }
\ No newline at end of file diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java index 3e2c4051b792..f25ae6a34242 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java @@ -128,28 +128,6 @@ public class RavenwoodUtils { runOnHandlerSync(getMainHandler(), r); } - public static class MockitoHelper { - private MockitoHelper() { - } - - /** - * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different - * method on. (Maybe we should do it in Ravenizer.) - */ - public static void verifyZeroInteractions(Object... mocks) { - if (RavenwoodRule.isOnRavenwood()) { - // Mockito 4 or later - reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class) - .callStatic(new Object[]{mocks}); - } else { - // Mockito 2 - reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class) - .callStatic(new Object[]{mocks}); - } - } - } - - /** * Wrap the given {@link Supplier} to become memoized. * 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 739ea0df87ab..cc93d0887d89 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -248,7 +248,11 @@ public class AutoclickController extends BaseEventStreamTransformation { private boolean isPaused() { return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused() - && !mAutoclickTypePanel.isHovered(); + && !isHovered(); + } + + private boolean isHovered() { + return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isHovered(); } private void cancelPendingClick() { @@ -495,6 +499,8 @@ public class AutoclickController extends BaseEventStreamTransformation { private int mEventPolicyFlags; /** Current meta state. This value will be used as meta state for click event sequence. */ private int mMetaState; + /** Last observed panel hovered state when click was scheduled. */ + private boolean mHoveredState; /** * The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null. @@ -648,6 +654,7 @@ public class AutoclickController extends BaseEventStreamTransformation { } mLastMotionEvent = MotionEvent.obtain(event); mEventPolicyFlags = policyFlags; + mHoveredState = isHovered(); if (useAsAnchor) { final int pointerIndex = mLastMotionEvent.getActionIndex(); @@ -729,14 +736,18 @@ public class AutoclickController extends BaseEventStreamTransformation { final long now = SystemClock.uptimeMillis(); - // TODO(b/395094903): always triggers left-click when the cursor hovers over the - // autoclick type panel, to always allow users to change a different click type. - // Otherwise, if one chooses the right-click, this user won't be able to rely on - // autoclick to select other click types. - final int actionButton = - mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK - ? BUTTON_SECONDARY - : BUTTON_PRIMARY; + int actionButton; + if (mHoveredState) { + // Always triggers left-click when the cursor hovers over the autoclick type + // panel, to always allow users to change a different click type. Otherwise, if + // one chooses the right-click, this user won't be able to rely on autoclick to + // select other click types. + actionButton = BUTTON_PRIMARY; + } else { + actionButton = mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK + ? BUTTON_SECONDARY + : BUTTON_PRIMARY; + } MotionEvent downEvent = MotionEvent.obtain( diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java index ebb1194c7c4a..b173f76e5f6f 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java @@ -25,6 +25,7 @@ import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_A import android.annotation.UserIdInt; import android.app.ApplicationThreadConstants; import android.app.IBackupAgent; +import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupTransport; import android.app.backup.FullBackupDataOutput; import android.content.pm.ApplicationInfo; @@ -268,6 +269,12 @@ public class FullBackupEngine { mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent); } catch (IOException e) { Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage()); + // This is likely due to the app process dying. + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN, + mPkg, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + /* extras= */ null); result = BackupTransport.AGENT_ERROR; } finally { try { diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index f677c9dbf4d0..48d21c3a222f 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -294,6 +294,14 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // SinglePackageBackupPreflight. if (cancellationReason == CancellationReason.TIMEOUT) { Slog.wtf(TAG, "This task cannot time out"); + return; + } + + // We don't cancel the entire operation if a single agent is disconnected unexpectedly. + // SinglePackageBackupRunner and SinglePackageBackupPreflight will receive the same + // callback and fail gracefully. The operation should then continue to the next package. + if (cancellationReason == CancellationReason.AGENT_DISCONNECTED) { + return; } if (mCancelled) { diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 1263146fe405..20f103cdfab4 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -589,6 +589,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { monitoringExtras); Slog.e(TAG, "Failure getting next package name"); EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupTransport.TRANSPORT_ERROR; nextState = UnifiedRestoreState.FINAL; return; } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) { diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java index fad59d23a6dc..855c72acd7ca 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java @@ -389,6 +389,8 @@ public class BackupManagerMonitorDumpsysUtils { "Agent failure during restore"; case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT -> "Failed to read data from Transport"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN -> + "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN"; default -> "Unknown log event ID: " + code; }; return id; 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/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b0b34d0ab9c4..76ba0054583b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16190,14 +16190,16 @@ public class ActivityManagerService extends IActivityManager.Stub return mUserController.switchUser(targetUserId); } + @Nullable @Override - public String getSwitchingFromUserMessage() { - return mUserController.getSwitchingFromSystemUserMessage(); + public String getSwitchingFromUserMessage(@UserIdInt int userId) { + return mUserController.getSwitchingFromUserMessage(userId); } + @Nullable @Override - public String getSwitchingToUserMessage() { - return mUserController.getSwitchingToSystemUserMessage(); + public String getSwitchingToUserMessage(@UserIdInt int userId) { + return mUserController.getSwitchingToUserMessage(userId); } @Override @@ -16938,13 +16940,13 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) { - mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage); + public void setSwitchingFromUserMessage(@UserIdInt int userId, @Nullable String message) { + mUserController.setSwitchingFromUserMessage(userId, message); } @Override - public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) { - mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage); + public void setSwitchingToUserMessage(@UserIdInt int userId, @Nullable String message) { + mUserController.setSwitchingToUserMessage(userId, message); } @Override diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 18f3500b2d56..40a9bbec3598 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -340,16 +340,16 @@ class UserController implements Handler.Callback { private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks; /** - * Messages for switching from {@link android.os.UserHandle#SYSTEM}. + * Message shown when switching from a user. */ @GuardedBy("mLock") - private String mSwitchingFromSystemUserMessage; + private final SparseArray<String> mSwitchingFromUserMessage = new SparseArray<>(); /** - * Messages for switching to {@link android.os.UserHandle#SYSTEM}. + * Message shown when switching to a user. */ @GuardedBy("mLock") - private String mSwitchingToSystemUserMessage; + private final SparseArray<String> mSwitchingToUserMessage = new SparseArray<>(); /** * Callbacks that are still active after {@link #getUserSwitchTimeoutMs} @@ -2271,8 +2271,8 @@ class UserController implements Handler.Callback { private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) { // The dialog will show and then initiate the user switch by calling startUserInForeground mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second, - getSwitchingFromSystemUserMessageUnchecked(), - getSwitchingToSystemUserMessageUnchecked(), + getSwitchingFromUserMessageUnchecked(fromToUserPair.first.id), + getSwitchingToUserMessageUnchecked(fromToUserPair.second.id), /* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id)); } @@ -3388,41 +3388,45 @@ class UserController implements Handler.Callback { return mLockPatternUtils.isLockScreenDisabled(userId); } - void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) { + void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message) { synchronized (mLock) { - mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage; + mSwitchingFromUserMessage.put(user, message); } } - void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) { + void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message) { synchronized (mLock) { - mSwitchingToSystemUserMessage = switchingToSystemUserMessage; + mSwitchingToUserMessage.put(user, message); } } // Called by AMS, must check permission - String getSwitchingFromSystemUserMessage() { - checkHasManageUsersPermission("getSwitchingFromSystemUserMessage()"); + @Nullable + String getSwitchingFromUserMessage(@UserIdInt int userId) { + checkHasManageUsersPermission("getSwitchingFromUserMessage()"); - return getSwitchingFromSystemUserMessageUnchecked(); + return getSwitchingFromUserMessageUnchecked(userId); } // Called by AMS, must check permission - String getSwitchingToSystemUserMessage() { - checkHasManageUsersPermission("getSwitchingToSystemUserMessage()"); + @Nullable + String getSwitchingToUserMessage(@UserIdInt int userId) { + checkHasManageUsersPermission("getSwitchingToUserMessage()"); - return getSwitchingToSystemUserMessageUnchecked(); + return getSwitchingToUserMessageUnchecked(userId); } - private String getSwitchingFromSystemUserMessageUnchecked() { + @Nullable + private String getSwitchingFromUserMessageUnchecked(@UserIdInt int userId) { synchronized (mLock) { - return mSwitchingFromSystemUserMessage; + return mSwitchingFromUserMessage.get(userId); } } - private String getSwitchingToSystemUserMessageUnchecked() { + @Nullable + private String getSwitchingToUserMessageUnchecked(@UserIdInt int userId) { synchronized (mLock) { - return mSwitchingToSystemUserMessage; + return mSwitchingToUserMessage.get(userId); } } @@ -3518,12 +3522,8 @@ class UserController implements Handler.Callback { + mIsBroadcastSentForSystemUserStarted); pw.println(" mIsBroadcastSentForSystemUserStarting:" + mIsBroadcastSentForSystemUserStarting); - if (mSwitchingFromSystemUserMessage != null) { - pw.println(" mSwitchingFromSystemUserMessage: " + mSwitchingFromSystemUserMessage); - } - if (mSwitchingToSystemUserMessage != null) { - pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage); - } + pw.println(" mSwitchingFromUserMessage:" + mSwitchingFromUserMessage); + pw.println(" mSwitchingToUserMessage:" + mSwitchingToUserMessage); pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime); } } @@ -4046,7 +4046,7 @@ class UserController implements Handler.Callback { } void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, - String switchingFromSystemUserMessage, String switchingToSystemUserMessage, + @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage, @NonNull Runnable onShown) { if (mService.mContext.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { @@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback { synchronized (mUserSwitchingDialogLock) { dismissUserSwitchingDialog(null); mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser, - mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage); + mHandler, switchingFromUserMessage, switchingToUserMessage); mUserSwitchingDialog.show(onShown); } } diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java index 223e0b79ec0b..f4e733a0c99f 100644 --- a/services/core/java/com/android/server/am/UserSwitchingDialog.java +++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java @@ -52,6 +52,7 @@ import com.android.internal.R; import com.android.internal.util.ObjectUtils; import com.android.internal.util.UserIcons; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -75,21 +76,23 @@ class UserSwitchingDialog extends Dialog { protected final UserInfo mOldUser; protected final UserInfo mNewUser; - private final String mSwitchingFromSystemUserMessage; - private final String mSwitchingToSystemUserMessage; + @Nullable + private final String mSwitchingFromUserMessage; + @Nullable + private final String mSwitchingToUserMessage; protected final Context mContext; private final int mTraceCookie; UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler, - String switchingFromSystemUserMessage, String switchingToSystemUserMessage) { + @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) { super(context, R.style.Theme_Material_NoActionBar_Fullscreen); mContext = context; mOldUser = oldUser; mNewUser = newUser; mHandler = handler; - mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage; - mSwitchingToSystemUserMessage = switchingToSystemUserMessage; + mSwitchingFromUserMessage = switchingFromUserMessage; + mSwitchingToUserMessage = switchingToUserMessage; mDisableAnimations = SystemProperties.getBoolean( "debug.usercontroller.disable_user_switching_dialog_animations", false); mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id; @@ -166,14 +169,14 @@ class UserSwitchingDialog extends Dialog { : R.string.demo_starting_message); } - final String message = - mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage - : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null; + if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) { + if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) { + return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage; + } + return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage); + } - return message != null ? message - // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null, - // fallback to system message. - : res.getString(R.string.user_switching_message, mNewUser.name); + return res.getString(R.string.user_switching_message, mNewUser.name); } @Override diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 8e8455ad5288..6e640d890fb8 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -17,7 +17,6 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE; -import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.app.WallpaperManager.getOrientation; import static android.app.WallpaperManager.getRotatedOrientation; @@ -85,20 +84,11 @@ public class WallpaperCropper { private final WallpaperDisplayHelper mWallpaperDisplayHelper; - /** - * Helpers exposed to the window manager part (WallpaperController) - */ - public interface WallpaperCropUtils { - - /** - * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)} - */ - Rect getCrop(Point displaySize, Point bitmapSize, - SparseArray<Rect> suggestedCrops, boolean rtl); - } + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { mWallpaperDisplayHelper = wallpaperDisplayHelper; + mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo(); } /** @@ -116,16 +106,16 @@ public class WallpaperCropper { * {@link #getAdjustedCrop}. * </ul> * - * @param displaySize The dimensions of the surface where we want to render the wallpaper - * @param bitmapSize The dimensions of the wallpaper bitmap - * @param rtl Whether the device is right-to-left - * @param suggestedCrops An optional list of user-defined crops for some orientations. - * If there is a suggested crop for + * @param displaySize The dimensions of the surface where we want to render the wallpaper + * @param defaultDisplayInfo The default display info + * @param bitmapSize The dimensions of the wallpaper bitmap + * @param rtl Whether the device is right-to-left + * @param suggestedCrops An optional list of user-defined crops for some orientations. * * @return A Rect indicating how to crop the bitmap for the current display. */ - public Rect getCrop(Point displaySize, Point bitmapSize, - SparseArray<Rect> suggestedCrops, boolean rtl) { + public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo, + Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) { int orientation = getOrientation(displaySize); @@ -135,23 +125,24 @@ public class WallpaperCropper { // The first exception is if the device is a foldable and we're on the folded screen. // In that case, show the center of what's on the unfolded screen. - int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation); if (unfoldedOrientation != ORIENTATION_UNKNOWN) { // Let the system know that we're showing the full image on the unfolded screen SparseArray<Rect> newSuggestedCrops = new SparseArray<>(); newSuggestedCrops.put(unfoldedOrientation, crop); // This will fall into "Case 4" of this function and center the folded screen - return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, newSuggestedCrops, + rtl); } // The second exception is if we're on tablet and we're on portrait mode. // In that case, center the wallpaper relatively to landscape and put some parallax. - boolean isTablet = mWallpaperDisplayHelper.isLargeScreen() - && !mWallpaperDisplayHelper.isFoldable(); + boolean isTablet = defaultDisplayInfo.isLargeScreen && !defaultDisplayInfo.isFoldable; if (isTablet && displaySize.x < displaySize.y) { Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x); // compute the crop on landscape (without parallax) - Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + Rect landscapeCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl); landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl); // compute the crop on portrait at the center of the landscape crop crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD); @@ -173,7 +164,8 @@ public class WallpaperCropper { if (testCrop == null || testCrop.left < 0 || testCrop.top < 0 || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) { Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize); - return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), + rtl); } } @@ -185,10 +177,9 @@ public class WallpaperCropper { // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and // trying to preserve the zoom level and the center of the image - SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); int rotatedOrientation = getRotatedOrientation(orientation); suggestedCrop = suggestedCrops.get(rotatedOrientation); - Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation); if (suggestedCrop != null) { // only keep the visible part (without parallax) Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -197,9 +188,9 @@ public class WallpaperCropper { // Case 4: if the device is a foldable, if we're looking for a folded orientation and have // the suggested crop of the relative unfolded orientation, reuse it by removing content. - int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation); suggestedCrop = suggestedCrops.get(unfoldedOrientation); - suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); + suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(unfoldedOrientation); if (suggestedCrop != null) { // compute the visible part (without parallax) of the unfolded screen Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -207,8 +198,11 @@ public class WallpaperCropper { Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); // if we removed some width, add it back to add a parallax effect if (res.width() < adjustedCrop.width()) { - if (rtl) res.left = Math.min(res.left, adjustedCrop.left); - else res.right = Math.max(res.right, adjustedCrop.right); + if (rtl) { + res.left = Math.min(res.left, adjustedCrop.left); + } else { + res.right = Math.max(res.right, adjustedCrop.right); + } // use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD); } @@ -218,9 +212,9 @@ public class WallpaperCropper { // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and // have the suggested crop of the relative folded orientation, reuse it by adding content. - int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); + int foldedOrientation = defaultDisplayInfo.getFoldedOrientation(orientation); suggestedCrop = suggestedCrops.get(foldedOrientation); - suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation); + suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(foldedOrientation); if (suggestedCrop != null) { // only keep the visible part (without parallax) Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -229,17 +223,19 @@ public class WallpaperCropper { // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: // rotate, then fold or unfold - Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + Point rotatedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation); if (rotatedDisplaySize != null) { - int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation); - int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation); + int rotatedFolded = defaultDisplayInfo.getFoldedOrientation(rotatedOrientation); + int rotateUnfolded = defaultDisplayInfo.getUnfoldedOrientation(rotatedOrientation); for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) { suggestedCrop = suggestedCrops.get(suggestedOrientation); if (suggestedCrop != null) { - Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + Rect rotatedCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl); SparseArray<Rect> rotatedCropMap = new SparseArray<>(); rotatedCropMap.put(rotatedOrientation, rotatedCrop); - return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, rotatedCropMap, + rtl); } } } @@ -248,8 +244,8 @@ public class WallpaperCropper { Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops + ", orientation: " + orientation + ", rtl: " + rtl - + ", defaultDisplaySizes: " + defaultDisplaySizes); - return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + + ", defaultDisplaySizes: " + defaultDisplayInfo.defaultDisplaySizes); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), rtl); } /** @@ -445,7 +441,7 @@ public class WallpaperCropper { Rect suggestedCrop = suggestedCrops.get(orientation); if (suggestedCrop != null) { adjustedSuggestedCrops.put(orientation, - getCrop(displaySize, bitmapSize, suggestedCrops, rtl)); + getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, suggestedCrops, rtl)); } } @@ -455,7 +451,8 @@ public class WallpaperCropper { int orientation = defaultDisplaySizes.keyAt(i); if (result.contains(orientation)) continue; Point displaySize = defaultDisplaySizes.valueAt(i); - Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl); + Rect newCrop = getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, + adjustedSuggestedCrops, rtl); result.put(orientation, newCrop); } return result; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index ba0262a8bd19..69f0ef7c430e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -542,9 +542,11 @@ public class WallpaperDataParser { // to support back compatibility in B&R, save the crops for one orientation in the // legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet; - if (mWallpaperDisplayHelper.isFoldable()) { - int unfoldedOrientation = mWallpaperDisplayHelper - .getUnfoldedOrientation(orientationToPutInLegacyCrop); + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); + if (defaultDisplayInfo.isFoldable) { + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation( + orientationToPutInLegacyCrop); if (unfoldedOrientation != ORIENTATION_UNKNOWN) { orientationToPutInLegacyCrop = unfoldedOrientation; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java new file mode 100644 index 000000000000..dabe91968338 --- /dev/null +++ b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java @@ -0,0 +1,207 @@ +/* + * 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.wallpaper; + +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.app.WallpaperManager.getRotatedOrientation; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + +import android.app.WallpaperManager; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + + +/** A data class for the default display attributes used in wallpaper related operations. */ +public final class WallpaperDefaultDisplayInfo { + /** + * A data class representing the screen orientations for a foldable device in the folded and + * unfolded states. + */ + @VisibleForTesting + static final class FoldableOrientations { + @WallpaperManager.ScreenOrientation + public final int foldedOrientation; + @WallpaperManager.ScreenOrientation + public final int unfoldedOrientation; + + FoldableOrientations(int foldedOrientation, int unfoldedOrientation) { + this.foldedOrientation = foldedOrientation; + this.unfoldedOrientation = unfoldedOrientation; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof FoldableOrientations that)) return false; + return foldedOrientation == that.foldedOrientation + && unfoldedOrientation == that.unfoldedOrientation; + } + + @Override + public int hashCode() { + return Objects.hash(foldedOrientation, unfoldedOrientation); + } + } + + public final SparseArray<Point> defaultDisplaySizes; + public final boolean isLargeScreen; + public final boolean isFoldable; + @VisibleForTesting + final List<FoldableOrientations> foldableOrientations; + + public WallpaperDefaultDisplayInfo() { + this.defaultDisplaySizes = new SparseArray<>(); + this.isLargeScreen = false; + this.isFoldable = false; + this.foldableOrientations = Collections.emptyList(); + } + + public WallpaperDefaultDisplayInfo(WindowManager windowManager, Resources resources) { + Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + boolean isFoldable = resources.getIntArray(R.array.config_foldedDeviceStates).length > 0; + if (isFoldable) { + this.foldableOrientations = getFoldableOrientations(metrics); + } else { + this.foldableOrientations = Collections.emptyList(); + } + this.defaultDisplaySizes = getDisplaySizes(metrics); + this.isLargeScreen = isLargeScreen(metrics); + this.isFoldable = isFoldable; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof WallpaperDefaultDisplayInfo that)) return false; + return isLargeScreen == that.isLargeScreen && isFoldable == that.isFoldable + && defaultDisplaySizes.contentEquals(that.defaultDisplaySizes) + && Objects.equals(foldableOrientations, that.foldableOrientations); + } + + @Override + public int hashCode() { + return 31 * Objects.hash(isLargeScreen, isFoldable, foldableOrientations) + + defaultDisplaySizes.contentHashCode(); + } + + /** + * Returns the folded orientation corresponds to the {@code unfoldedOrientation} found in + * {@link #foldableOrientations}. If not found, returns + * {@link WallpaperManager.ORIENTATION_UNKNOWN}. + */ + public int getFoldedOrientation(int unfoldedOrientation) { + for (FoldableOrientations orientations : foldableOrientations) { + if (orientations.unfoldedOrientation == unfoldedOrientation) { + return orientations.foldedOrientation; + } + } + return ORIENTATION_UNKNOWN; + } + + /** + * Returns the unfolded orientation corresponds to the {@code foldedOrientation} found in + * {@link #foldableOrientations}. If not found, returns + * {@link WallpaperManager.ORIENTATION_UNKNOWN}. + */ + public int getUnfoldedOrientation(int foldedOrientation) { + for (FoldableOrientations orientations : foldableOrientations) { + if (orientations.foldedOrientation == foldedOrientation) { + return orientations.unfoldedOrientation; + } + } + return ORIENTATION_UNKNOWN; + } + + private static SparseArray<Point> getDisplaySizes(Set<WindowMetrics> displayMetrics) { + SparseArray<Point> displaySizes = new SparseArray<>(); + for (WindowMetrics metric : displayMetrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); + for (Point point : List.of(displaySize, reversedDisplaySize)) { + int orientation = WallpaperManager.getOrientation(point); + // don't add an entry if there is already a larger display of the same orientation + Point display = displaySizes.get(orientation); + if (display == null || display.x * display.y < point.x * point.y) { + displaySizes.put(orientation, point); + } + } + } + return displaySizes; + } + + private static boolean isLargeScreen(Set<WindowMetrics> displayMetrics) { + float smallestWidth = Float.MAX_VALUE; + for (WindowMetrics metric : displayMetrics) { + Rect bounds = metric.getBounds(); + smallestWidth = Math.min(smallestWidth, bounds.width() / metric.getDensity()); + } + return smallestWidth >= LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + } + + /** + * Determines all potential foldable orientations, populating {@code + * outFoldableOrientationPairs} with pairs of (folded orientation, unfolded orientation). If + * {@code defaultDisplayMetrics} isn't for foldable, {@code outFoldableOrientationPairs} will + * not be populated. + */ + private static List<FoldableOrientations> getFoldableOrientations( + Set<WindowMetrics> defaultDisplayMetrics) { + if (defaultDisplayMetrics.size() != 2) { + return Collections.emptyList(); + } + List<FoldableOrientations> foldableOrientations = new ArrayList<>(); + float surface = 0; + int firstOrientation = -1; + for (WindowMetrics metric : defaultDisplayMetrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + + int orientation = WallpaperManager.getOrientation(displaySize); + float newSurface = displaySize.x * displaySize.y + / (metric.getDensity() * metric.getDensity()); + if (surface <= 0) { + surface = newSurface; + firstOrientation = orientation; + } else { + FoldableOrientations orientations = (newSurface > surface) + ? new FoldableOrientations(firstOrientation, orientation) + : new FoldableOrientations(orientation, firstOrientation); + FoldableOrientations rotatedOrientations = new FoldableOrientations( + getRotatedOrientation(orientations.foldedOrientation), + getRotatedOrientation(orientations.unfoldedOrientation)); + foldableOrientations.add(orientations); + foldableOrientations.add(rotatedOrientations); + } + } + return Collections.unmodifiableList(foldableOrientations); + } +} diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index 3636f5aa8f27..bff5fc9c49f3 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -16,31 +16,25 @@ package com.android.server.wallpaper; -import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; -import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.window.flags.Flags.multiCrop; import android.app.WallpaperManager; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Debug; -import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; -import android.view.WindowMetrics; import com.android.server.wm.WindowManagerInternal; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import java.util.function.Consumer; /** @@ -59,65 +53,25 @@ class WallpaperDisplayHelper { } private static final String TAG = WallpaperDisplayHelper.class.getSimpleName(); - private static final float LARGE_SCREEN_MIN_DP = 600f; private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>(); private final DisplayManager mDisplayManager; private final WindowManagerInternal mWindowManagerInternal; - private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>(); - // related orientations pairs for foldable (folded orientation, unfolded orientation) - private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>(); - - private final boolean mIsFoldable; - private boolean mIsLargeScreen = false; + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; WallpaperDisplayHelper( DisplayManager displayManager, WindowManager windowManager, WindowManagerInternal windowManagerInternal, - boolean isFoldable) { + Resources resources) { mDisplayManager = displayManager; mWindowManagerInternal = windowManagerInternal; - mIsFoldable = isFoldable; - if (!multiCrop()) return; - Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); - boolean populateOrientationPairs = isFoldable && metrics.size() == 2; - float surface = 0; - int firstOrientation = -1; - for (WindowMetrics metric: metrics) { - Rect bounds = metric.getBounds(); - Point displaySize = new Point(bounds.width(), bounds.height()); - Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); - for (Point point : List.of(displaySize, reversedDisplaySize)) { - int orientation = WallpaperManager.getOrientation(point); - // don't add an entry if there is already a larger display of the same orientation - Point display = mDefaultDisplaySizes.get(orientation); - if (display == null || display.x * display.y < point.x * point.y) { - mDefaultDisplaySizes.put(orientation, point); - } - } - - mIsLargeScreen |= (displaySize.x / metric.getDensity() >= LARGE_SCREEN_MIN_DP); - - if (populateOrientationPairs) { - int orientation = WallpaperManager.getOrientation(displaySize); - float newSurface = displaySize.x * displaySize.y - / (metric.getDensity() * metric.getDensity()); - if (surface <= 0) { - surface = newSurface; - firstOrientation = orientation; - } else { - Pair<Integer, Integer> pair = (newSurface > surface) - ? new Pair<>(firstOrientation, orientation) - : new Pair<>(orientation, firstOrientation); - Pair<Integer, Integer> rotatedPair = new Pair<>( - getRotatedOrientation(pair.first), getRotatedOrientation(pair.second)); - mFoldableOrientationPairs.add(pair); - mFoldableOrientationPairs.add(rotatedPair); - } - } + if (!multiCrop()) { + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(); + return; } + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources); } DisplayData getDisplayDataOrCreate(int displayId) { @@ -203,51 +157,21 @@ class WallpaperDisplayHelper { } SparseArray<Point> getDefaultDisplaySizes() { - return mDefaultDisplaySizes; + return mDefaultDisplayInfo.defaultDisplaySizes; } /** Return the number of pixel of the largest dimension of the default display */ int getDefaultDisplayLargestDimension() { + SparseArray<Point> defaultDisplaySizes = mDefaultDisplayInfo.defaultDisplaySizes; int result = -1; - for (int i = 0; i < mDefaultDisplaySizes.size(); i++) { - Point size = mDefaultDisplaySizes.valueAt(i); + for (int i = 0; i < defaultDisplaySizes.size(); i++) { + Point size = defaultDisplaySizes.valueAt(i); result = Math.max(result, Math.max(size.x, size.y)); } return result; } - boolean isFoldable() { - return mIsFoldable; - } - - /** - * Return true if any of the screens of the default display is considered large (DP >= 600) - */ - boolean isLargeScreen() { - return mIsLargeScreen; - } - - /** - * If a given orientation corresponds to an unfolded orientation on foldable, return the - * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the - * device is not a foldable. - */ - int getFoldedOrientation(int orientation) { - for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { - if (pair.second.equals(orientation)) return pair.first; - } - return ORIENTATION_UNKNOWN; - } - - /** - * If a given orientation corresponds to a folded orientation on foldable, return the - * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the - * device is not a foldable. - */ - int getUnfoldedOrientation(int orientation) { - for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { - if (pair.first.equals(orientation)) return pair.second; - } - return ORIENTATION_UNKNOWN; + public WallpaperDefaultDisplayInfo getDefaultDisplayInfo() { + return mDefaultDisplayInfo; } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index bac732637d8d..e7da33d50b27 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1666,12 +1666,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); displayManager.registerDisplayListener(mDisplayListener, null /* handler */); WindowManager windowManager = mContext.getSystemService(WindowManager.class); - boolean isFoldable = mContext.getResources() - .getIntArray(R.array.config_foldedDeviceStates).length > 0; mWallpaperDisplayHelper = new WallpaperDisplayHelper( - displayManager, windowManager, mWindowManagerInternal, isFoldable); + displayManager, windowManager, mWindowManagerInternal, mContext.getResources()); mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); - mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop); mActivityManager = mContext.getSystemService(ActivityManager.class); if (mContext.getResources().getBoolean( @@ -2510,9 +2507,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub List<Rect> result = new ArrayList<>(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); for (Point displaySize : displaySizes) { - result.add(mWallpaperCropper.getCrop( - displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); + result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, + croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); } if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result); return result; @@ -2548,8 +2547,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub List<Rect> result = new ArrayList<>(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); for (Point displaySize : displaySizes) { - result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl)); + result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, bitmapSize, + defaultCrops, rtl)); } return result; } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index a94183849bc5..e2b47b92f232 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -112,7 +112,6 @@ import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.apphibernation.AppHibernationManagerInternal; import com.android.server.apphibernation.AppHibernationService; -import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -807,14 +806,8 @@ class ActivityMetricsLogger { } final Task otherTask = otherInfo.mLastLaunchedActivity.getTask(); // The adjacent task is the split root in which activities are started - final boolean isDescendantOfAdjacent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks( - otherTask::isDescendantOf); - } else { - isDescendantOfAdjacent = otherTask.isDescendantOf( - launchedSplitRootTask.getAdjacentTask()); - } + final boolean isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks( + otherTask::isDescendantOf); if (isDescendantOfAdjacent) { if (DEBUG_METRICS) { Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3cd4db7d8dfc..e91d88901751 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1716,6 +1716,7 @@ final class ActivityRecord extends WindowToken { } mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId()); + mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay(); } void layoutLetterboxIfNeeded(WindowState winHint) { @@ -3801,19 +3802,10 @@ final class ActivityRecord extends WindowToken { final TaskFragment taskFragment = getTaskFragment(); if (next != null && taskFragment != null && taskFragment.isEmbedded()) { final TaskFragment organized = taskFragment.getOrganizedTaskFragment(); - if (Flags.allowMultipleAdjacentTaskFragments()) { - delayRemoval = organized != null - && organized.topRunningActivity() == null - && organized.isDelayLastActivityRemoval() - && organized.forOtherAdjacentTaskFragments(next::isDescendantOf); - } else { - final TaskFragment adjacent = - organized != null ? organized.getAdjacentTaskFragment() : null; - if (adjacent != null && next.isDescendantOf(adjacent) - && organized.topRunningActivity() == null) { - delayRemoval = organized.isDelayLastActivityRemoval(); - } - } + delayRemoval = organized != null + && organized.topRunningActivity() == null + && organized.isDelayLastActivityRemoval() + && organized.forOtherAdjacentTaskFragments(next::isDescendantOf); } // isNextNotYetVisible is to check if the next activity is invisible, or it has been @@ -4787,11 +4779,6 @@ final class ActivityRecord extends WindowToken { } // Make sure the embedded adjacent can also be shown. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment() - .getTopNonFinishingActivity(); - return canShowWhenLocked(adjacentActivity); - } final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments( adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity())); return !hasAdjacentNotAllowToShow; @@ -8980,6 +8967,7 @@ final class ActivityRecord extends WindowToken { // Reset the existing override configuration so it can be updated according to the latest // configuration. mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); + mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted(); if (!attachedToProcess()) { return; diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index cb122f2080a2..0f1939bfbb49 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -34,7 +34,6 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; -import com.android.window.flags.Flags; import java.io.File; import java.io.PrintWriter; @@ -532,26 +531,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord final int currentIndex = currTF.asTask() != null ? currentTask.mChildren.indexOf(currentActivity) : currentTask.mChildren.indexOf(currTF); - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final int prevAdjacentIndex = currentTask.mChildren.indexOf( - prevTF.getAdjacentTaskFragment()); - if (prevAdjacentIndex > currentIndex) { - // PrevAdjacentTF already above currentActivity - return; - } - // Add both the one below, and its adjacent. - if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { - result.add(initPrev); - } - final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment() - .getTopMostActivity(); - if (prevAdjacentActivity != null && (!inTransition - || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { - result.add(prevAdjacentActivity); - } - return; - } - final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments( prevAdjacentTF -> { final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index a7f2153993bb..b0563128870a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -164,7 +164,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.pm.SaferIntentUtils; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; -import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -2991,17 +2990,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { if (child.asTaskFragment() != null && child.asTaskFragment().hasAdjacentTaskFragment()) { - final boolean isAnyTranslucent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment.AdjacentSet set = - child.asTaskFragment().getAdjacentTaskFragments(); - isAnyTranslucent = set.forAllTaskFragments( - tf -> !isOpaque(tf), null); - } else { - final TaskFragment adjacent = child.asTaskFragment() - .getAdjacentTaskFragment(); - isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent); - } + final boolean isAnyTranslucent = !isOpaque(child) + || child.asTaskFragment().forOtherAdjacentTaskFragments( + tf -> !isOpaque(tf)); if (!isAnyTranslucent) { // This task fragment and all its adjacent task fragments are opaque, // consider it opaque even if it doesn't fill its parent. diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 48f08e945a59..c479591a5e0d 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -46,6 +46,8 @@ class AppCompatController { private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy; @NonNull private final AppCompatSandboxingPolicy mSandboxingPolicy; + @NonNull + private final AppCompatDisplayCompatModePolicy mDisplayCompatModePolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -69,6 +71,7 @@ class AppCompatController { mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord); + mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy(); } @NonNull @@ -151,6 +154,11 @@ class AppCompatController { return mSandboxingPolicy; } + @NonNull + AppCompatDisplayCompatModePolicy getDisplayCompatModePolicy() { + return mDisplayCompatModePolicy; + } + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java new file mode 100644 index 000000000000..acf51707c894 --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import com.android.window.flags.Flags; + +/** + * Encapsulate app-compat logic for multi-display environments. + */ +class AppCompatDisplayCompatModePolicy { + + private boolean mIsRestartMenuEnabledForDisplayMove; + + boolean isRestartMenuEnabledForDisplayMove() { + return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove; + } + + void onMovedToDisplay() { + mIsRestartMenuEnabledForDisplayMove = true; + } + + void onProcessRestarted() { + mIsRestartMenuEnabledForDisplayMove = false; + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 146044008b3f..b91a12598e01 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -161,6 +161,9 @@ final class AppCompatUtils { top.mAppCompatController.getLetterboxOverrides() .isLetterboxEducationEnabled()); + appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController + .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove()); + final AppCompatAspectRatioOverrides aspectRatioOverrides = top.mAppCompatController.getAspectRatioOverrides(); appCompatTaskInfo.setUserFullscreenOverrideEnabled( diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e9b7649e8cbd..dfe323c43abb 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -479,21 +479,16 @@ class BackNavigationController { } } else { // If adjacent TF has companion to current TF, those two TF will be closed together. - final TaskFragment adjacentTF; - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (currTF.getAdjacentTaskFragments().size() > 2) { - throw new IllegalStateException( - "Not yet support 3+ adjacent for non-Task TFs"); - } - final TaskFragment[] tmpAdjacent = new TaskFragment[1]; - currTF.forOtherAdjacentTaskFragments(tf -> { - tmpAdjacent[0] = tf; - return true; - }); - adjacentTF = tmpAdjacent[0]; - } else { - adjacentTF = currTF.getAdjacentTaskFragment(); + if (currTF.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException( + "Not yet support 3+ adjacent for non-Task TFs"); } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + currTF.forOtherAdjacentTaskFragments(tf -> { + tmpAdjacent[0] = tf; + return true; + }); + final TaskFragment adjacentTF = tmpAdjacent[0]; if (isSecondCompanionToFirst(currTF, adjacentTF)) { // The two TFs are adjacent (visually displayed side-by-side), search if any // activity below the lowest one. @@ -553,15 +548,6 @@ class BackNavigationController { if (!prevTF.hasAdjacentTaskFragment()) { return; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment(); - final ActivityRecord prevActivityAdjacent = - prevTFAdjacent.getTopNonFinishingActivity(); - if (prevActivityAdjacent != null) { - outPrevActivities.add(prevActivityAdjacent); - } - return; - } prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> { final ActivityRecord prevActivityAdjacent = prevTFAdjacent.getTopNonFinishingActivity(); @@ -577,14 +563,6 @@ class BackNavigationController { if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) { return; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment(); - final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); - if (topActivity != null) { - outList.add(topActivity); - } - return; - } mainTF.forOtherAdjacentTaskFragments(adjacentTF -> { final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); if (topActivity != null) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 2287a687700c..f50a68cc5389 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -49,8 +49,8 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictModeRo; import static com.android.window.flags.Flags.balStrictModeGracePeriod; +import static com.android.window.flags.Flags.balStrictModeRo; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -91,7 +91,6 @@ import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; -import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -1687,14 +1686,6 @@ public class BackgroundActivityStartController { } // Check the adjacent fragment. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate); - if (topActivity == null) { - return bas; - } - return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas); - } final BlockActivityStart[] out = { bas }; taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> { final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate); diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index f473b7b7e4fb..fcc697242ff6 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -18,7 +18,7 @@ package com.android.server.wm; import static android.view.WindowManager.TRANSIT_CHANGE; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN; import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS; import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; @@ -140,8 +140,9 @@ class DeferredDisplayUpdater { if (displayInfoDiff == DIFF_EVERYTHING || !mDisplayContent.getLastHasContent() || !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: applying DisplayInfo immediately"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) immediately", + displayInfo.logicalWidth, displayInfo.logicalHeight); mLastWmDisplayInfo = displayInfo; applyLatestDisplayInfo(); @@ -151,17 +152,23 @@ class DeferredDisplayUpdater { // If there are non WM-specific display info changes, apply only these fields immediately if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: partially applying DisplayInfo immediately"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: partially applying DisplayInfo(%d x %d) immediately", + displayInfo.logicalWidth, displayInfo.logicalHeight); applyLatestDisplayInfo(); } // If there are WM-specific display info changes, apply them through a Shell transition if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: deferring DisplayInfo update"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: deferring DisplayInfo(%d x %d) update", + displayInfo.logicalWidth, displayInfo.logicalHeight); requestDisplayChangeTransition(physicalDisplayUpdated, () -> { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) after deferring", + displayInfo.logicalWidth, displayInfo.logicalHeight); + // Apply deferrable fields to DisplayContent only when the transition // starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo mLastWmDisplayInfo = displayInfo; @@ -199,7 +206,7 @@ class DeferredDisplayUpdater { mDisplayContent.getDisplayPolicy().getNotificationShade(); if (notificationShade != null && notificationShade.isVisible() && mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing( - mDisplayContent.mDisplayId)) { + mDisplayContent.mDisplayId)) { Slog.i(TAG, notificationShade + " uses blast for display switch"); notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST; } @@ -209,9 +216,6 @@ class DeferredDisplayUpdater { try { onStartCollect.run(); - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: applied DisplayInfo after deferring"); - if (physicalDisplayUpdated) { onDisplayUpdated(transition, fromRotation, startBounds); } else { diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index d466a646c7dd..ddcb5eccb1d8 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -19,6 +19,12 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -131,6 +137,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { return RESULT_SKIP; } + if (DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue()) { + ActivityRecord topVisibleFreeformActivity = + task.getDisplayContent().getTopMostVisibleFreeformActivity(); + if (shouldInheritExistingTaskBounds(topVisibleFreeformActivity, activity, task)) { + appendLog("inheriting bounds from existing closing instance"); + outParams.mBounds.set(topVisibleFreeformActivity.getBounds()); + appendLog("final desktop mode task bounds set to %s", outParams.mBounds); + // Return result done to prevent other modifiers from changing or cascading bounds. + return RESULT_DONE; + } + } + DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options, outParams.mBounds, this::appendLog); appendLog("final desktop mode task bounds set to %s", outParams.mBounds); @@ -159,7 +177,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { // activity will also enter desktop mode. On this same relationship, we can also assume // if there are not visible freeform tasks but a freeform activity is now launching, it // will force the device into desktop mode. - return (task.getDisplayContent().getTopMostVisibleFreeformActivity() != null + return (task.getDisplayContent().getTopMostFreeformActivity() != null && checkSourceWindowModesCompatible(task, options, currentParams)) || isRequestingFreeformWindowMode(task, options, currentParams); } @@ -201,6 +219,40 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { }; } + /** + * Whether the launching task should inherit the task bounds of an existing closing instance. + */ + private boolean shouldInheritExistingTaskBounds( + @Nullable ActivityRecord existingTaskActivity, + @Nullable ActivityRecord launchingActivity, + @NonNull Task launchingTask) { + if (existingTaskActivity == null || launchingActivity == null) return false; + return (existingTaskActivity.packageName == launchingActivity.packageName) + && isLaunchingNewTask(launchingActivity.launchMode, + launchingTask.getBaseIntent().getFlags()) + && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags()); + } + + /** + * Returns true if the launch mode or intent will result in a new task being created for the + * activity. + */ + private boolean isLaunchingNewTask(int launchMode, int intentFlags) { + return launchMode == LAUNCH_SINGLE_TASK + || launchMode == LAUNCH_SINGLE_INSTANCE + || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK + || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0; + } + + /** + * Returns true if the intent will result in an existing task instance being closed if a new + * one appears. + */ + private boolean isClosingExitingInstance(int intentFlags) { + return (intentFlags & FLAG_ACTIVITY_CLEAR_TASK) != 0 + || (intentFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0; + } + private void initLogBuilder(Task task, ActivityRecord activity) { if (DEBUG) { mLogBuilder = new StringBuilder( diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java index a017a1173d97..e508a6d23178 100644 --- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java +++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java @@ -23,8 +23,6 @@ import static com.android.server.wm.Task.TAG_VISIBILITY; import android.annotation.Nullable; import android.util.Slog; -import com.android.window.flags.Flags; - import java.util.ArrayList; /** Helper class to ensure activities are in the right visible state for a container. */ @@ -112,18 +110,11 @@ class EnsureActivitiesVisibleHelper { if (adjacentTaskFragments != null && adjacentTaskFragments.contains( childTaskFragment)) { - final boolean isTranslucent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - isTranslucent = childTaskFragment.isTranslucent(starting) - || childTaskFragment.forOtherAdjacentTaskFragments( - adjacentTaskFragment -> { - return adjacentTaskFragment.isTranslucent(starting); - }); - } else { - isTranslucent = childTaskFragment.isTranslucent(starting) - || childTaskFragment.getAdjacentTaskFragment() - .isTranslucent(starting); - } + final boolean isTranslucent = childTaskFragment.isTranslucent(starting) + || childTaskFragment.forOtherAdjacentTaskFragments( + adjacentTaskFragment -> { + return adjacentTaskFragment.isTranslucent(starting); + }); if (!isTranslucent) { // Everything behind two adjacent TaskFragments are occluded. mBehindFullyOccludedContainer = true; @@ -135,14 +126,10 @@ class EnsureActivitiesVisibleHelper { if (adjacentTaskFragments == null) { adjacentTaskFragments = new ArrayList<>(); } - if (Flags.allowMultipleAdjacentTaskFragments()) { - final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments; - childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - adjacentTfs.add(adjacentTf); - }); - } else { - adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment()); - } + final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments; + childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + adjacentTfs.add(adjacentTf); + }); } } else if (child.asActivityRecord() != null) { setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index cf201c9f34f0..609302ce3f56 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1732,26 +1732,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> activityAssistInfos.clear(); activityAssistInfos.add(new ActivityAssistInfo(top)); // Check if the activity on the split screen. - if (Flags.allowMultipleAdjacentTaskFragments()) { - top.getTask().forOtherAdjacentTasks(task -> { - final ActivityRecord adjacentActivityRecord = - task.getTopNonFinishingActivity(); - if (adjacentActivityRecord != null) { - activityAssistInfos.add( - new ActivityAssistInfo(adjacentActivityRecord)); - } - }); - } else { - final Task adjacentTask = top.getTask().getAdjacentTask(); - if (adjacentTask != null) { - final ActivityRecord adjacentActivityRecord = - adjacentTask.getTopNonFinishingActivity(); - if (adjacentActivityRecord != null) { - activityAssistInfos.add( - new ActivityAssistInfo(adjacentActivityRecord)); - } + top.getTask().forOtherAdjacentTasks(task -> { + final ActivityRecord adjacentActivityRecord = + task.getTopNonFinishingActivity(); + if (adjacentActivityRecord != null) { + activityAssistInfos.add( + new ActivityAssistInfo(adjacentActivityRecord)); } - } + }); if (rootTask == topFocusedRootTask) { topVisibleActivities.addAll(0, activityAssistInfos); } else { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d16c301cec40..e98b2b749af8 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2461,21 +2461,6 @@ class Task extends TaskFragment { return parentTask == null ? null : parentTask.getCreatedByOrganizerTask(); } - /** @deprecated b/373709676 replace with {@link #forOtherAdjacentTasks(Consumer)} ()}. */ - @Deprecated - @Nullable - Task getAdjacentTask() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. " - + "Use #forOtherAdjacentTasks instead"); - } - final Task taskWithAdjacent = getTaskWithAdjacent(); - if (taskWithAdjacent == null) { - return null; - } - return taskWithAdjacent.getAdjacentTaskFragment().asTask(); - } - /** Finds the first Task parent (or itself) that has adjacent. */ @Nullable Task getTaskWithAdjacent() { @@ -2499,11 +2484,6 @@ class Task extends TaskFragment { * Tasks. The invoke order is not guaranteed. */ void forOtherAdjacentTasks(@NonNull Consumer<Task> callback) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. " - + "Use #getAdjacentTask instead"); - } - final Task taskWithAdjacent = getTaskWithAdjacent(); if (taskWithAdjacent == null) { return; @@ -2521,10 +2501,6 @@ class Task extends TaskFragment { * guaranteed. */ boolean forOtherAdjacentTasks(@NonNull Predicate<Task> callback) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. " - + "Use getAdjacentTask instead"); - } final Task taskWithAdjacent = getTaskWithAdjacent(); if (taskWithAdjacent == null) { return false; @@ -3651,20 +3627,13 @@ class Task extends TaskFragment { final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && taskFragment.isEmbedded() && taskFragment.hasAdjacentTaskFragment()) { - if (Flags.allowMultipleAdjacentTaskFragments()) { - final int[] nextLayer = { layer }; - taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - if (adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, nextLayer[0]++); - } - }); - layer = nextLayer[0]; - } else { - final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); + final int[] nextLayer = { layer }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { if (adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, layer++); + adjacentTf.assignLayer(t, nextLayer[0]++); } - } + }); + layer = nextLayer[0]; } // Place the decor surface just above the owner TaskFragment. diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 1966ecf57c73..fb7bab4b3e26 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -60,7 +60,6 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.LaunchParamsController.LaunchParams; -import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -1089,19 +1088,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // Use launch-adjacent-flag-root if launching with launch-adjacent flag. if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0 && mLaunchAdjacentFlagRootTask != null) { - final Task launchAdjacentRootAdjacentTask; - if (Flags.allowMultipleAdjacentTaskFragments()) { - final Task[] tmpTask = new Task[1]; - mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> { - // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+. - // Find the first adjacent for now. - tmpTask[0] = task; - return true; - }); - launchAdjacentRootAdjacentTask = tmpTask[0]; - } else { - launchAdjacentRootAdjacentTask = mLaunchAdjacentFlagRootTask.getAdjacentTask(); - } + final Task[] tmpTask = new Task[1]; + mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> { + // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+. + // Find the first adjacent for now. + tmpTask[0] = task; + return true; + }); + final Task launchAdjacentRootAdjacentTask = tmpTask[0]; if (sourceTask != null && (sourceTask == candidateTask || sourceTask.topRunningActivity() == null)) { // Do nothing when task that is getting opened is same as the source or when @@ -1129,14 +1123,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { if (launchRootTask == null || sourceTask == null) { return launchRootTask; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final Task adjacentRootTask = launchRootTask.getAdjacentTask(); - if (adjacentRootTask != null && (sourceTask == adjacentRootTask - || sourceTask.isDescendantOf(adjacentRootTask))) { - return adjacentRootTask; - } - return launchRootTask; - } final Task[] adjacentRootTask = new Task[1]; launchRootTask.forOtherAdjacentTasks(task -> { if (sourceTask == task || sourceTask.isDescendantOf(task)) { @@ -1163,24 +1149,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return sourceTask.getCreatedByOrganizerTask(); } // Check if the candidate is already positioned in the adjacent Task. - if (Flags.allowMultipleAdjacentTaskFragments()) { - final Task[] adjacentRootTask = new Task[1]; - sourceTask.forOtherAdjacentTasks(task -> { - if (candidateTask == task || candidateTask.isDescendantOf(task)) { - adjacentRootTask[0] = task; - return true; - } - return false; - }); - if (adjacentRootTask[0] != null) { - return adjacentRootTask[0]; - } - } else { - final Task adjacentTarget = taskWithAdjacent.getAdjacentTask(); - if (candidateTask == adjacentTarget - || candidateTask.isDescendantOf(adjacentTarget)) { - return adjacentTarget; + final Task[] adjacentRootTask = new Task[1]; + sourceTask.forOtherAdjacentTasks(task -> { + if (candidateTask == task || candidateTask.isDescendantOf(task)) { + adjacentRootTask[0] = task; + return true; } + return false; + }); + if (adjacentRootTask[0] != null) { + return adjacentRootTask[0]; } return sourceTask.getCreatedByOrganizerTask(); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 5183c6b57f15..2dabb253744a 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -235,11 +235,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** This task fragment will be removed when the cleanup of its children are done. */ private boolean mIsRemovalRequested; - /** @deprecated b/373709676 replace with {@link #mAdjacentTaskFragments} */ - @Deprecated - @Nullable - private TaskFragment mAdjacentTaskFragment; - /** * The TaskFragments that are adjacent to each other, including this TaskFragment. * All TaskFragments in this set share the same set instance. @@ -455,22 +450,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return service.mWindowOrganizerController.getTaskFragment(token); } - /** @deprecated b/373709676 replace with {@link #setAdjacentTaskFragments}. */ - @Deprecated - void setAdjacentTaskFragment(@NonNull TaskFragment taskFragment) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - if (mAdjacentTaskFragment == taskFragment) { - return; - } - resetAdjacentTaskFragment(); - mAdjacentTaskFragment = taskFragment; - taskFragment.setAdjacentTaskFragment(this); - return; - } - - setAdjacentTaskFragments(new AdjacentSet(this, taskFragment)); - } - void setAdjacentTaskFragments(@NonNull AdjacentSet adjacentTaskFragments) { adjacentTaskFragments.setAsAdjacent(); } @@ -483,56 +462,18 @@ class TaskFragment extends WindowContainer<WindowContainer> { return mCompanionTaskFragment; } - /** @deprecated b/373709676 replace with {@link #clearAdjacentTaskFragments()}. */ - @Deprecated - private void resetAdjacentTaskFragment() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("resetAdjacentTaskFragment shouldn't be called when" - + " allowMultipleAdjacentTaskFragments is enabled. Use either" - + " #clearAdjacentTaskFragments or #removeFromAdjacentTaskFragments"); - } - // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment. - if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) { - mAdjacentTaskFragment.mAdjacentTaskFragment = null; - mAdjacentTaskFragment.mDelayLastActivityRemoval = false; - } - mAdjacentTaskFragment = null; - mDelayLastActivityRemoval = false; - } - void clearAdjacentTaskFragments() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - resetAdjacentTaskFragment(); - return; - } - if (mAdjacentTaskFragments != null) { mAdjacentTaskFragments.clear(); } } void removeFromAdjacentTaskFragments() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - resetAdjacentTaskFragment(); - return; - } - if (mAdjacentTaskFragments != null) { mAdjacentTaskFragments.remove(this); } } - /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */ - @Deprecated - @Nullable - TaskFragment getAdjacentTaskFragment() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. " - + "Use #getAdjacentTaskFragments instead"); - } - return mAdjacentTaskFragment; - } - @Nullable AdjacentSet getAdjacentTaskFragments() { return mAdjacentTaskFragments; @@ -561,16 +502,10 @@ class TaskFragment extends WindowContainer<WindowContainer> { } boolean hasAdjacentTaskFragment() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return mAdjacentTaskFragment != null; - } return mAdjacentTaskFragments != null; } boolean isAdjacentTo(@NonNull TaskFragment other) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return mAdjacentTaskFragment == other; - } return other != this && mAdjacentTaskFragments != null && mAdjacentTaskFragments.contains(other); @@ -1377,21 +1312,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (taskFragment.isAdjacentTo(this)) { continue; } - if (Flags.allowMultipleAdjacentTaskFragments()) { - final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds()) - || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - return mTmpRect.intersect(adjacentTf.getBounds()); - }); - if (isOccluding) { - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } - } else { - final TaskFragment adjacentTaskFragment = - taskFragment.mAdjacentTaskFragment; - if (mTmpRect.intersect(taskFragment.getBounds()) - || mTmpRect.intersect(adjacentTaskFragment.getBounds())) { - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } + final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds()) + || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + return mTmpRect.intersect(adjacentTf.getBounds()); + }); + if (isOccluding) { + return TASK_FRAGMENT_VISIBILITY_INVISIBLE; } } } @@ -1427,37 +1353,22 @@ class TaskFragment extends WindowContainer<WindowContainer> { // 2. Adjacent TaskFragments do not overlap, so that if this TaskFragment is behind // any translucent TaskFragment in the adjacent set, then this TaskFragment is // visible behind translucent. - if (Flags.allowMultipleAdjacentTaskFragments()) { - final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments( - adjacentTaskFragments::contains); - if (hasTraversedAdj) { - final boolean isTranslucent = - isBehindTransparentTaskFragment(otherTaskFrag, starting) - || otherTaskFrag.forOtherAdjacentTaskFragments( - (Predicate<TaskFragment>) tf -> - isBehindTransparentTaskFragment(tf, starting)); - if (isTranslucent) { - // Can be visible behind a translucent adjacent TaskFragments. - gotTranslucentFullscreen = true; - gotTranslucentAdjacent = true; - continue; - } - // Can not be visible behind adjacent TaskFragments. - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } - } else { - if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) { - if (isBehindTransparentTaskFragment(otherTaskFrag, starting) - || isBehindTransparentTaskFragment( - otherTaskFrag.mAdjacentTaskFragment, starting)) { - // Can be visible behind a translucent adjacent TaskFragments. - gotTranslucentFullscreen = true; - gotTranslucentAdjacent = true; - continue; - } - // Can not be visible behind adjacent TaskFragments. - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; + final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments( + adjacentTaskFragments::contains); + if (hasTraversedAdj) { + final boolean isTranslucent = + isBehindTransparentTaskFragment(otherTaskFrag, starting) + || otherTaskFrag.forOtherAdjacentTaskFragments( + (Predicate<TaskFragment>) tf -> + isBehindTransparentTaskFragment(tf, starting)); + if (isTranslucent) { + // Can be visible behind a translucent adjacent TaskFragments. + gotTranslucentFullscreen = true; + gotTranslucentAdjacent = true; + continue; } + // Can not be visible behind adjacent TaskFragments. + return TASK_FRAGMENT_VISIBILITY_INVISIBLE; } adjacentTaskFragments.add(otherTaskFrag); } @@ -3299,40 +3210,23 @@ class TaskFragment extends WindowContainer<WindowContainer> { final ArrayList<WindowContainer> siblings = getParent().mChildren; final int zOrder = siblings.indexOf(this); - - if (!Flags.allowMultipleAdjacentTaskFragments()) { - if (siblings.indexOf(getAdjacentTaskFragment()) < zOrder) { - // early return if this TF already has higher z-ordering. - return false; - } - } else { - final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments( - tf -> siblings.indexOf(tf) > zOrder); - if (!hasAdjacentOnTop) { - // early return if this TF already has higher z-ordering. - return false; - } + final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments( + tf -> siblings.indexOf(tf) > zOrder); + if (!hasAdjacentOnTop) { + // early return if this TF already has higher z-ordering. + return false; } final ToBooleanFunction<WindowState> getDimBehindWindow = (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested() || w.mActivityRecord.isVisible()); - - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTf = getAdjacentTaskFragment(); - if (adjacentTf.forAllWindows(getDimBehindWindow, true)) { - // early return if the adjacent Tf has a dimming window. - return false; - } - } else { - final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> { - return tf.forAllWindows(getDimBehindWindow, true); - }); - if (adjacentHasDimmingWindow) { - // early return if the adjacent Tf has a dimming window. - return false; - } + final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> { + return tf.forAllWindows(getDimBehindWindow, true); + }); + if (adjacentHasDimmingWindow) { + // early return if the adjacent Tf has a dimming window. + return false; } // boost if there's an Activity window that has FLAG_DIM_BEHIND flag. @@ -3456,16 +3350,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { sb.append(" organizerProc="); sb.append(mTaskFragmentOrganizerProcessName); } - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (mAdjacentTaskFragments != null) { - sb.append(" adjacent="); - sb.append(mAdjacentTaskFragments); - } - } else { - if (mAdjacentTaskFragment != null) { - sb.append(" adjacent="); - sb.append(mAdjacentTaskFragment); - } + if (mAdjacentTaskFragments != null) { + sb.append(" adjacent="); + sb.append(mAdjacentTaskFragments); } sb.append('}'); return sb.toString(); @@ -3591,10 +3478,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { } AdjacentSet(@NonNull ArraySet<TaskFragment> taskFragments) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be" - + " enabled to set more than two TaskFragments adjacent to each other."); - } final int size = taskFragments.size(); if (size < 2) { throw new IllegalArgumentException("Adjacent TaskFragments must contain at least" diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index c78cdaa10df2..803c21ccab6e 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2589,9 +2589,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } // When the TaskFragment has an adjacent TaskFragment, sibling behind them should be // hidden unless any of them are translucent. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition(); - } return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition); } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 3a4d9d27f65a..e1553cd37d03 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -57,7 +57,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ToBooleanFunction; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; +import com.android.server.wallpaper.WallpaperCropper; +import com.android.server.wallpaper.WallpaperDefaultDisplayInfo; import java.io.PrintWriter; import java.util.ArrayList; @@ -71,7 +72,6 @@ import java.util.function.Consumer; class WallpaperController { private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM; private WindowManagerService mService; - private WallpaperCropUtils mWallpaperCropUtils = null; private DisplayContent mDisplayContent; // Larger index has higher z-order. @@ -116,6 +116,10 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + // This is for WallpaperCropper, which has cropping logic for the default display only. + // TODO(b/400685784) make the WallpaperCropper operate on every display independently + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; + private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> { final ActivityRecord ar = w.mActivityRecord; // The animating window can still be visible on screen if it is in transition, so we @@ -198,12 +202,14 @@ class WallpaperController { WallpaperController(WindowManagerService service, DisplayContent displayContent) { mService = service; mDisplayContent = displayContent; + WindowManager windowManager = service.mContext.getSystemService(WindowManager.class); Resources resources = service.mContext.getResources(); mMinWallpaperScale = resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale); mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale); mShouldOffsetWallpaperCenter = resources.getBoolean( com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources); } void resetLargestDisplay(Display display) { @@ -246,10 +252,6 @@ class WallpaperController { return largestDisplaySize; } - void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { - mWallpaperCropUtils = wallpaperCropUtils; - } - WindowState getWallpaperTarget() { return mWallpaperTarget; } @@ -352,16 +354,12 @@ class WallpaperController { int offsetY; if (multiCrop()) { - if (mWallpaperCropUtils == null) { - Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting"); - return false; - } Point bitmapSize = new Point( wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight); SparseArray<Rect> cropHints = token.getCropHints(); wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame() - : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints, - wallpaperWin.isRtl()); + : WallpaperCropper.getCrop(screenSize, mDefaultDisplayInfo, bitmapSize, + cropHints, wallpaperWin.isRtl()); int frameWidth = wallpaperFrame.width(); int frameHeight = wallpaperFrame.height(); float frameRatio = (float) frameWidth / frameHeight; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 466ed7863c84..772a7fdfc684 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2006,11 +2006,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity(r -> !r.finishing, true /* traverseTopToBottom */); } - ActivityRecord getTopMostVisibleFreeformActivity() { + ActivityRecord getTopMostFreeformActivity() { return getActivity(r -> r.isVisibleRequested() && r.inFreeformWindowingMode(), true /* traverseTopToBottom */); } + ActivityRecord getTopMostVisibleFreeformActivity() { + return getActivity(r -> r.isVisible() && r.inFreeformWindowingMode(), + true /* traverseTopToBottom */); + } + ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) { // Break down into 4 calls to avoid object creation due to capturing input params. if (includeFinishing) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 4b5a3a031931..5f2a2ad7f0eb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -54,7 +54,6 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.input.InputManagerService; import com.android.server.policy.WindowManagerPolicy; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import java.lang.annotation.Retention; @@ -772,12 +771,6 @@ public abstract class WindowManagerInternal { public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints); /** - * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}. - * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper. - */ - public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils); - - /** * Returns {@code true} if a Window owned by {@code uid} has focus. */ public abstract boolean isUidFocused(int uid); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9fc0339c52a2..c078d67b6cc6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -356,7 +356,6 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -8100,12 +8099,6 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { - mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController - .setWallpaperCropUtils(wallpaperCropUtils); - } - - @Override public boolean isUidFocused(int uid) { synchronized (mGlobalLock) { for (int i = mRoot.getChildCount() - 1; i >= 0; i--) { @@ -9374,23 +9367,6 @@ public class WindowManagerService extends IWindowManager.Stub return focusedActivity; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity(); - if (adjacentTopActivity == null) { - // Return if no adjacent activity. - return focusedActivity; - } - - if (adjacentTopActivity.getLastWindowCreateTime() - < focusedActivity.getLastWindowCreateTime()) { - // Return if the current focus activity has more recently active window. - return focusedActivity; - } - - return adjacentTopActivity; - } - // Find the adjacent activity with more recently active window. final ActivityRecord[] mostRecentActiveActivity = { focusedActivity }; final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() }; @@ -9461,20 +9437,15 @@ public class WindowManagerService extends IWindowManager.Stub // No adjacent window. return false; } - final TaskFragment adjacentFragment; - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (fromFragment.getAdjacentTaskFragments().size() > 2) { - throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs"); - } - final TaskFragment[] tmpAdjacent = new TaskFragment[1]; - fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> { - tmpAdjacent[0] = adjacentTF; - return true; - }); - adjacentFragment = tmpAdjacent[0]; - } else { - adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (fromFragment.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs"); } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> { + tmpAdjacent[0] = adjacentTF; + return true; + }); + final TaskFragment adjacentFragment = tmpAdjacent[0]; if (adjacentFragment.isIsolatedNav()) { // Don't move the focus if the adjacent TF is isolated navigation. return false; diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index ea1f35a130b0..a012ec137892 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1671,13 +1671,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if (!taskFragment.isAdjacentTo(secondaryTaskFragment)) { // Only have lifecycle effect if the adjacent changed. - if (Flags.allowMultipleAdjacentTaskFragments()) { - // Activity Embedding only set two TFs adjacent. - taskFragment.setAdjacentTaskFragments( - new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment)); - } else { - taskFragment.setAdjacentTaskFragment(secondaryTaskFragment); - } + // Activity Embedding only set two TFs adjacent. + taskFragment.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment)); effects |= TRANSACT_EFFECTS_LIFECYCLE; } @@ -2220,30 +2216,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer()); - if (wc1 == null || !wc1.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1); - return TRANSACT_EFFECTS_NONE; - } - final TaskFragment root1 = wc1.asTaskFragment(); - final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot()); - if (wc2 == null || !wc2.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2); - return TRANSACT_EFFECTS_NONE; - } - final TaskFragment root2 = wc2.asTaskFragment(); - if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) { - throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by" - + " organizer root1=" + root1 + " root2=" + root2); - } - if (root1.isAdjacentTo(root2)) { - return TRANSACT_EFFECTS_NONE; - } - root1.setAdjacentTaskFragment(root2); - return TRANSACT_EFFECTS_LIFECYCLE; - } - final IBinder[] containers = hop.getContainers(); final ArraySet<TaskFragment> adjacentRoots = new ArraySet<>(); for (IBinder container : containers) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 0ad976c38565..51ed6bb2aa40 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3806,9 +3806,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Update user switcher message to activity manager. ActivityManagerInternal activityManagerInternal = mInjector.getActivityManagerInternal(); - activityManagerInternal.setSwitchingFromSystemUserMessage( + int deviceOwnerUserId = UserHandle.getUserId(deviceOwner.getUid()); + activityManagerInternal.setSwitchingFromUserMessage(deviceOwnerUserId, deviceOwner.startUserSessionMessage); - activityManagerInternal.setSwitchingToSystemUserMessage( + activityManagerInternal.setSwitchingToUserMessage(deviceOwnerUserId, deviceOwner.endUserSessionMessage); } @@ -19716,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingFromSystemUserMessage(startUserSessionMessageString); + .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString); } @Override @@ -19741,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingToSystemUserMessage(endUserSessionMessageString); + .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString); } @Override diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java index 586ff52aa78c..7c239ef02e58 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java @@ -328,7 +328,6 @@ public class TestDreamEnvironment { case DREAM_STATE_STARTED -> startDream(); case DREAM_STATE_WOKEN -> wakeDream(); } - mTestableLooper.processAllMessages(); } while (mCurrentDreamState < state); return true; diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index 49c37f163ff2..241ffdc19ce4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -36,24 +36,29 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; +import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.internal.R; import org.junit.AfterClass; import org.junit.Before; @@ -70,10 +75,11 @@ import java.io.File; import java.io.IOException; import java.util.Comparator; import java.util.List; +import java.util.Set; /** * Unit tests for the most important helpers of {@link WallpaperCropper}, in particular - * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}. + * {@link WallpaperCropper#getCrop(Point, WallpaperDefaultDisplayInfo, Point, SparseArray, boolean)}. */ @Presubmit @RunWith(AndroidJUnit4.class) @@ -83,6 +89,12 @@ public class WallpaperCropperTest { @Mock private WallpaperDisplayHelper mWallpaperDisplayHelper; + + @Mock + private WindowManager mWindowManager; + + @Mock + private Resources mResources; private WallpaperCropper mWallpaperCropper; private static final Point PORTRAIT_ONE = new Point(500, 800); @@ -175,14 +187,21 @@ public class WallpaperCropperTest { return tempDir; } - private void setUpWithDisplays(List<Point> displaySizes) { + private WallpaperDefaultDisplayInfo setUpWithDisplays(List<Point> displaySizes) { mDisplaySizes = new SparseArray<>(); displaySizes.forEach(size -> { mDisplaySizes.put(getOrientation(size), size); Point rotated = new Point(size.y, size.x); mDisplaySizes.put(getOrientation(rotated), rotated); }); + Set<WindowMetrics> windowMetrics = new ArraySet<>(); + for (Point displaySize : displaySizes) { + windowMetrics.add( + new WindowMetrics(new Rect(0, 0, displaySize.x, displaySize.y), + new WindowInsets.Builder().build())); + } when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())).thenReturn(windowMetrics); if (displaySizes.size() == 2) { Point largestDisplay = displaySizes.stream().max( Comparator.comparingInt(p -> p.x * p.y)).get(); @@ -192,11 +211,16 @@ public class WallpaperCropperTest { mFolded = getOrientation(smallestDisplay); mUnfoldedRotated = getRotatedOrientation(mUnfolded); mFoldedRotated = getRotatedOrientation(mFolded); + // foldable + doReturn(new int[]{0}).when(mResources).getIntArray(R.array.config_foldedDeviceStates); + } else { + // no foldable + doReturn(new int[]{}).when(mResources).getIntArray(R.array.config_foldedDeviceStates); } - doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0))) - .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt()); - doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0))) - .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt()); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + when(mWallpaperDisplayHelper.getDefaultDisplayInfo()).thenReturn(defaultDisplayInfo); + return defaultDisplayInfo; } private int getFoldedOrientation(int orientation) { @@ -435,7 +459,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_noSuggestedCrops() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); SparseArray<Rect> suggestedCrops = new SparseArray<>(); @@ -455,8 +479,9 @@ public class WallpaperCropperTest { for (boolean rtl : List.of(false, true)) { Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize) : leftOf(bitmapRect, expectedCropSize); - assertThat(mWallpaperCropper.getCrop( - displaySize, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop( + displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl)) .isEqualTo(expectedCrop); } } @@ -469,7 +494,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_hasSuggestedCrop() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); SparseArray<Rect> suggestedCrops = new SparseArray<>(); suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800)); @@ -479,11 +504,13 @@ public class WallpaperCropperTest { } for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - new Point(300, 800), bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT)); - assertThat(mWallpaperCropper.getCrop( - new Point(500, 800), bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(new Rect(0, 0, 500, 800)); } } @@ -499,7 +526,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_hasRotatedSuggestedCrop() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(2000, 1800); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); SparseArray<Rect> suggestedCrops = new SparseArray<>(); @@ -510,12 +537,14 @@ public class WallpaperCropperTest { suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait)); suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape)); for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - landscape, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(landscape, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(centerOf(bitmapRect, landscape)); - assertThat(mWallpaperCropper.getCrop( - squarePortrait, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(squarePortrait, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(centerOf(bitmapRect, squarePortrait)); } } @@ -532,7 +561,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasUnfoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2400); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); @@ -569,8 +598,9 @@ public class WallpaperCropperTest { expectedCrop.right = Math.min( unfoldedCrop.right, unfoldedCrop.right + maxParallax); } - assertThat(mWallpaperCropper.getCrop( - foldedDisplay, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(foldedDisplay, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(expectedCrop); } } @@ -588,7 +618,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasFoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); @@ -610,12 +640,14 @@ public class WallpaperCropperTest { Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo); for (boolean rtl : List.of(false, true)) { - assertThat(centerOf(mWallpaperCropper.getCrop( - unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne)) + assertThat(centerOf( + WallpaperCropper.getCrop(unfoldedDisplayOne, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl), foldedDisplayOne)) .isEqualTo(foldedCropOne); - assertThat(centerOf(mWallpaperCropper.getCrop( - unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo)) + assertThat(centerOf( + WallpaperCropper.getCrop(unfoldedDisplayTwo, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl), foldedDisplayTwo)) .isEqualTo(foldedCropTwo); } } @@ -633,7 +665,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); Point largestDisplay = displaySizes.stream().max( @@ -650,8 +682,9 @@ public class WallpaperCropperTest { Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded); for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(rotatedFoldedDisplay, defaultDisplayInfo, + bitmapSize, suggestedCrops, rtl)) .isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)); } } @@ -670,7 +703,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasRotatedFoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); @@ -689,8 +722,8 @@ public class WallpaperCropperTest { Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded); for (boolean rtl : List.of(false, true)) { - Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop( - rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl); + Rect rotatedUnfoldedCrop = WallpaperCropper.getCrop(rotatedUnfoldedDisplay, + defaultDisplayInfo, bitmapSize, suggestedCrops, rtl); assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)) .isEqualTo(rotatedFoldedCrop); } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java new file mode 100644 index 000000000000..312db91afb12 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java @@ -0,0 +1,340 @@ +/* + * 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.wallpaper; + +import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.util.SparseArray; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.server.wallpaper.WallpaperDefaultDisplayInfo.FoldableOrientations; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.Set; + +/** Unit tests for {@link WallpaperDefaultDisplayInfo}. */ +@Presubmit +@RunWith(AndroidJUnit4.class) +public class WallpaperDefaultDisplayInfoTest { + @Mock + private WindowManager mWindowManager; + + @Mock + private Resources mResources; + + @Before + public void setUp() { + initMocks(this); + } + + @Test + public void defaultDisplayInfo_foldable_shouldHaveExpectedContent() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1080, 2424)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2424, 1080)); + displaySizes.put(ORIENTATION_SQUARE_PORTRAIT, new Point(2076, 2152)); + displaySizes.put(ORIENTATION_SQUARE_LANDSCAPE, new Point(2152, 2076)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isTrue(); + assertThat(defaultDisplayInfo.isLargeScreen).isFalse(); + assertThat(defaultDisplayInfo.foldableOrientations).containsExactly( + new FoldableOrientations( + /* foldedOrientation= */ ORIENTATION_PORTRAIT, + /* unfoldedOrientation= */ ORIENTATION_SQUARE_PORTRAIT), + new FoldableOrientations( + /* foldedOrientation= */ ORIENTATION_LANDSCAPE, + /* unfoldedOrientation= */ ORIENTATION_SQUARE_LANDSCAPE)); + } + + @Test + public void defaultDisplayInfo_tablet_shouldHaveExpectedContent() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1600, 2560)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2560, 1600)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isFalse(); + assertThat(defaultDisplayInfo.isLargeScreen).isTrue(); + assertThat(defaultDisplayInfo.foldableOrientations).isEmpty(); + } + + @Test + public void defaultDisplayInfo_phone_shouldHaveExpectedContent() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 1280, 2856); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 3f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1280, 2856)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2856, 1280)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isFalse(); + assertThat(defaultDisplayInfo.isLargeScreen).isFalse(); + assertThat(defaultDisplayInfo.foldableOrientations).isEmpty(); + } + + @Test + public void defaultDisplayInfo_equals_sameContent_shouldEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo).isEqualTo(otherDefaultDisplayInfo); + } + + @Test + public void defaultDisplayInfo_equals_differentBounds_shouldNotEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + // For the first call + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)) + // For the second+ call + .thenReturn(Set.of(innerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo).isNotEqualTo(otherDefaultDisplayInfo); + } + + @Test + public void defaultDisplayInfo_hashCode_sameContent_shouldEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.hashCode()).isEqualTo(otherDefaultDisplayInfo.hashCode()); + } + + @Test + public void defaultDisplayInfo_hashCode_differentBounds_shouldNotEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + // For the first call + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)) + // For the second+ call + .thenReturn(Set.of(innerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.hashCode()).isNotEqualTo(otherDefaultDisplayInfo.hashCode()); + } + + @Test + public void getFoldedOrientation_foldable_shouldReturnExpectedOrientation() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_PORTRAIT); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_LANDSCAPE); + // Use a folded orientation for a folded orientation should return unknown. + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getUnfoldedOrientation_foldable_shouldReturnExpectedOrientation() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_SQUARE_PORTRAIT); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_SQUARE_LANDSCAPE); + // Use an unfolded orientation for an unfolded orientation should return unknown. + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getFoldedOrientation_nonFoldable_shouldReturnUnknown() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getUnFoldedOrientation_nonFoldable_shouldReturnUnknown() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 69877c372442..ea25e7992dd9 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -43,6 +43,7 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.server.accessibility.AccessibilityTraceManager; +import com.android.server.accessibility.BaseEventStreamTransformation; import org.junit.After; import org.junit.Before; @@ -70,6 +71,19 @@ public class AutoclickControllerTest { @Mock private WindowManager mMockWindowManager; private AutoclickController mController; + private static class MotionEventCaptor extends BaseEventStreamTransformation { + public MotionEvent downEvent; + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + downEvent = event; + break; + } + } + } + @Before public void setUp() { mTestableLooper = TestableLooper.get(this); @@ -713,6 +727,100 @@ public class AutoclickControllerTest { assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse(); } + @Test + public void sendClick_clickType_leftClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify left click sent. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_PRIMARY); + } + + @Test + public void sendClick_clickType_rightClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Set click type to right click. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify right click sent. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_SECONDARY); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Set click type to right click. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK); + // Set mouse to hover panel. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isHovered()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify left click is sent due to the mouse hovering the panel. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_PRIMARY); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index e3e9cc426bb3..08b0077c49b3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -797,7 +797,7 @@ public class ActivityStarterTests extends WindowTestsBase { // Create adjacent tasks and put one activity under it final Task parent = new TaskBuilder(mSupervisor).build(); final Task adjacentParent = new TaskBuilder(mSupervisor).build(); - parent.setAdjacentTaskFragment(adjacentParent); + parent.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(parent, adjacentParent)); final ActivityRecord activity = new ActivityBuilder(mAtm) .setParentTask(parent) .setCreateTask(true).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index a9be47d71213..86d901b640ff 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -488,14 +488,13 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask, WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); - tf1.setAdjacentTaskFragment(tf2); + tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2)); assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); } @Test - @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS}) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() { final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask, diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 8fe08553db95..cb98b9a490d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -243,6 +243,11 @@ class AppCompatActivityRobot { .getAspectRatioOverrides()).getUserMinAspectRatio(); } + void setShouldRefreshActivityForCameraCompat(boolean enabled) { + doReturn(enabled).when(mActivityStack.top().mAppCompatController.getCameraOverrides()) + .shouldRefreshActivityForCameraCompat(); + } + void setIgnoreOrientationRequest(boolean enabled) { mDisplayContent.setIgnoreOrientationRequest(enabled); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java index 05f6ed644632..7ef85262dfc2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java @@ -64,6 +64,19 @@ class AppCompatConfigurationRobot { .isCameraCompatTreatmentEnabledAtBuildTime(); } + void setCameraCompatAspectRatio(float aspectRatio) { + doReturn(aspectRatio).when(mAppCompatConfiguration).getCameraCompatAspectRatio(); + } + + void enableCameraCompatRefresh(boolean enabled) { + doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatRefreshEnabled(); + } + + void enableCameraCompatRefreshCycleThroughStop(boolean enabled) { + doReturn(enabled).when(mAppCompatConfiguration) + .isCameraCompatRefreshCycleThroughStopEnabled(); + } + void enableUserAppAspectRatioFullscreen(boolean enabled) { doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index bdee3c323549..dd3e9fcbbdaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -343,8 +343,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { // Adjacent + no companion => unable to predict // TF1 | TF2 - tf1.setAdjacentTaskFragment(tf2); - tf2.setAdjacentTaskFragment(tf1); + tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2)); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.isEmpty()); @@ -393,8 +392,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { // Adjacent => predict for previous activity. // TF2 | TF3 // TF1 - tf2.setAdjacentTaskFragment(tf3); - tf3.setAdjacentTaskFragment(tf2); + tf2.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf2, tf3)); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.contains(prevAr)); @@ -657,8 +655,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer); final ActivityRecord primaryActivity = primaryTf.getTopMostActivity(); final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity(); - primaryTf.setAdjacentTaskFragment(secondaryTf); - secondaryTf.setAdjacentTaskFragment(primaryTf); + primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf)); final WindowState primaryWindow = mock(WindowState.class); final WindowState secondaryWindow = mock(WindowState.class); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index f5bec04a98d5..6f959812d742 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -21,13 +21,13 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT; +import static android.app.CameraCompatTaskInfo.FreeformCameraCompatMode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT; -import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; @@ -40,18 +40,15 @@ import static android.view.Surface.ROTATION_90; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -59,13 +56,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.annotation.NonNull; -import android.app.CameraCompatTaskInfo; import android.app.IApplicationThread; import android.app.WindowConfiguration.WindowingMode; import android.app.servertransaction.RefreshCallbackItem; import android.app.servertransaction.ResumeActivityItem; import android.compat.testing.PlatformCompatChangeRule; -import android.content.ComponentName; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; @@ -73,17 +68,16 @@ import android.content.res.Configuration.Orientation; import android.graphics.Rect; import android.hardware.camera2.CameraManager; import android.os.Handler; +import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; -import android.view.DisplayInfo; import android.view.Surface; import androidx.test.filters.SmallTest; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -91,6 +85,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Tests for {@link CameraCompatFreeformPolicy}. @@ -109,30 +104,18 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests"; private static final String TEST_PACKAGE_2 = "com.test.package.two"; private static final String CAMERA_ID_1 = "camera-1"; - private AppCompatConfiguration mAppCompatConfiguration; - - private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; - private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; - private ActivityRecord mActivity; - - // TODO(b/384465100): use a robot structure. - @Before - public void setUp() throws Exception { - setupAppCompatConfiguration(); - setupCameraManager(); - setupHandler(); - doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); - } @Test @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testFeatureDisabled_cameraCompatFreeformPolicyNotCreated() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertNull(mCameraCompatFreeformPolicy); + robot.checkCameraCompatPolicyNotCreated(); + }); } @Test @@ -140,31 +123,37 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION}) public void testIsCameraRunningAndWindowingModeEligible_disabledViaOverride_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_cameraNotRunning_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @@ -172,64 +161,76 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(true); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testIsCameraRunningAndWindowingModeEligible_freeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) public void testIsFreeformLetterboxingForCameraAllowed_optInMechanism_notOptedIn_retFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testIsFreeformLetterboxingForCameraAllowed_notOptedOut_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @@ -237,519 +238,603 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testFullscreen_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - doReturn(false).when(mActivity).inFreeformWindowingMode(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + robot.setInFreeformWindowingMode(false); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testNoCameraConnection_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertNotInCameraCompatMode(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_0); + public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_0); + public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_270); + public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testCameraReconnected_cameraCompatModeAndRefresh() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true, /* lastLetterbox= */ false); - assertActivityRefreshRequested(/* refreshRequested */ true); - onCameraClosed(CAMERA_ID_1); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - // Activity is letterboxed from the previous configuration change. - callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, - /* lastLetterbox= */ true); + robot.assertActivityRefreshRequested(/* refreshRequested */ true); + robot.onCameraClosed(CAMERA_ID_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + // Activity is letterboxed from the previous configuration change. + robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true, + /* lastLetterbox= */ true); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ true); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(false); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testShouldApplyCameraCompatFreeformTreatment_notOptedOut_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT) public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mActivity.info - .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)); - assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ false); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + robot.checkShouldRefreshActivity(/* expected= */ true, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0), + robot.createConfiguration(/* letterbox= */ false, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - oldConfiguration.windowConfiguration.setDisplayRotation(0); - newConfiguration.windowConfiguration.setDisplayRotation(90); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.checkShouldRefreshActivity(/* expected= */ true, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 90), + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - oldConfiguration.windowConfiguration.setDisplayRotation(0); - newConfiguration.windowConfiguration.setDisplayRotation(0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.checkShouldRefreshActivity(/* expected= */ false, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0), + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() - throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - - doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides()) - .shouldRefreshActivityForCameraCompat(); + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().setShouldRefreshActivityForCameraCompat(false); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { - when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) - .thenReturn(false); + public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.conf().enableCameraCompatRefreshCycleThroughStop(false); - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); - - assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ true, + /* cycleThroughStop */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() - throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) - .shouldRefreshActivityViaPauseForCameraCompat(); + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.setShouldRefreshActivityViaPause(true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ true, + /* cycleThroughStop */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() { - configureActivity(SCREEN_ORIENTATION_FULL_USER); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_FULL_USER); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - final float configAspectRatio = 1.5f; - mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + final float configAspectRatio = 1.5f; + robot.conf().setCameraCompatAspectRatio(configAspectRatio); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(configAspectRatio, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(configAspectRatio); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - final float configAspectRatio = 1.5f; - mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); - doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) - .isOverrideMinAspectRatioForCameraEnabled(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.conf().setCameraCompatAspectRatio(1.5f); + robot.setOverrideMinAspectRatioEnabled(true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() throws - Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - // This is a portrait rotation for a device with portrait natural orientation (most common, - // currently the only one supported). - assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0); + // This is a portrait rotation for a device with portrait natural orientation (most + // common, currently the only one supported). + robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() throws - Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_0); - - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - - // This is a landscape rotation for a device with portrait natural orientation (most common, - // currently the only one supported). - assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90); - } - - private void setupAppCompatConfiguration() { - mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; - spyOn(mAppCompatConfiguration); - when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()) - .thenReturn(false); - when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) - .thenReturn(true); - } - - private void setupCameraManager() { - final CameraManager mockCameraManager = mock(CameraManager.class); - doAnswer(invocation -> { - mCameraAvailabilityCallback = invocation.getArgument(1); - return null; - }).when(mockCameraManager).registerAvailabilityCallback( - any(Executor.class), any(CameraManager.AvailabilityCallback.class)); - - when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager); - } - - private void setupHandler() { - final Handler handler = mDisplayContent.mWmService.mH; - spyOn(handler); - - when(handler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( - invocation -> { - ((Runnable) invocation.getArgument(0)).run(); - return null; - }); - } - - private void configureActivity(@ScreenOrientation int activityOrientation) { - configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); - } - - private void configureActivity(@ScreenOrientation int activityOrientation, - @WindowingMode int windowingMode) { - configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode); - } - - private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, - @Orientation int naturalOrientation, @WindowingMode int windowingMode) { - setupDisplayContent(naturalOrientation); - final Task task = setupTask(windowingMode); - setupActivity(task, activityOrientation, windowingMode); - setupMockApplicationThread(); - - mCameraCompatFreeformPolicy = mDisplayContent.mAppCompatCameraPolicy - .mCameraCompatFreeformPolicy; - } - - private void setupDisplayContent(@Orientation int naturalOrientation) { - // Create a new DisplayContent so that the flag values create the camera freeform policy. - mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(), - mDisplayContent.getSurfaceHeight()).build(); - mDisplayContent.setIgnoreOrientationRequest(true); - setDisplayRotation(ROTATION_90); - doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); - } - - private Task setupTask(@WindowingMode int windowingMode) { - final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea(); - spyOn(tda); - doReturn(true).when(tda).supportsNonResizableMultiWindow(); - - final Task task = new TaskBuilder(mSupervisor) - .setDisplay(mDisplayContent) - .setWindowingMode(windowingMode) - .build(); - task.setBounds(0, 0, 1000, 500); - return task; - } - - private void setupActivity(@NonNull Task task, @ScreenOrientation int activityOrientation, - @WindowingMode int windowingMode) { - mActivity = new ActivityBuilder(mAtm) - // Set the component to be that of the test class in order to enable compat changes - .setComponent(ComponentName.createRelative(mContext, - com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName())) - .setScreenOrientation(activityOrientation) - .setResizeMode(RESIZE_MODE_RESIZEABLE) - .setCreateTask(true) - .setOnTop(true) - .setTask(task) - .build(); - mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); - - spyOn(mActivity.mAppCompatController.getCameraOverrides()); - spyOn(mActivity.info); - - doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); - doReturn(windowingMode == WINDOWING_MODE_FREEFORM).when(mActivity) - .inFreeformWindowingMode(); - } - - private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { - mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName); - waitHandlerIdle(mDisplayContent.mWmService.mH); - } - - private void onCameraClosed(@NonNull String cameraId) { - mCameraAvailabilityCallback.onCameraClosed(cameraId); - waitHandlerIdle(mDisplayContent.mWmService.mH); - } - - private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) { - assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity)); - } - - private void assertNotInCameraCompatMode() { - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE); - } - - private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception { - assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); - } - - private void assertActivityRefreshRequested(boolean refreshRequested, - boolean cycleThroughStop) throws Exception { - verify(mActivity.mAppCompatController.getCameraOverrides(), - times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true); - - final RefreshCallbackItem refreshCallbackItem = - new RefreshCallbackItem(mActivity.token, cycleThroughStop ? ON_STOP : ON_PAUSE); - final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token, - /* isForward */ false, /* shouldSendCompatFakeFocus */ false); - - verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0)) - .scheduleTransactionItems(mActivity.app.getThread(), - refreshCallbackItem, resumeActivityItem); - } - - private void callOnActivityConfigurationChanging(ActivityRecord activity) { - callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true, - /* lastLetterbox= */false); - } - - private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew, - boolean lastLetterbox) { - mDisplayContent.mAppCompatCameraPolicy.mActivityRefresher - .onActivityConfigurationChanging(activity, - /* newConfig */ createConfiguration(letterboxNew), - /* lastReportedConfig */ createConfiguration(lastLetterbox)); - } - - private Configuration createConfiguration(boolean letterbox) { - final Configuration configuration = new Configuration(); - Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600) - : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600); - configuration.windowConfiguration.setAppBounds(bounds); - return configuration; - } - - private void setDisplayRotation(@Surface.Rotation int displayRotation) { - doAnswer(invocation -> { - DisplayInfo displayInfo = new DisplayInfo(); - mDisplayContent.getDisplay().getDisplayInfo(displayInfo); - displayInfo.rotation = displayRotation; - // Set height so that the natural orientation (rotation is 0) is portrait. This is the - // case for most standard phones and tablets. - // TODO(b/365725400): handle landscape natural orientation. - displayInfo.logicalHeight = displayRotation % 180 == 0 ? 800 : 600; - displayInfo.logicalWidth = displayRotation % 180 == 0 ? 600 : 800; - return displayInfo; - }).when(mDisplayContent.mWmService.mDisplayManagerInternal) - .getDisplayInfo(anyInt()); - } - - private void setupMockApplicationThread() { - IApplicationThread mockApplicationThread = mock(IApplicationThread.class); - spyOn(mActivity.app); - doReturn(mockApplicationThread).when(mActivity.app).getThread(); - } - - private void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int - expectedRotation) throws Exception { - final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor = - ArgumentCaptor.forClass(CompatibilityInfo.class); - verify(mActivity.app.getThread()).updatePackageCompatibilityInfo(eq(mActivity.packageName), - compatibilityInfoArgumentCaptor.capture()); - - final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue(); - assertTrue(compatInfo.isOverrideDisplayRotationRequired()); - assertEquals(expectedRotation, compatInfo.applicationDisplayRotation); + public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); + + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + // This is a landscape rotation for a device with portrait natural orientation (most + // common, currently the only one supported). + robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90); + }); + } + + /** + * Runs a test scenario providing a Robot. + */ + void runTestScenario(@NonNull Consumer<CameraCompatFreeformPolicyRobotTests> consumer) { + final CameraCompatFreeformPolicyRobotTests robot = + new CameraCompatFreeformPolicyRobotTests(mWm, mAtm, mSupervisor, this); + consumer.accept(robot); + } + + private static class CameraCompatFreeformPolicyRobotTests extends AppCompatRobotBase { + private final WindowTestsBase mWindowTestsBase; + + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + + CameraCompatFreeformPolicyRobotTests(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, + @NonNull ActivityTaskSupervisor supervisor, + @NonNull WindowTestsBase windowTestsBase) { + super(wm, atm, supervisor); + mWindowTestsBase = windowTestsBase; + setupCameraManager(); + setupAppCompatConfiguration(); + } + + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + if (displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy != null) { + spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy); + } + } + + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + setupCameraManager(); + setupHandler(); + setupMockApplicationThread(); + } + + private void setupMockApplicationThread() { + IApplicationThread mockApplicationThread = mock(IApplicationThread.class); + spyOn(activity().top().app); + doReturn(mockApplicationThread).when(activity().top().app).getThread(); + } + + private Configuration createConfiguration(boolean letterbox, int rotation) { + final Configuration configuration = createConfiguration(letterbox); + configuration.windowConfiguration.setDisplayRotation(rotation); + return configuration; + } + + private Configuration createConfiguration(boolean letterbox) { + final Configuration configuration = new Configuration(); + Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ + 600) + : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600); + configuration.windowConfiguration.setAppBounds(bounds); + return configuration; + } + + private void setupAppCompatConfiguration() { + applyOnConf((c) -> { + c.enableCameraCompatTreatment(true); + c.enableCameraCompatTreatmentAtBuildTime(true); + c.enableCameraCompatRefresh(true); + c.enableCameraCompatRefreshCycleThroughStop(true); + c.enableCameraCompatSplitScreenAspectRatio(false); + }); + } + + private void setupCameraManager() { + final CameraManager mockCameraManager = mock(CameraManager.class); + doAnswer(invocation -> { + mCameraAvailabilityCallback = invocation.getArgument(1); + return null; + }).when(mockCameraManager).registerAvailabilityCallback( + any(Executor.class), any(CameraManager.AvailabilityCallback.class)); + + doReturn(mockCameraManager).when(mWindowTestsBase.mWm.mContext).getSystemService( + CameraManager.class); + } + + private void setupHandler() { + final Handler handler = activity().top().mWmService.mH; + spyOn(handler); + + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(handler).postDelayed(any(Runnable.class), anyLong()); + } + + private void configureActivity(@ScreenOrientation int activityOrientation) { + configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); + } + + private void configureActivity(@ScreenOrientation int activityOrientation, + @WindowingMode int windowingMode) { + configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode); + } + + private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, + @Orientation int naturalOrientation, @WindowingMode int windowingMode) { + applyOnActivity(a -> { + dw().allowEnterDesktopMode(true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.setIgnoreOrientationRequest(true); + a.rotateDisplayForTopActivity(ROTATION_90); + a.configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1, + activityOrientation, /* isUnresizable */ false); + a.top().setWindowingMode(windowingMode); + a.displayContent().setWindowingMode(windowingMode); + a.setDisplayNaturalOrientation(naturalOrientation); + spyOn(a.top().mAppCompatController.getCameraOverrides()); + spyOn(a.top().info); + doReturn(a.displayContent().getDisplayInfo()).when( + a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo( + a.displayContent().mDisplayId); + }); + } + + private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { + mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName); + waitHandlerIdle(); + } + + private void onCameraClosed(@NonNull String cameraId) { + mCameraAvailabilityCallback.onCameraClosed(cameraId); + } + + private void waitHandlerIdle() { + mWindowTestsBase.waitHandlerIdle(activity().displayContent().mWmService.mH); + } + + void setInFreeformWindowingMode(boolean inFreeform) { + doReturn(inFreeform).when(activity().top()).inFreeformWindowingMode(); + } + + void setShouldRefreshActivityViaPause(boolean enabled) { + doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides()) + .shouldRefreshActivityViaPauseForCameraCompat(); + } + + void checkShouldRefreshActivity(boolean expected, Configuration newConfig, + Configuration oldConfig) { + assertEquals(expected, cameraCompatFreeformPolicy().shouldRefreshActivity( + activity().top(), newConfig, oldConfig)); + } + + void checkCameraCompatPolicyNotCreated() { + assertNull(cameraCompatFreeformPolicy()); + } + + void checkIsCameraRunningAndWindowingModeEligible(boolean expected) { + assertEquals(expected, cameraCompatFreeformPolicy() + .isCameraRunningAndWindowingModeEligible(activity().top())); + } + + void checkIsFreeformLetterboxingForCameraAllowed(boolean expected) { + assertEquals(expected, cameraCompatFreeformPolicy() + .isFreeformLetterboxingForCameraAllowed(activity().top())); + } + + void checkCameraCompatAspectRatioEquals(float aspectRatio) { + assertEquals(aspectRatio, + cameraCompatFreeformPolicy().getCameraCompatAspectRatio(activity().top()), + /* delta= */ 0.001); + } + + private void assertInCameraCompatMode(@FreeformCameraCompatMode int mode) { + assertEquals(mode, cameraCompatFreeformPolicy().getCameraCompatMode(activity().top())); + } + + private void assertNotInCameraCompatMode() { + assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE); + } + + private void assertActivityRefreshRequested(boolean refreshRequested) { + assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); + } + + private void assertActivityRefreshRequested(boolean refreshRequested, + boolean cycleThroughStop) { + verify(activity().top().mAppCompatController.getCameraOverrides(), + times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true); + + final RefreshCallbackItem refreshCallbackItem = + new RefreshCallbackItem(activity().top().token, + cycleThroughStop ? ON_STOP : ON_PAUSE); + final ResumeActivityItem resumeActivityItem = new ResumeActivityItem( + activity().top().token, + /* isForward */ false, /* shouldSendCompatFakeFocus */ false); + try { + verify(activity().top().mAtmService.getLifecycleManager(), + times(refreshRequested ? 1 : 0)) + .scheduleTransactionItems(activity().top().app.getThread(), + refreshCallbackItem, resumeActivityItem); + } catch (RemoteException e) { + fail(e.getMessage()); + } + } + + private void callOnActivityConfigurationChanging() { + callOnActivityConfigurationChanging(/* letterboxNew= */ true, + /* lastLetterbox= */false); + } + + private void callOnActivityConfigurationChanging(boolean letterboxNew, + boolean lastLetterbox) { + activity().displayContent().mAppCompatCameraPolicy.mActivityRefresher + .onActivityConfigurationChanging(activity().top(), + /* newConfig */ createConfiguration(letterboxNew), + /* lastReportedConfig */ createConfiguration(lastLetterbox)); + } + + void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) { + assertEquals(active, + cameraCompatFreeformPolicy().isTreatmentEnabledForActivity(activity().top(), + /* checkOrientation */ true)); + } + + void setOverrideMinAspectRatioEnabled(boolean enabled) { + doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides()) + .isOverrideMinAspectRatioForCameraEnabled(); + } + + void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int + expectedRotation) { + final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor = + ArgumentCaptor.forClass(CompatibilityInfo.class); + try { + verify(activity().top().app.getThread()).updatePackageCompatibilityInfo( + eq(activity().top().packageName), + compatibilityInfoArgumentCaptor.capture()); + } catch (RemoteException e) { + fail(e.getMessage()); + } + + final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue(); + assertTrue(compatInfo.isOverrideDisplayRotationRequired()); + assertEquals(expectedRotation, compatInfo.applicationDisplayRotation); + } + + CameraCompatFreeformPolicy cameraCompatFreeformPolicy() { + return activity().displayContent().mAppCompatCameraPolicy.mCameraCompatFreeformPolicy; + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index bc37496d14a7..e87e107cd793 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE; @@ -157,7 +158,7 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX}) - public void testReturnsContinueIfVisibleFreeformTaskExists() { + public void testReturnsContinueIfFreeformTaskExists() { setupDesktopModeLaunchParamsModifier(); when(mTarget.isEnteringDesktopMode(any(), any(), any())).thenCallRealMethod(); @@ -165,7 +166,7 @@ public class DesktopModeLaunchParamsModifierTests extends final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true) .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); doReturn(existingFreeformTask.getRootActivity()).when(dc) - .getTopMostVisibleFreeformActivity(); + .getTopMostFreeformActivity(); final Task launchingTask = new TaskBuilder(mSupervisor).build(); launchingTask.onDisplayChanged(dc); @@ -269,6 +270,38 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES}) + public void testInheritTaskBoundsFromExistingInstanceIfClosing() { + setupDesktopModeLaunchParamsModifier(); + + final String packageName = "com.same.package"; + // Setup existing task. + final DisplayContent dc = spy(createNewDisplay()); + final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true) + .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build(); + existingFreeformTask.setBounds( + /* left */ 0, + /* top */ 0, + /* right */ 500, + /* bottom */ 500); + doReturn(existingFreeformTask.getRootActivity()).when(dc) + .getTopMostVisibleFreeformActivity(); + // Set up new instance of already existing task. By default multi instance is not supported + // so first instance will close. + final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName) + .setCreateActivity(true).build(); + launchingTask.onDisplayChanged(dc); + launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + + // New instance should inherit task bounds of old instance. + assertEquals(RESULT_DONE, + new CalculateRequestBuilder().setTask(launchingTask) + .setActivity(launchingTask.getRootActivity()).calculate()); + assertEquals(existingFreeformTask.getBounds(), mResult.mBounds); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java new file mode 100644 index 000000000000..1445a6982c60 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; + +import static junit.framework.Assert.assertFalse; + +import static org.junit.Assert.assertTrue; + +import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; + +import androidx.test.filters.MediumTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:DisplayCompatTests + */ +@MediumTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DisplayCompatTests extends WindowTestsBase { + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS) + @Test + public void testFixedMiscConfigurationWhenMovingToDisplay() { + // Create an app on the default display, at which point the restart menu isn't enabled. + final Task task = createTask(mDefaultDisplay); + final ActivityRecord activity = createActivityRecord(task); + assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + + // Move the app to a secondary display, and the restart menu must get enabled. + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + final DisplayContent secondaryDisplay = createNewDisplay(displayInfo); + task.reparent(secondaryDisplay.getDefaultTaskDisplayArea(), true); + assertTrue(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + + // Once the app gets restarted, the restart menu must be gone. + activity.restartProcessIfVisible(); + assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 71e34ef220d3..3c6a89842af9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -93,7 +93,7 @@ public class InsetsPolicyTest extends WindowTestsBase { final Task task1 = createTask(mDisplayContent); final Task task2 = createTask(mDisplayContent); - task1.setAdjacentTaskFragment(task2); + task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2)); final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app"); final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 986532ce5897..ec83c50e95aa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -87,7 +87,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); adjacentRootTask.mCreatedByOrganizer = true; final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask); Task actualRootTask = taskDisplayArea.getLaunchRootTask( @@ -113,7 +114,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { final Task adjacentRootTask = createTask( mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); adjacentRootTask.mCreatedByOrganizer = true; - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchRootTask(rootTask, new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD}); @@ -135,7 +137,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { adjacentRootTask.mCreatedByOrganizer = true; createActivityRecord(adjacentRootTask); final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask); final Task actualRootTask = taskDisplayArea.getLaunchRootTask( @@ -821,7 +824,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { adjacentRootTask.mCreatedByOrganizer = true; final Task candidateTask = createTaskInRootTask(rootTask, 0 /* userId*/); final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); // Verify the launch root with candidate task Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index ab76ae8e378a..76660bdc7355 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -784,7 +784,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setFragmentToken(fragmentToken2) .build(); mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); - mTaskFragment.setAdjacentTaskFragment(taskFragment2); + mTaskFragment.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(mTaskFragment, taskFragment2)); mTransaction.clearAdjacentTaskFragments(mFragmentToken); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, @@ -1267,7 +1268,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testTaskFragmentInPip_setAdjacentTaskFragment() { + public void testTaskFragmentInPip_setAdjacentTaskFragments() { setupTaskFragmentInPip(); spyOn(mWindowOrganizerController); @@ -1279,7 +1280,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), any(IllegalArgumentException.class)); - verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); + verify(mTaskFragment, never()).setAdjacentTaskFragments(any()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index cc2a76dcc9f2..7c1d7fec819b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -67,7 +67,6 @@ import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Rect; import android.os.Binder; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; import android.view.View; @@ -363,7 +362,7 @@ public class TaskFragmentTest extends WindowTestsBase { doReturn(true).when(primaryActivity).supportsPictureInPicture(); doReturn(false).when(secondaryActivity).supportsPictureInPicture(); - primaryTf.setAdjacentTaskFragment(secondaryTf); + primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf)); primaryActivity.setState(RESUMED, "test"); secondaryActivity.setState(RESUMED, "test"); @@ -390,7 +389,8 @@ public class TaskFragmentTest extends WindowTestsBase { task.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); taskFragment0.setBounds(taskFragmentBounds); - taskFragment0.setAdjacentTaskFragment(taskFragment1); + taskFragment0.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragment0, taskFragment1)); taskFragment0.setCompanionTaskFragment(taskFragment1); taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder() .setAnimationBackgroundColor(Color.GREEN) @@ -779,7 +779,7 @@ public class TaskFragmentTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setFragmentToken(new Binder()) .build(); - tf0.setAdjacentTaskFragment(tf1); + tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1)); tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -834,7 +834,7 @@ public class TaskFragmentTest extends WindowTestsBase { final Task task = createTask(mDisplayContent); final TaskFragment tf0 = createTaskFragmentWithActivity(task); final TaskFragment tf1 = createTaskFragmentWithActivity(task); - tf0.setAdjacentTaskFragment(tf1); + tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1)); tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -982,7 +982,8 @@ public class TaskFragmentTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setFragmentToken(new Binder()) .build(); - taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight); + taskFragmentLeft.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight)); taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -1051,8 +1052,8 @@ public class TaskFragmentTest extends WindowTestsBase { .setParentTask(task) .createActivityCount(1) .build(); - taskFragmentRight.setAdjacentTaskFragment(taskFragmentLeft); - taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight); + taskFragmentRight.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight)); final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity(); final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity(); @@ -1103,7 +1104,6 @@ public class TaskFragmentTest extends WindowTestsBase { Math.min(outConfig.screenWidthDp, outConfig.screenHeightDp)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testAdjacentSetForTaskFragments() { final Task task = createTask(mDisplayContent); @@ -1119,7 +1119,6 @@ public class TaskFragmentTest extends WindowTestsBase { () -> new TaskFragment.AdjacentSet(tf0, tf1, tf2)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testSetAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1148,7 +1147,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task2.hasAdjacentTaskFragment()); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testClearAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1167,7 +1165,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task2.hasAdjacentTaskFragment()); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1190,7 +1187,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task1.isAdjacentTo(task1)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragmentsWhenRemove() { final Task task0 = createTask(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 724d7e7c111c..044aacc4b988 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1750,8 +1750,7 @@ public class TaskTests extends WindowTestsBase { primary.mVisibleRequested = true; secondary.mVisibleRequested = true; - primary.setAdjacentTaskFragment(secondary); - secondary.setAdjacentTaskFragment(primary); + primary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primary, secondary)); primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK); doReturn(true).when(primary).shouldBoostDimmer(); task.assignChildLayers(t); 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 7030d986494f..ae6144713a1d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -929,7 +929,6 @@ public class WindowOrganizerTests extends WindowTestsBase { assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testSetAdjacentLaunchRootSet() { final DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 57ab13ffee89..471b065aebb4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -122,7 +122,6 @@ import android.window.WindowContainerTransaction; import com.android.internal.policy.AttributeCache; import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.FakeSettingsProvider; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; import org.junit.After; @@ -289,18 +288,6 @@ public class WindowTestsBase extends SystemServiceTestsBase { mAtm.mWindowManager.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false); - // Setup WallpaperController crop utils with a simple center-align strategy - WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> { - Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); - crop.scale(Math.min( - ((float) bitmapSize.x) / displaySize.x, - ((float) bitmapSize.y) / displaySize.y)); - crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2); - return crop; - }; - mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils); - mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils); - checkDeviceSpecificOverridesNotApplied(); } @@ -1890,7 +1877,7 @@ public class WindowTestsBase extends SystemServiceTestsBase { mSecondary = mService.mTaskOrganizerController.createRootTask( display, WINDOWING_MODE_MULTI_WINDOW, null); - mPrimary.setAdjacentTaskFragment(mSecondary); + mPrimary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(mPrimary, mSecondary)); display.getDefaultTaskDisplayArea().setLaunchAdjacentFlagRootTask(mSecondary); final Rect primaryBounds = new Rect(); diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt index 044f11d6904c..890c346ea015 100644 --- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt +++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt @@ -184,6 +184,8 @@ class BatteryControllerTests { @get:Rule val rule = MockitoJUnit.rule()!! @get:Rule + val context = TestableContext(ApplicationProvider.getApplicationContext()) + @get:Rule val inputManagerRule = MockInputManagerRule() @Mock @@ -194,7 +196,6 @@ class BatteryControllerTests { private lateinit var bluetoothBatteryManager: BluetoothBatteryManager private lateinit var batteryController: BatteryController - private lateinit var context: TestableContext private lateinit var testLooper: TestLooper private lateinit var devicesChangedListener: IInputDevicesChangedListener private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession @@ -202,7 +203,6 @@ class BatteryControllerTests { @Before fun setup() { - context = TestableContext(ApplicationProvider.getApplicationContext()) testLooper = TestLooper() val inputManager = InputManager(context) context.addMockSystemService(InputManager::class.java, inputManager) diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java index 5875520cd259..20528f23cc8c 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java @@ -23,7 +23,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.Context; import android.graphics.Rect; import android.hardware.input.InputManager; import android.testing.AndroidTestingRunner; @@ -60,9 +59,12 @@ public class TouchpadDebugViewControllerTests { private static final String TAG = "TouchpadDebugViewController"; @Rule + public final TestableContext mTestableContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - private Context mContext; private TouchpadDebugViewController mTouchpadDebugViewController; @Mock private InputManager mInputManagerMock; @@ -74,8 +76,6 @@ public class TouchpadDebugViewControllerTests { @Before public void setup() throws Exception { - mContext = InstrumentationRegistry.getInstrumentation().getContext(); - TestableContext mTestableContext = new TestableContext(mContext); mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock); Rect bounds = new Rect(0, 0, 2560, 1600); diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java index 60fa52f85e34..1c366a134300 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.content.Context; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; @@ -51,6 +50,7 @@ import com.android.server.input.TouchpadHardwareProperties; import com.android.server.input.TouchpadHardwareState; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -70,6 +70,10 @@ public class TouchpadDebugViewTest { private TouchpadDebugView mTouchpadDebugView; private WindowManager.LayoutParams mWindowLayoutParams; + @Rule + public final TestableContext mTestableContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); + @Mock WindowManager mWindowManager; @Mock @@ -77,14 +81,10 @@ public class TouchpadDebugViewTest { Rect mWindowBounds; WindowMetrics mWindowMetrics; - TestableContext mTestableContext; @Before public void setUp() { MockitoAnnotations.initMocks(this); - Context context = InstrumentationRegistry.getInstrumentation().getContext(); - mTestableContext = new TestableContext(context); - mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); mTestableContext.addMockSystemService(InputManager.class, mInputManager); |