diff options
152 files changed, 4659 insertions, 1141 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index b92df4cf7884..a0547411cd9e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2950,6 +2950,7 @@ package android.app.supervision { @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent(); method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); + method @FlaggedApi("android.permission.flags.enable_system_supervision_role_behavior") @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public boolean shouldAllowBypassingSupervisionRoleQualification(); } } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index daa1902edf02..1e21991cd380 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -369,7 +369,7 @@ package android.app { } public final class NotificationChannel implements android.os.Parcelable { - method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @NonNull public android.app.NotificationChannel copy(); + method @NonNull public android.app.NotificationChannel copy(); method public int getOriginalImportance(); method public boolean isImportanceLockedByCriticalDeviceFunction(); method public void lockFields(int); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index d88395331656..c1d80c93cfd6 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -508,7 +508,6 @@ public final class NotificationChannel implements Parcelable { /** @hide */ @TestApi @NonNull - @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) public NotificationChannel copy() { NotificationChannel copy = new NotificationChannel(mId, mName, mImportance); copy.setDescription(mDesc); diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java index 92db8b329045..06b492c417d8 100644 --- a/core/java/android/app/NotificationChannelGroup.java +++ b/core/java/android/app/NotificationChannelGroup.java @@ -221,7 +221,10 @@ public final class NotificationChannelGroup implements Parcelable { * @hide */ public void setChannels(List<NotificationChannel> channels) { - mChannels = channels; + mChannels.clear(); + if (channels != null) { + mChannels.addAll(channels); + } } /** @@ -331,7 +334,9 @@ public final class NotificationChannelGroup implements Parcelable { NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName()); cloned.setDescription(getDescription()); cloned.setBlocked(isBlocked()); - cloned.setChannels(getChannels()); + for (NotificationChannel c : mChannels) { + cloned.addChannel(c.copy()); + } cloned.lockFields(mUserLockedFields); return cloned; } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 69e3ef9086d5..f24eb0a63b26 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1317,10 +1317,16 @@ public class NotificationManager { */ public List<NotificationChannel> getNotificationChannels() { if (Flags.nmBinderPerfCacheChannels()) { - return mNotificationChannelListCache.query(new NotificationChannelQuery( - mContext.getOpPackageName(), - mContext.getPackageName(), - mContext.getUserId())); + List<NotificationChannel> channelList = mNotificationChannelListCache.query( + new NotificationChannelQuery(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId())); + List<NotificationChannel> out = new ArrayList(); + if (channelList != null) { + for (NotificationChannel c : channelList) { + out.add(c.copy()); + } + } + return out; } else { INotificationManager service = service(); try { @@ -1343,7 +1349,7 @@ public class NotificationManager { } for (NotificationChannel channel : channels) { if (channelId.equals(channel.getId())) { - return channel; + return channel.copy(); } } return null; @@ -1364,12 +1370,12 @@ public class NotificationManager { for (NotificationChannel channel : channels) { if (conversationId.equals(channel.getConversationId()) && channelId.equals(channel.getParentChannelId())) { - return channel; + return channel.copy(); } else if (channelId.equals(channel.getId())) { parent = channel; } } - return parent; + return parent != null ? parent.copy() : null; } /** @@ -1405,8 +1411,9 @@ public class NotificationManager { new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId())); Map<String, NotificationChannelGroup> groupHeaders = mNotificationChannelGroupsCache.query(pkgName); - return NotificationChannelGroupsHelper.getGroupWithChannels(channelGroupId, channelList, - groupHeaders, /* includeDeleted= */ false); + NotificationChannelGroup ncg = NotificationChannelGroupsHelper.getGroupWithChannels( + channelGroupId, channelList, groupHeaders, /* includeDeleted= */ false); + return ncg != null ? ncg.clone() : null; } else { INotificationManager service = service(); try { @@ -1428,8 +1435,14 @@ public class NotificationManager { new NotificationChannelQuery(pkgName, pkgName, mContext.getUserId())); Map<String, NotificationChannelGroup> groupHeaders = mNotificationChannelGroupsCache.query(pkgName); - return NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders, - NotificationChannelGroupsHelper.Params.forAllGroups()); + List<NotificationChannelGroup> populatedGroupList = + NotificationChannelGroupsHelper.getGroupsWithChannels(channelList, groupHeaders, + NotificationChannelGroupsHelper.Params.forAllGroups()); + List<NotificationChannelGroup> out = new ArrayList<>(); + for (NotificationChannelGroup g : populatedGroupList) { + out.add(g.clone()); + } + return out; } else { INotificationManager service = service(); try { diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index 2f67a8abcd17..801162f3cbd3 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -27,4 +27,6 @@ interface ISupervisionManager { boolean isSupervisionEnabledForUser(int userId); void setSupervisionEnabledForUser(int userId, boolean enabled); String getActiveSupervisionAppPackage(int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS)") + boolean shouldAllowBypassingSupervisionRoleQualification(); } diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index 172ed2358a5d..76a789d3426f 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -19,6 +19,7 @@ package android.app.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_USERS; import static android.Manifest.permission.QUERY_USERS; +import static android.permission.flags.Flags.FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR; import android.annotation.FlaggedApi; import android.annotation.Nullable; @@ -193,4 +194,25 @@ public class SupervisionManager { } return null; } + + + /** + * @return {@code true} if bypassing the qualification is allowed for the specified role based + * on the current state of the device. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_ENABLE_SYSTEM_SUPERVISION_ROLE_BEHAVIOR) + @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) + public boolean shouldAllowBypassingSupervisionRoleQualification() { + if (mService != null) { + try { + return mService.shouldAllowBypassingSupervisionRoleQualification(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } } diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 9dd1fed4a85a..1249af7cc595 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -72,7 +72,8 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_ALL_APPS = 21; public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22; public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23; - public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24; + @Deprecated + public static final int DEPRECATED_KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24; public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25; public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26; public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27; @@ -167,7 +168,6 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_ALL_APPS, KEY_GESTURE_TYPE_LAUNCH_SEARCH, KEY_GESTURE_TYPE_LANGUAGE_SWITCH, - KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, KEY_GESTURE_TYPE_SYSTEM_MUTE, KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, @@ -525,8 +525,6 @@ public final class KeyGestureEvent { return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH; case KEY_GESTURE_TYPE_LANGUAGE_SWITCH: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH; - case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS; case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK; case KEY_GESTURE_TYPE_SYSTEM_MUTE: @@ -707,8 +705,6 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_LAUNCH_SEARCH"; case KEY_GESTURE_TYPE_LANGUAGE_SWITCH: return "KEY_GESTURE_TYPE_LANGUAGE_SWITCH"; - case KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - return "KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS"; case KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: return "KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK"; case KEY_GESTURE_TYPE_SYSTEM_MUTE: diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 84d96bd1e155..3d6da5452ad2 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -435,6 +435,11 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Cached value of {@link #canImeRenderGesturalNavButtons}, as it doesn't change at runtime. + */ + private final boolean mCanImeRenderGesturalNavButtons = canImeRenderGesturalNavButtons(); + + /** * Allows the system to optimize the back button affordance based on the presence of software * keyboard. * @@ -564,6 +569,9 @@ public class InputMethodService extends AbstractInputMethodService { private final NavigationBarController mNavigationBarController = new NavigationBarController(this); + /** Whether a custom IME Switcher button was requested to be visible. */ + private boolean mCustomImeSwitcherButtonRequestedVisible; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) int mTheme = 0; @@ -783,7 +791,7 @@ public class InputMethodService extends AbstractInputMethodService { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal"); mPrivOps.set(params.privilegedOperations); InputMethodPrivilegedOperationsRegistry.put(params.token, mPrivOps); - mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags); + onNavButtonFlagsChanged(params.navigationBarFlags); attachToken(params.token); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -893,7 +901,7 @@ public class InputMethodService extends AbstractInputMethodService { public final void dispatchStartInput(@Nullable InputConnection inputConnection, @NonNull IInputMethod.StartInputParams params) { mPrivOps.reportStartInputAsync(params.startInputToken); - mNavigationBarController.onNavButtonFlagsChanged(params.navigationBarFlags); + onNavButtonFlagsChanged(params.navigationBarFlags); if (params.restarting) { restartInput(inputConnection, params.editorInfo); } else { @@ -918,6 +926,20 @@ public class InputMethodService extends AbstractInputMethodService { @Override public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); + if (!mCanImeRenderGesturalNavButtons) { + final boolean showImeSwitcher = (navButtonFlags + & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) != 0; + // The IME cannot draw the IME nav bar, so this will never be visible. In this case + // the system nav bar hosts the IME buttons. + // The system nav bar will be hidden when the IME is shown and the config is set. + final boolean navBarNotVisible = getApplicationContext().getResources() + .getBoolean(com.android.internal.R.bool.config_hideNavBarForKeyboard); + final boolean visible = showImeSwitcher && navBarNotVisible; + if (visible != mCustomImeSwitcherButtonRequestedVisible) { + mCustomImeSwitcherButtonRequestedVisible = visible; + onCustomImeSwitcherButtonRequestedVisible(visible); + } + } } /** @@ -4473,28 +4495,27 @@ public class InputMethodService extends AbstractInputMethodService { /** * Called when the requested visibility of a custom IME Switcher button changes. * - * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher - * button inside this bar. However, the IME can request hiding the bar provided by the system - * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides - * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful, - * then it becomes the IME's responsibility to provide a custom IME Switcher button in its - * input view, with equivalent functionality.</p> + * <p>When this method is called with {@code true} by the system, the IME must show a button + * within its UI to switch IMEs. When it is called with {@code false}, it must hide this button. + * + * <p>Normally, the system provides a button for switching to a different IME when that is + * appropriate. Under certain circumstances, namely when the IME successfully asks to hide the + * system-provided navigation bar (with {@code getWindowInsetsController().hide(captionBar())}), + * providing this button is delegated to the IME through this callback. * - * <p>This custom button is only requested to be visible when the system provides the IME - * navigation bar, both the bar and the IME Switcher button inside it should be visible, - * but the IME successfully requested to hide the bar. This does not depend on the current - * visibility of the IME. It could be called with {@code true} while the IME is hidden, in - * which case the IME should prepare to show the button as soon as the IME itself is shown.</p> + * <p>This does not depend on the current visibility of the IME. It could be called with + * {@code true} while the IME is hidden, in which case the IME should prepare to show the button + * as soon as the IME itself is shown. * * <p>This is only called when the requested visibility changes. The default value is * {@code false} and as such, this will not be called initially if the resulting value is - * {@code false}.</p> + * {@code false}. * * <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently - * visible. However, this is not guaranteed to be called before the IME is shown, as it depends - * on when the IME requested hiding the IME navigation bar. If the request is sent during - * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after - * {@link #onWindowShown}, but before the first IME frame is drawn.</p> + * visible. However, this is not guaranteed to be called before the IME is shown, as it may + * depend on the IME requesting to hide the system-provided navigation bar. If the request is + * sent during the showing flow (e.g. during {@link #onStartInputView}), this will be called + * shortly after {@link #onWindowShown}, but before the first IME frame is drawn. * * @param visible whether the button is requested visible or not. */ @@ -4686,6 +4707,8 @@ public class InputMethodService extends AbstractInputMethodService { + " touchableRegion=" + mTmpInsets.touchableRegion); p.println(" mSettingsObserver=" + mSettingsObserver); p.println(" mNavigationBarController=" + mNavigationBarController.toDebugString()); + p.println(" mCustomImeSwitcherButtonRequestedVisible=" + + mCustomImeSwitcherButtonRequestedVisible); } private final ImeTracing.ServiceDumper mDumper = new ImeTracing.ServiceDumper() { diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 7da053d0010e..f1dee89b0b1d 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -170,6 +170,9 @@ final class NavigationBarController { private boolean mShouldShowImeSwitcherWhenImeIsShown; + /** Whether a custom IME Switcher button should be visible. */ + private boolean mCustomImeSwitcherButtonRequestedVisible; + @Appearance private int mAppearance; @@ -181,9 +184,6 @@ final class NavigationBarController { private boolean mDrawLegacyNavigationBarBackground; - /** Whether a custom IME Switcher button should be visible. */ - private boolean mCustomImeSwitcherVisible; - private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; @@ -275,7 +275,9 @@ final class NavigationBarController { // IME navigation bar. boolean visible = insets.isVisible(captionBar()); mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); - checkCustomImeSwitcherVisibility(); + checkCustomImeSwitcherButtonRequestedVisible( + mShouldShowImeSwitcherWhenImeIsShown, mImeDrawsImeNavBar, + !visible /* imeNavBarNotVisible */); } return view.onApplyWindowInsets(insets); }); @@ -502,33 +504,31 @@ final class NavigationBarController { mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; - checkCustomImeSwitcherVisibility(); - mService.mWindow.getWindow().getDecorView().getWindowInsetsController() .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar)); if (imeDrawsImeNavBar) { installNavigationBarFrameIfNecessary(); - if (mNavigationBarFrame == null) { - return; - } - if (mShouldShowImeSwitcherWhenImeIsShown - == prevShouldShowImeSwitcherWhenImeIsShown) { - return; - } - final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( - NavigationBarView.class::isInstance); - if (navigationBarView != null) { - // TODO(b/213337792): Support InputMethodService#setBackDisposition(). - // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. - final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE - | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); - navigationBarView.setNavbarFlags(flags); + if (mNavigationBarFrame != null && mShouldShowImeSwitcherWhenImeIsShown + != prevShouldShowImeSwitcherWhenImeIsShown) { + final NavigationBarView navigationBarView = mNavigationBarFrame + .findViewByPredicate(NavigationBarView.class::isInstance); + if (navigationBarView != null) { + // TODO(b/213337792): Support InputMethodService#setBackDisposition(). + // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. + final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE + | (mShouldShowImeSwitcherWhenImeIsShown + ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); + navigationBarView.setNavbarFlags(flags); + } } } else { uninstallNavigationBarFrameIfNecessary(); } + + // Check custom IME Switcher button visibility after (un)installing nav bar frame. + checkCustomImeSwitcherButtonRequestedVisible(shouldShowImeSwitcherWhenImeIsShown, + imeDrawsImeNavBar, !isShown() /* imeNavBarNotVisible */); } @Override @@ -631,22 +631,29 @@ final class NavigationBarController { } /** - * Checks if a custom IME Switcher button should be visible, and notifies the IME when this - * state changes. This can only be {@code true} if three conditions are met: + * Checks if a custom IME Switcher button should be requested visible, and notifies the IME + * when this state changes. This is only {@code true} when the IME Switcher button is + * requested visible, and the navigation bar is not requested visible. * - * <li>The IME should draw the IME navigation bar.</li> - * <li>The IME Switcher button should be visible when the IME is visible.</li> - * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li> + * @param buttonVisible whether the IME Switcher button is requested visible. + * @param shouldDrawImeNavBar whether the IME navigation bar should be drawn. + * @param imeNavBarNotVisible whether the IME navigation bar is not requested visible. This + * will be {@code true} if it is requested hidden or not + * installed. */ - private void checkCustomImeSwitcherVisibility() { + private void checkCustomImeSwitcherButtonRequestedVisible(boolean buttonVisible, + boolean shouldDrawImeNavBar, boolean imeNavBarNotVisible) { if (!Flags.imeSwitcherRevampApi()) { return; } - final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown - && mNavigationBarFrame != null && !isShown(); - if (visible != mCustomImeSwitcherVisible) { - mCustomImeSwitcherVisible = visible; - mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible); + // The system nav bar will be hidden when the IME is shown and the config is set. + final boolean navBarNotVisible = shouldDrawImeNavBar ? imeNavBarNotVisible + : mService.getResources().getBoolean( + com.android.internal.R.bool.config_hideNavBarForKeyboard); + final boolean visible = buttonVisible && navBarNotVisible; + if (visible != mCustomImeSwitcherButtonRequestedVisible) { + mCustomImeSwitcherButtonRequestedVisible = visible; + mService.onCustomImeSwitcherButtonRequestedVisible(visible); } } @@ -656,7 +663,8 @@ final class NavigationBarController { + " mNavigationBarFrame=" + mNavigationBarFrame + " mShouldShowImeSwitcherWhenImeIsShown=" + mShouldShowImeSwitcherWhenImeIsShown - + " mCustomImeSwitcherVisible=" + mCustomImeSwitcherVisible + + " mCustomImeSwitcherButtonRequestedVisible=" + + mCustomImeSwitcherButtonRequestedVisible + " mAppearance=0x" + Integer.toHexString(mAppearance) + " mDarkIntensity=" + mDarkIntensity + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 34272b17cf54..ef6f37ac6f9c 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -554,3 +554,12 @@ flag { description: "This flag is used to add role protection to READ_BLOCKED_NUMBERS for SYSTEM_UI_INTELLIGENCE" bug: "354758615" } + +flag { + name: "enable_system_supervision_role_behavior" + is_fixed_read_only: true + is_exported: true + namespace: "supervision" + description: "This flag is used to enable the role behavior for the system supervision role" + bug: "378102594" +} diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 237d8f96496f..4bf64954a380 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -135,8 +135,29 @@ interface IWindowManager int getDisplayIdByUniqueId(String uniqueId); @EnforcePermission("WRITE_SECURE_SETTINGS") void setForcedDisplayDensityForUser(int displayId, int density, int userId); + /** + * Clears forced density and forced density ratio in DisplayWindowSettings for the given + * displayId. + * + * @param displayId Id of the display. + * @param userId Id of the user. + */ @EnforcePermission("WRITE_SECURE_SETTINGS") void clearForcedDisplayDensityForUser(int displayId, int userId); + /** + * Sets display forced density ratio and forced density in DisplayWindowSettings for + * the given displayId. Ratio is used to update forced density to persist display size when + * resolution change happens. Use {@link #setForcedDisplayDensityForUser} when there is no need + * to handle resolution changes for the display. If setForcedDisplayDensityForUser is used after, + * this the ratio will be updated to use the last set forced density. Use + * {@link #clearForcedDisplayDensityForUser} to reset. + * + * @param displayId Id of the display. + * @param ratio The ratio of forced density to the default density. + * @param userId Id of the user. + */ + @EnforcePermission("WRITE_SECURE_SETTINGS") + void setForcedDisplayDensityRatio(int displayId, float ratio, int userId); /** * Sets settings for a specific user in a batch to minimize configuration updates. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c275ed3a3b06..b1676dde3b70 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -138,6 +138,7 @@ import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateO import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static com.android.window.flags.Flags.setScPropertiesInClient; +import static com.android.window.flags.Flags.fixViewRootCallTrace; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -1590,7 +1591,9 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); } - mAdded = true; + if (!fixViewRootCallTrace()) { + mAdded = true; + } int res; /* = WindowManagerImpl.ADD_OKAY; */ // Schedule the first layout -before- adding to the window @@ -1641,7 +1644,9 @@ public final class ViewRootImpl implements ViewParent, mTmpFrames.compatScale = compatScale[0]; mInvCompatScale = 1f / compatScale[0]; } catch (RemoteException | RuntimeException e) { - mAdded = false; + if (!fixViewRootCallTrace()) { + mAdded = false; + } mView = null; mAttachInfo.mRootView = null; mFallbackEventHandler.setView(null); @@ -1672,7 +1677,9 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow); if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; - mAdded = false; + if (!fixViewRootCallTrace()) { + mAdded = false; + } mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); @@ -1781,6 +1788,9 @@ public final class ViewRootImpl implements ViewParent, mFirstInputStage = nativePreImeStage; mFirstPostImeInputStage = earlyPostImeStage; mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix; + if (fixViewRootCallTrace()) { + mAdded = true; + } if (!mRemoved || !mAppVisible) { AnimationHandler.requestAnimatorsEnabled(mAppVisible, this); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 0d87b73a5e03..07882b6e5c67 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -1006,3 +1006,23 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_desktop_swipe_back_minimize_animation_bugfix" + namespace: "lse_desktop_experience" + description: "Enabling a minimize animation when a window is minimized via a swipe-back navigation gesture in Desktop Windowing mode." + bug: "359343764" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_desktop_close_task_animation_in_dtc_bugfix" + namespace: "lse_desktop_experience" + description: "Enables bugfix to handle close task animation within DesktopTasksController." + bug: "403345083" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 59dd32258d8c..99f9929e1071 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -388,6 +388,17 @@ flag { } flag { + name: "remove_depart_target_from_motion" + namespace: "windowing_frontend" + description: "Remove DepartingAnimationTarget from BackMotionEvent" + bug: "395035430" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "predictive_back_default_enable_sdk_36" namespace: "systemui" description: "Enable Predictive Back by default with targetSdk>=36" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index e2eb193293c9..f162b1f40d8e 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -163,3 +163,25 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "exclude_task_from_recents" + description: "Enables WCT to set whether the task should be excluded from the Recents list" + bug: "404726350" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "windowing_sdk" + name: "fix_view_root_call_trace" + description: "Do not set mAdded=true unless #setView finished successfully" + bug: "385705687" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 3d81e4fc7acd..e20a52b24485 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -120,7 +120,6 @@ import com.android.internal.view.menu.MenuHelper; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.BackgroundFallback; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; -import com.android.window.flags.Flags; import java.util.List; import java.util.concurrent.Executor; @@ -1004,8 +1003,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onWindowSystemUiVisibilityChanged(int visible) { updateColorViews(null /* insets */, true /* animate */); - if (!Flags.actionModeEdgeToEdge() - && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { + if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { updateStatusGuardColor(); } } @@ -1042,7 +1040,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } mFrameOffsets.set(insets.getSystemWindowInsetsAsRect()); insets = updateColorViews(insets, true /* animate */); - insets = updateActionModeInsets(insets); + insets = updateStatusGuard(insets); if (getForeground() != null) { drawableChanged(); } @@ -1594,7 +1592,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } - private WindowInsets updateActionModeInsets(WindowInsets insets) { + private WindowInsets updateStatusGuard(WindowInsets insets) { boolean showStatusGuard = false; // Show the status guard when the non-overlay contextual action bar is showing if (mPrimaryActionModeView != null) { @@ -1610,78 +1608,54 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final Rect rect = mTempRect; // Apply the insets that have not been applied by the contentParent yet. - final WindowInsets innerInsets = + WindowInsets innerInsets = mWindow.mContentParent.computeSystemWindowInsets(insets, rect); - final boolean consumesSystemWindowInsetsTop; - if (Flags.actionModeEdgeToEdge()) { - final Insets newPadding = innerInsets.getSystemWindowInsets(); - final Insets newMargin = innerInsets.getInsets( - WindowInsets.Type.navigationBars()); - - // Don't extend into navigation bar area so the width can align with status - // bar color view. - if (mlp.leftMargin != newMargin.left - || mlp.rightMargin != newMargin.right) { - mlpChanged = true; - mlp.leftMargin = newMargin.left; - mlp.rightMargin = newMargin.right; - } - - mPrimaryActionModeView.setPadding( - newPadding.left - newMargin.left, - newPadding.top, - newPadding.right - newMargin.right, - 0); - consumesSystemWindowInsetsTop = newPadding.top > 0; - } else { - int newTopMargin = innerInsets.getSystemWindowInsetTop(); - int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); - int newRightMargin = innerInsets.getSystemWindowInsetRight(); - - // Must use root window insets for the guard, because the color views - // consume the navigation bar inset if the window does not request - // LAYOUT_HIDE_NAV - but the status guard is attached at the root. - WindowInsets rootInsets = getRootWindowInsets(); - int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); - int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); - - if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin - || mlp.rightMargin != newRightMargin) { - mlpChanged = true; - mlp.topMargin = newTopMargin; - mlp.leftMargin = newLeftMargin; - mlp.rightMargin = newRightMargin; - } + int newTopMargin = innerInsets.getSystemWindowInsetTop(); + int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); + int newRightMargin = innerInsets.getSystemWindowInsetRight(); + + // Must use root window insets for the guard, because the color views consume + // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but + // the status guard is attached at the root. + WindowInsets rootInsets = getRootWindowInsets(); + int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); + int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); + + if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin + || mlp.rightMargin != newRightMargin) { + mlpChanged = true; + mlp.topMargin = newTopMargin; + mlp.leftMargin = newLeftMargin; + mlp.rightMargin = newRightMargin; + } - if (newTopMargin > 0 && mStatusGuard == null) { - mStatusGuard = new View(mContext); - mStatusGuard.setVisibility(GONE); - final LayoutParams lp = new LayoutParams(MATCH_PARENT, - mlp.topMargin, Gravity.LEFT | Gravity.TOP); + if (newTopMargin > 0 && mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setVisibility(GONE); + final LayoutParams lp = new LayoutParams(MATCH_PARENT, + mlp.topMargin, Gravity.LEFT | Gravity.TOP); + lp.leftMargin = newGuardLeftMargin; + lp.rightMargin = newGuardRightMargin; + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); + } else if (mStatusGuard != null) { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin + || lp.rightMargin != newGuardRightMargin) { + lp.height = mlp.topMargin; lp.leftMargin = newGuardLeftMargin; lp.rightMargin = newGuardRightMargin; - addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); - } else if (mStatusGuard != null) { - final LayoutParams lp = (LayoutParams) - mStatusGuard.getLayoutParams(); - if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin - || lp.rightMargin != newGuardRightMargin) { - lp.height = mlp.topMargin; - lp.leftMargin = newGuardLeftMargin; - lp.rightMargin = newGuardRightMargin; - mStatusGuard.setLayoutParams(lp); - } + mStatusGuard.setLayoutParams(lp); } + } - // The action mode's theme may differ from the app, so - // always show the status guard above it if we have one. - showStatusGuard = mStatusGuard != null; + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; - if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { - // If it wasn't previously shown, the color may be stale - updateStatusGuardColor(); - } - consumesSystemWindowInsetsTop = showStatusGuard; + if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { + // If it wasn't previously shown, the color may be stale + updateStatusGuardColor(); } // We only need to consume the insets if the action @@ -1690,16 +1664,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // screen_simple_overlay_action_mode.xml). final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate() & (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0; - if (nonOverlay && consumesSystemWindowInsetsTop) { + if (nonOverlay && showStatusGuard) { insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0); } } else { - if (!Flags.actionModeEdgeToEdge()) { - // reset top margin - if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { - mlpChanged = true; - mlp.topMargin = 0; - } + // reset top margin + if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; } } if (mlpChanged) { @@ -1707,7 +1679,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } } - if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) { + if (mStatusGuard != null) { mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE); } return insets; @@ -2211,7 +2183,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view - && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) { + && v != mStatusGuard) { removeViewAt(i); } } diff --git a/core/java/com/android/internal/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java index 783c68695fb3..e5badc87fb13 100644 --- a/core/java/com/android/internal/policy/SystemBarUtils.java +++ b/core/java/com/android/internal/policy/SystemBarUtils.java @@ -66,12 +66,14 @@ public final class SystemBarUtils { display.getDisplayInfo(info); Insets insets; Insets waterfallInsets; + final int localWidth = context.getResources().getDisplayMetrics().widthPixels; + final int localHeight = context.getResources().getDisplayMetrics().heightPixels; if (cutout == null) { insets = Insets.NONE; waterfallInsets = Insets.NONE; } else { DisplayCutout rotated = - cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot); + cutout.getRotated(localWidth, localHeight, rotation, targetRot); insets = Insets.of(rotated.getSafeInsets()); waterfallInsets = rotated.getWaterfallInsets(); } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index d5bb51187ba4..80fc218839d5 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -34,7 +34,6 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; -import com.android.window.flags.Flags; /** * @hide @@ -316,14 +315,12 @@ public class ActionBarContextView extends AbsActionBarView { final int contentWidth = MeasureSpec.getSize(widthMeasureSpec); - final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0 - ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec); + int maxHeight = mContentHeight > 0 ? + mContentHeight : MeasureSpec.getSize(heightMeasureSpec); final int verticalPadding = getPaddingTop() + getPaddingBottom(); int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); - final int height = Flags.actionModeEdgeToEdge() - ? mContentHeight > 0 ? mContentHeight : maxHeight - : maxHeight - verticalPadding; + final int height = maxHeight - verticalPadding; final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); if (mClose != null) { @@ -379,8 +376,7 @@ public class ActionBarContextView extends AbsActionBarView { } setMeasuredDimension(contentWidth, measuredHeight); } else { - setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge() - ? mContentHeight + verticalPadding : maxHeight); + setMeasuredDimension(contentWidth, maxHeight); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index 362b79db4003..ff57fd4fe2ce 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -294,24 +294,54 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar } } - private boolean setMargin(View view, int left, int top, int right, int bottom) { + private boolean applyInsets(View view, Rect insets, boolean toPadding, + boolean left, boolean top, boolean right, boolean bottom) { + boolean changed; + if (toPadding) { + changed = setMargin(view, EMPTY_RECT, left, top, right, bottom); + changed |= setPadding(view, insets, left, top, right, bottom); + } else { + changed = setPadding(view, EMPTY_RECT, left, top, right, bottom); + changed |= setMargin(view, insets, left, top, right, bottom); + } + return changed; + } + + private boolean setPadding(View view, Rect insets, + boolean left, boolean top, boolean right, boolean bottom) { + if ((left && view.getPaddingLeft() != insets.left) + || (top && view.getPaddingTop() != insets.top) + || (right && view.getPaddingRight() != insets.right) + || (bottom && view.getPaddingBottom() != insets.bottom)) { + view.setPadding( + left ? insets.left : view.getPaddingLeft(), + top ? insets.top : view.getPaddingTop(), + right ? insets.right : view.getPaddingRight(), + bottom ? insets.bottom : view.getPaddingBottom()); + return true; + } + return false; + } + + private boolean setMargin(View view, Rect insets, + boolean left, boolean top, boolean right, boolean bottom) { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); boolean changed = false; - if (lp.leftMargin != left) { + if (left && lp.leftMargin != insets.left) { changed = true; - lp.leftMargin = left; + lp.leftMargin = insets.left; } - if (lp.topMargin != top) { + if (top && lp.topMargin != insets.top) { changed = true; - lp.topMargin = top; + lp.topMargin = insets.top; } - if (lp.rightMargin != right) { + if (right && lp.rightMargin != insets.right) { changed = true; - lp.rightMargin = right; + lp.rightMargin = insets.right; } - if (lp.bottomMargin != bottom) { + if (bottom && lp.bottomMargin != insets.bottom) { changed = true; - lp.bottomMargin = bottom; + lp.bottomMargin = insets.bottom; } return changed; } @@ -337,30 +367,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar final Insets sysInsets = insets.getSystemWindowInsets(); mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom); - boolean changed = false; - if (mActionBarExtendsIntoSystemInsets) { - // Don't extend into navigation bar area so the width can align with status bar - // color view. - final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - final int paddingLeft = sysInsets.left - navBarInsets.left; - final int paddingRight = sysInsets.right - navBarInsets.right; - mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0); - changed |= setMargin( - mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0); - if (mActionBarBottom != null) { - mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom); - changed |= setMargin( - mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0); - } - } else { - mActionBarTop.setPadding(0, 0, 0, 0); - changed |= setMargin( - mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0); - if (mActionBarBottom != null) { - mActionBarBottom.setPadding(0, 0, 0, 0); - changed |= setMargin( - mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom); - } + // The top and bottom action bars are always within the content area. + boolean changed = applyInsets(mActionBarTop, mSystemInsets, + mActionBarExtendsIntoSystemInsets, true, true, true, false); + if (mActionBarBottom != null) { + changed |= applyInsets(mActionBarBottom, mSystemInsets, + mActionBarExtendsIntoSystemInsets, true, false, true, true); } // Cannot use the result of computeSystemWindowInsets, because that consumes the @@ -509,12 +521,7 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar ); } } - setMargin( - mContent, - mContentInsets.left, - mContentInsets.top, - mContentInsets.right, - mContentInsets.bottom); + setMargin(mContent, mContentInsets, true, true, true, true); if (!mLastInnerInsets.equals(mInnerInsets)) { // If the inner insets have changed, we need to dispatch this down to diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 32e612f36cce..785b8c78863d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4091,6 +4091,11 @@ <item>-44</item> </integer-array> + <!-- Provides default value for operator name in status bar option in setting: + 0 - Hide operator name in status bar + 1 - Show operator name in status bar (default) --> + <integer name="config_showOperatorNameDefault">1</integer> + <!-- Enabled built-in zen mode condition providers --> <string-array translatable="false" name="config_system_condition_providers"> <item>countdown</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e80243a8f752..246035ae8119 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2719,6 +2719,9 @@ <java-symbol type="string" name="muted_by" /> <java-symbol type="string" name="zen_mode_alarm" /> + <!-- For default state of operator name in status bar --> + <java-symbol type="integer" name="config_showOperatorNameDefault"/> + <java-symbol type="string" name="select_day" /> <java-symbol type="string" name="select_year" /> diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 250b9ce8d89d..001eb620dd0f 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -442,6 +442,44 @@ public class NotificationManagerTest { @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_localModificationDoesNotChangeCache() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel original = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel originalConv = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + originalConv.setConversationId("id", "id_conversation"); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>( + List.of(original.copy(), originalConv.copy()))); + + // modify the output channel, but only locally + NotificationChannel out = mNotificationManager.getNotificationChannel("id"); + out.setName("modified"); + + // This should not change the result of getNotificationChannel + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(original); + assertThat(mNotificationManager.getNotificationChannel("id")).isNotEqualTo(out); + + // and also check the conversation channel + NotificationChannel outConv = mNotificationManager.getNotificationChannel("id", + "id_conversation"); + outConv.setName("conversation_modified"); + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + originalConv); + assertThat( + mNotificationManager.getNotificationChannel("id", "id_conversation")).isNotEqualTo( + outConv); + + // nonexistent conversation returns the (not modified) parent channel + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo( + original); + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isNotEqualTo( + out); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) public void getNotificationChannelGroup_cachedUntilInvalidated() throws Exception { // Data setup: group has some channels in it NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one"); @@ -521,6 +559,37 @@ public class NotificationManagerTest { } @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannelGroup_localModificationDoesNotChangeCache() throws Exception { + // Group setup + NotificationChannelGroup g1 = new NotificationChannelGroup("g1", "group one"); + NotificationChannel nc1 = new NotificationChannel("nc1", "channel one", + NotificationManager.IMPORTANCE_DEFAULT); + nc1.setGroup("g1"); + NotificationChannel nc2 = new NotificationChannel("nc2", "channel two", + NotificationManager.IMPORTANCE_DEFAULT); + nc2.setGroup("g1"); + + NotificationManager.invalidateNotificationChannelCache(); + NotificationManager.invalidateNotificationChannelGroupCache(); + when(mNotificationManager.mBackendService.getNotificationChannelGroupsWithoutChannels( + any())).thenReturn(new ParceledListSlice<>(List.of(g1.clone()))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(nc1.copy(), nc2.copy()))); + + NotificationChannelGroup g1result = mNotificationManager.getNotificationChannelGroup("g1"); + g1result.setDescription("something different!"); + for (NotificationChannel c : g1result.getChannels()) { + c.setDescription("also something different"); + } + + // expected output equivalent to original, unchanged + NotificationChannelGroup expectedG1 = g1.clone(); + expectedG1.setChannels(List.of(nc1, nc2)); + assertThat(mNotificationManager.getNotificationChannelGroup("g1")).isEqualTo(expectedG1); + } + + @Test @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_handheld_isTrue() { PackageManager pm = mock(PackageManager.class); diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 74b6023bde36..c3987caa87e5 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -45,7 +45,7 @@ <dimen name="drop_target_full_screen_padding">20dp</dimen> <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> - <dimen name="drop_target_expanded_view_width">364</dimen> + <dimen name="drop_target_expanded_view_width">330</dimen> <dimen name="drop_target_expanded_view_height">578</dimen> <dimen name="drop_target_expanded_view_padding_bottom">108</dimen> <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index afeaf70c9d62..ffd1f5f3a39e 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -133,7 +133,7 @@ class DragZoneFactory( fullScreenDropTargetPadding = 20.dpToPx() desktopWindowDropTargetPaddingSmall = 100.dpToPx() desktopWindowDropTargetPaddingLarge = 130.dpToPx() - expandedViewDropTargetWidth = 364.dpToPx() + expandedViewDropTargetWidth = 330.dpToPx() expandedViewDropTargetHeight = 578.dpToPx() expandedViewDropTargetPaddingBottom = 108.dpToPx() expandedViewDropTargetPaddingHorizontal = 24.dpToPx() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 746632f67725..f91154c7a362 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1050,7 +1050,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont () -> mShellExecutor.execute(this::onBackAnimationFinished)); if (mApps.length >= 1) { - BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]); + BackMotionEvent startEvent = mCurrentTracker.createStartEvent( + Flags.removeDepartTargetFromMotion() ? null : mApps[0]); dispatchOnBackStarted(mActiveCallback, startEvent); if (startEvent.getSwipeEdge() == EDGE_NONE) { // TODO(b/373544911): onBackStarted is dispatched here so that diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java index ad0e7fc187e9..394c445787b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java @@ -17,6 +17,10 @@ package com.android.wm.shell.common.split; import static com.android.wm.shell.shared.split.SplitScreenConstants.NOT_IN_SPLIT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10; import static com.android.wm.shell.shared.split.SplitScreenConstants.SplitScreenState; import android.graphics.Rect; @@ -62,4 +66,12 @@ public class SplitState { public List<RectF> getCurrentLayout() { return getLayout(mState); } + + /** @return {@code true} if at least one app is partially offscreen in the current layout. */ + public boolean currentStateSupportsOffscreenApps() { + return mState == SNAP_TO_2_10_90 + || mState == SNAP_TO_2_90_10 + || mState == SNAP_TO_3_10_45_45 + || mState == SNAP_TO_3_45_45_10; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 25737c4950d6..80c6f2e5ff33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -49,9 +49,6 @@ class DesktopDisplayEventHandler( private val desktopDisplayModeController: DesktopDisplayModeController, ) : OnDisplaysChangedListener, OnDeskRemovedListener { - private val desktopRepository: DesktopRepository - get() = desktopUserRepositories.current - init { shellInit.addInitCallback({ onInit() }, this) } @@ -66,7 +63,7 @@ class DesktopDisplayEventHandler( object : UserChangeListener { override fun onUserChanged(newUserId: Int, userContext: Context) { val displayIds = rootTaskDisplayAreaOrganizer.displayIds - createDefaultDesksIfNeeded(displayIds.toSet()) + createDefaultDesksIfNeeded(displayIds.toSet(), newUserId) } } ) @@ -75,15 +72,18 @@ class DesktopDisplayEventHandler( override fun onDisplayAdded(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - desktopDisplayModeController.refreshDisplayWindowingMode() + desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) + // The default display's windowing mode depends on the availability of the external + // display. So updating the default display's windowing mode here. + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } - createDefaultDesksIfNeeded(displayIds = setOf(displayId)) + createDefaultDesksIfNeeded(displayIds = setOf(displayId), userId = null) } override fun onDisplayRemoved(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - desktopDisplayModeController.refreshDisplayWindowingMode() + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } // TODO: b/362720497 - move desks in closing display to the remaining desk. @@ -94,28 +94,30 @@ class DesktopDisplayEventHandler( DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue && displayId != DEFAULT_DISPLAY ) { - desktopDisplayModeController.refreshDisplayWindowingMode() + desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) + // The default display's windowing mode depends on the desktop eligibility of the + // external display. So updating the default display's windowing mode here. + desktopDisplayModeController.updateDefaultDisplayWindowingMode() } } override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { - val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId) - if (remainingDesks == 0) { - logV("All desks removed from display#$lastDisplayId") - createDefaultDesksIfNeeded(setOf(lastDisplayId)) - } + createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null) } - private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) { + private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) { if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return logV("createDefaultDesksIfNeeded displays=%s", displayIds) mainScope.launch { desktopRepositoryInitializer.isInitialized.collect { initialized -> if (!initialized) return@collect + val repository = + userId?.let { desktopUserRepositories.getProfile(userId) } + ?: desktopUserRepositories.current displayIds .filter { displayId -> displayId != Display.INVALID_DISPLAY } .filter { displayId -> supportsDesks(displayId) } - .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 } + .filter { displayId -> repository.getNumberOfDesks(displayId) == 0 } .also { displaysNeedingDesk -> logV( "createDefaultDesksIfNeeded creating default desks in displays=%s", @@ -125,7 +127,7 @@ class DesktopDisplayEventHandler( .forEach { displayId -> // TODO: b/393978539 - consider activating the desk on creation when // applicable, such as for connected displays. - desktopTasksController.createDesk(displayId) + desktopTasksController.createDesk(displayId, repository.userId) } cancel() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt index 0a3e2cc3b434..dec489e8fc63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -59,15 +59,15 @@ class DesktopDisplayModeController( private val inputDeviceListener = object : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } override fun onInputDeviceChanged(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } override fun onInputDeviceRemoved(deviceId: Int) { - refreshDisplayWindowingMode() + updateDefaultDisplayWindowingMode() } } @@ -77,12 +77,30 @@ class DesktopDisplayModeController( } } - fun refreshDisplayWindowingMode() { + fun updateExternalDisplayWindowingMode(displayId: Int) { + if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue) return + + val desktopModeSupported = + displayController.getDisplay(displayId)?.let { display -> + DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) + } ?: false + if (!desktopModeSupported) return + + // An external display should always be a freeform display when desktop mode is enabled. + updateDisplayWindowingMode(displayId, WINDOWING_MODE_FREEFORM) + } + + fun updateDefaultDisplayWindowingMode() { if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return - val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay() - val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) - requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } + updateDisplayWindowingMode(DEFAULT_DISPLAY, getTargetWindowingModeForDefaultDisplay()) + } + + private fun updateDisplayWindowingMode(displayId: Int, targetDisplayWindowingMode: Int) { + val tdaInfo = + requireNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)) { + "DisplayAreaInfo of display#$displayId must be non-null." + } val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode if (currentDisplayWindowingMode == targetDisplayWindowingMode) { // Already in the target mode. @@ -90,15 +108,16 @@ class DesktopDisplayModeController( } logV( - "As an external display is connected, changing default display's windowing mode from" + - " ${windowingModeToString(currentDisplayWindowingMode)}" + - " to ${windowingModeToString(targetDisplayWindowingMode)}" + "Changing display#%d's windowing mode from %s to %s", + displayId, + windowingModeToString(currentDisplayWindowingMode), + windowingModeToString(targetDisplayWindowingMode), ) val wct = WindowContainerTransaction() wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) shellTaskOrganizer - .getRunningTasks(DEFAULT_DISPLAY) + .getRunningTasks(displayId) .filter { it.activityType == ACTIVITY_TYPE_STANDARD } .forEach { // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy @@ -114,7 +133,7 @@ class DesktopDisplayModeController( // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display // right after the first launch while its resolved windowing mode is FULLSCREEN. We here // it has the FULLSCREEN override windowing mode. - desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token -> + desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) 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 6214f329e0fd..8b1d3fa65ac6 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 @@ -42,6 +42,7 @@ import android.os.Handler import android.os.IBinder import android.os.SystemProperties import android.os.UserHandle +import android.os.UserManager import android.util.Slog import android.view.Display import android.view.Display.DEFAULT_DISPLAY @@ -115,7 +116,6 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener -import com.android.wm.shell.desktopmode.multidesks.createDesk import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory import com.android.wm.shell.draganddrop.DragAndDropController @@ -163,6 +163,7 @@ import java.util.Optional import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.function.Consumer +import kotlin.coroutines.suspendCoroutine import kotlin.jvm.optionals.getOrNull /** @@ -283,14 +284,8 @@ class DesktopTasksController( if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desktopRepositoryInitializer.deskRecreationFactory = - DeskRecreationFactory { deskUserId, destinationDisplayId, deskId -> - if (deskUserId != userId) { - // TODO: b/400984250 - add multi-user support for multi-desk restoration. - logW("Tried to re-create desk of another user.") - null - } else { - desksOrganizer.createDesk(destinationDisplayId) - } + DeskRecreationFactory { deskUserId, destinationDisplayId, _ -> + createDeskSuspending(displayId = destinationDisplayId, userId = deskUserId) } } } @@ -493,22 +488,55 @@ class DesktopTasksController( runOnTransitStart?.invoke(transition) } - /** Creates a new desk in the given display. */ - fun createDesk(displayId: Int) { + /** Adds a new desk to the given display for the given user. */ + fun createDesk(displayId: Int, userId: Int = this.userId) { + logV("addDesk displayId=%d, userId=%d", displayId, userId) + val repository = userRepositories.getProfile(userId) + createDesk(displayId, userId) { deskId -> + if (deskId == null) { + logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId) + } else { + repository.addDesk(displayId = displayId, deskId = deskId) + } + } + } + + private fun createDesk(displayId: Int, userId: Int = this.userId, onResult: (Int?) -> Unit) { if (displayId == Display.INVALID_DISPLAY) { logW("createDesk attempt with invalid displayId", displayId) + onResult(null) return } - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksOrganizer.createDesk(displayId) { deskId -> - taskRepository.addDesk(displayId = displayId, deskId = deskId) - } - } else { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // In single-desk, the desk reuses the display id. - taskRepository.addDesk(displayId = displayId, deskId = displayId) + logD("createDesk reusing displayId=%d for single-desk", displayId) + onResult(displayId) + return + } + if ( + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue && + UserManager.isHeadlessSystemUserMode() && + UserHandle.USER_SYSTEM == userId + ) { + logW("createDesk ignoring attempt for system user") + return + } + desksOrganizer.createDesk(displayId, userId) { deskId -> + logD( + "createDesk obtained deskId=%d for displayId=%d and userId=%d", + deskId, + displayId, + userId, + ) + onResult(deskId) } } + private suspend fun createDeskSuspending(displayId: Int, userId: Int = this.userId): Int? = + suspendCoroutine { cont -> + createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) } + } + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads fun moveTaskToDefaultDeskAndActivate( @@ -3024,18 +3052,17 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() - if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - tasksToRemove.forEach { - val task = shellTaskOrganizer.getRunningTaskInfo(it) - if (task != null) { - wct.removeTask(task.token) - } else { - recentTasksController?.removeBackgroundTask(it) - } + tasksToRemove.forEach { + // TODO: b/404595635 - consider moving this block into [DesksOrganizer]. + val task = shellTaskOrganizer.getRunningTaskInfo(it) + if (task != null) { + wct.removeTask(task.token) + } else { + recentTasksController?.removeBackgroundTask(it) } - } else { - // TODO: 362720497 - double check background tasks are also removed. - desksOrganizer.removeDesk(wct, deskId) + } + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksOrganizer.removeDesk(wct, deskId, userId) } if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) @@ -3601,6 +3628,7 @@ class DesktopTasksController( pw.println("${prefix}DesktopTasksController") DesktopModeStatus.dump(pw, innerPrefix, context) userRepositories.dump(pw, innerPrefix) + focusTransitionObserver.dump(pw, innerPrefix) } /** The interface for calls from outside the shell, within the host process. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 1effcdb20505..605465b15468 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode.multidesks import android.app.ActivityManager import android.window.TransitionInfo import android.window.WindowContainerTransaction -import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback -import kotlin.coroutines.suspendCoroutine /** An organizer of desk containers in which to host child desktop windows. */ interface DesksOrganizer { - /** Creates a new desk container in the given display. */ - fun createDesk(displayId: Int, callback: OnCreateCallback) + /** Creates a new desk container to use in the given display for the given user. */ + fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) /** Activates the given desk, making it visible in its display. */ fun activateDesk(wct: WindowContainerTransaction, deskId: Int) @@ -32,8 +30,8 @@ interface DesksOrganizer { /** Deactivates the given desk, removing it as the default launch container for new tasks. */ fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) - /** Removes the given desk and its desktop windows. */ - fun removeDesk(wct: WindowContainerTransaction, deskId: Int) + /** Removes the given desk of the given user. */ + fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) /** Moves the given task to the given desk. */ fun moveTaskToDesk( @@ -87,9 +85,3 @@ interface DesksOrganizer { fun onCreated(deskId: Int) } } - -/** Creates a new desk container in the given display. */ -suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont -> - val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) } - createDesk(displayId, onCreateCallback) -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index c30987ac7640..e4edeb95be6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -30,6 +30,7 @@ import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.core.util.forEach +import androidx.core.util.valueIterator import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer @@ -40,7 +41,13 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter -/** A [DesksOrganizer] that uses root tasks as the container of each desk. */ +/** + * A [DesksOrganizer] that uses root tasks as the container of each desk. + * + * Note that root tasks are reusable between multiple users at the same time, and may also be + * pre-created to have one ready for the first entry to the default desk, so root-task existence + * does not imply a formal desk exists to the user. + */ class RootTaskDesksOrganizer( shellInit: ShellInit, shellCommandHandler: ShellCommandHandler, @@ -65,9 +72,26 @@ class RootTaskDesksOrganizer( } } - override fun createDesk(displayId: Int, callback: OnCreateCallback) { - logV("createDesk in display: %d", displayId) - createDeskRootRequests += CreateDeskRequest(displayId, callback) + override fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) { + logV("createDesk in displayId=%d userId=%s", displayId, userId) + // Find an existing desk that is not yet used by this user. + val unassignedDesk = + deskRootsByDeskId + .valueIterator() + .asSequence() + .filterNot { desk -> userId in desk.users } + .firstOrNull() + if (unassignedDesk != null) { + unassignedDesk.users.add(userId) + callback.onCreated(unassignedDesk.deskId) + return + } + createDeskRoot(displayId, userId, callback) + } + + private fun createDeskRoot(displayId: Int, userId: Int, callback: OnCreateCallback) { + logV("createDeskRoot in display: %d for user: %d", displayId, userId) + createDeskRootRequests += CreateDeskRequest(displayId, userId, callback) shellTaskOrganizer.createRootTask( displayId, WINDOWING_MODE_FREEFORM, @@ -76,31 +100,52 @@ class RootTaskDesksOrganizer( ) } - override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) { - logV("removeDesk %d", deskId) - deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } - deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } + override fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) { + logV("removeDesk %d for userId=%d", deskId, userId) + val deskRoot = deskRootsByDeskId[deskId] + if (deskRoot == null) { + logW("removeDesk attempted to remove non-existent desk=%d", deskId) + return + } + updateLaunchRoot(wct, deskId, enabled = false) + deskRoot.users.remove(userId) + if (deskRoot.users.isEmpty()) { + // No longer in use by any users, remove it completely. + logD("removeDesk %d is no longer used by any users, removing it completely", deskId) + wct.removeRootTask(deskRoot.token) + deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } + } } override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("activateDesk %d", deskId) val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } wct.reorder(root.token, /* onTop= */ true) - wct.setLaunchRoot( - /* container= */ root.taskInfo.token, - /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), - /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), - ) + updateLaunchRoot(wct, deskId, enabled = true) } override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("deactivateDesk %d", deskId) + updateLaunchRoot(wct, deskId, enabled = false) + } + + private fun updateLaunchRoot(wct: WindowContainerTransaction, deskId: Int, enabled: Boolean) { val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } - wct.setLaunchRoot( - /* container= */ root.taskInfo.token, - /* windowingModes= */ null, - /* activityTypes= */ null, - ) + root.isLaunchRootRequested = enabled + logD("updateLaunchRoot deskId=%d enabled=%b", deskId, enabled) + if (enabled) { + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), + /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), + ) + } else { + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ null, + /* activityTypes= */ null, + ) + } } override fun moveTaskToDesk( @@ -275,7 +320,13 @@ class RootTaskDesksOrganizer( // Appearing root matches desk request. val deskId = taskInfo.taskId logV("Desk #$deskId appeared") - deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash) + deskRootsByDeskId[deskId] = + DeskRoot( + deskId = deskId, + taskInfo = taskInfo, + leash = leash, + users = mutableSetOf(deskRequest.userId), + ) createDeskRootRequests.remove(deskRequest) deskRequest.onCreateCallback.onCreated(deskId) createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId) @@ -430,6 +481,8 @@ class RootTaskDesksOrganizer( val taskInfo: RunningTaskInfo, val leash: SurfaceControl, val children: MutableSet<Int> = mutableSetOf(), + val users: MutableSet<Int> = mutableSetOf(), + var isLaunchRootRequested: Boolean = false, ) { val token: WindowContainerToken = taskInfo.token } @@ -449,15 +502,24 @@ class RootTaskDesksOrganizer( private data class CreateDeskRequest( val displayId: Int, + val userId: Int, val onCreateCallback: OnCreateCallback, ) private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int) + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logV(msg: String, vararg arguments: Any?) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logE(msg: String, vararg arguments: Any?) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } @@ -473,7 +535,9 @@ class RootTaskDesksOrganizer( val minimizationRoot = deskMinimizationRootsByDeskId[deskId] pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") pw.println("$innerPrefix displayId=${root.taskInfo.displayId}") + pw.println("$innerPrefix isLaunchRootRequested=${root.isLaunchRootRequested}") pw.println("$innerPrefix children=${root.children}") + pw.println("$innerPrefix users=${root.users}") pw.println("$innerPrefix minimization root:") pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}") if (minimizationRoot != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 10db5ca03637..d240aca522bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -3618,9 +3618,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); - if (Flags.enableFlexibleTwoAppSplit()) { - addAllDimLayersToTransition(info, true /* show */); - } + addAllDimLayersToTransition(info, true /* show */); return true; } @@ -3871,9 +3869,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } addDividerBarToTransition(info, false /* show */); - if (Flags.enableFlexibleTwoAppSplit()) { - addAllDimLayersToTransition(info, false /* show */); - } + addAllDimLayersToTransition(info, false /* show */); } /** Call this when the recents animation canceled during split-screen. */ @@ -3999,8 +3995,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, info.addChange(barChange); } - /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */ + /** + * Add dim layers to the transition, so that they can be hidden/shown when animation starts. + * They're only added if there is at least one offscreen app. + */ private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) { + if (!mSplitState.currentStateSupportsOffscreenApps()) { + return; + } + if (Flags.enableFlexibleSplit()) { List<StageTaskListener> stages = mStageOrderOperator.getActiveStages(); for (int i = 0; i < stages.size(); i++) { @@ -4008,7 +4011,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1); addDimLayerToTransition(info, show, stage, mTempRect1); } - } else { + } else if (enableFlexibleTwoAppSplit()) { addDimLayerToTransition(info, show, mMainStage, getMainStageBounds()); addDimLayerToTransition(info, show, mSideStage, getSideStageBounds()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index bff08ba6d88f..3240cbb779c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -169,7 +169,7 @@ public class DefaultSurfaceAnimator { needCrop = true; } if (needCrop) { - t.setCrop(leash, mAnimClipRect); + t.setWindowCrop(leash, mAnimClipRect); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java index f0f1ad05008b..b91fb048dc09 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -31,6 +31,7 @@ import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.os.RemoteException; import android.util.ArraySet; +import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; import android.window.TransitionInfo; @@ -38,6 +39,7 @@ import android.window.TransitionInfo; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.IFocusTransitionListener; +import java.io.PrintWriter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -237,4 +239,21 @@ public class FocusTransitionObserver { } return task.displayId == mFocusedDisplayId && isFocusedOnDisplay(task); } + + /** Dumps focused display and tasks. */ + public void dump(PrintWriter originalWriter, String prefix) { + final IndentingPrintWriter writer = + new IndentingPrintWriter(originalWriter, " ", prefix); + writer.println("FocusTransitionObserver:"); + writer.increaseIndent(); + writer.printf("currentFocusedDisplayId=%d\n", mFocusedDisplayId); + writer.println("currentFocusedTaskOnDisplay:"); + writer.increaseIndent(); + for (int i = 0; i < mFocusedTaskOnDisplay.size(); i++) { + writer.printf("Display #%d: taskId=%d topActivity=%s\n", + mFocusedTaskOnDisplay.keyAt(i), + mFocusedTaskOnDisplay.valueAt(i).taskId, + mFocusedTaskOnDisplay.valueAt(i).topActivity); + } + } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp index 7585c977809e..50581f7e01f3 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp @@ -33,7 +33,6 @@ android_test { "WMShellFlickerTestsBase", "WMShellScenariosDesktopMode", "WMShellTestUtils", - "ui-trace-collector", ], data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt index 7d9f2bf8fdf6..05ddb4043b82 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt @@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.device.apphelpers.GmailAppHelper +import android.tools.device.apphelpers.CalculatorAppHelper import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -44,8 +44,9 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val gmailHelper = GmailAppHelper(instrumentation) - private val gmailApp = DesktopModeAppHelper(gmailHelper) + private val calculatorHelper = CalculatorAppHelper(instrumentation) + private val calculatorApp = DesktopModeAppHelper(calculatorHelper) + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -59,20 +60,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopMode(wmHelper, device) tapl.showTaskbarIfHidden() - gmailApp.launchViaIntent(wmHelper) - gmailApp.minimizeDesktopApp(wmHelper, device) + calculatorApp.launchViaIntent(wmHelper) + calculatorApp.minimizeDesktopApp(wmHelper, device) } @Test open fun unminimizeApp() { tapl.launchedAppState.taskbar - .getAppIcon(gmailHelper.appName) - .launch(gmailHelper.packageName) + .getAppIcon(calculatorHelper.appName) + .launch(calculatorApp.packageName) } @After fun teardown() { testApp.exit(wmHelper) - gmailApp.exit(wmHelper) + calculatorApp.exit(wmHelper) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 7659ec903480..8cbec687d8d8 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -74,10 +74,6 @@ <option name="shell-timeout" value="6600s"/> <option name="test-timeout" value="6000s"/> <option name="hidden-api-checks" value="false"/> - <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> - <!-- DefaultUITraceListener args --> - <option name="instrumentation-arg" key="per_run" value="true"/> - <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> </test> <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 85a431be8e8b..42dcaf4b4f33 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -51,6 +51,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -213,12 +214,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testUserChanged_createsDeskWhenNeeded() = testScope.runTest { + val userId = 11 whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) val userChangeListenerCaptor = argumentCaptor<UserChangeListener>() verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture()) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0) - whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1) + val mockRepository = mock<DesktopRepository>() + whenever(mockDesktopUserRepositories.getProfile(userId)).thenReturn(mockRepository) + whenever(mockRepository.getNumberOfDesks(displayId = 2)).thenReturn(0) + whenever(mockRepository.getNumberOfDesks(displayId = 3)).thenReturn(0) + whenever(mockRepository.getNumberOfDesks(displayId = 4)).thenReturn(1) whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4)) desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) handler.onDisplayAdded(displayId = 2) @@ -227,7 +231,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { runCurrent() clearInvocations(mockDesktopTasksController) - userChangeListenerCaptor.lastValue.onUserChanged(1, context) + userChangeListenerCaptor.lastValue.onUserChanged(userId, context) runCurrent() verify(mockDesktopTasksController).createDesk(displayId = 2) @@ -238,20 +242,22 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @Test fun testConnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) - verify(desktopDisplayModeController).refreshDisplayWindowingMode() + verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId) + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() } @Test fun testDisconnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) - verify(desktopDisplayModeController).refreshDisplayWindowingMode() + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() } @Test @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) fun testDesktopModeEligibleChanged() { onDisplaysChangedListenerCaptor.lastValue.onDesktopModeEligibleChanged(externalDisplayId) - verify(desktopDisplayModeController).refreshDisplayWindowingMode() + verify(desktopDisplayModeController).updateExternalDisplayWindowingMode(externalDisplayId) + verify(desktopDisplayModeController).updateDefaultDisplayWindowingMode() } private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt index 488025a3d754..7e9ee34c8f68 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -101,6 +101,7 @@ class DesktopDisplayModeControllerTest( private val fullscreenTask = TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + private val externalTDA = DisplayAreaInfo(MockToken().token(), EXTERNAL_DISPLAY_ID, 0) private val wallpaperToken = MockToken().token() private val defaultDisplay = mock<Display>() private val externalDisplay = mock<Display>() @@ -129,6 +130,8 @@ class DesktopDisplayModeControllerTest( whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .thenReturn(defaultTDA) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(EXTERNAL_DISPLAY_ID)) + .thenReturn(externalTDA) controller = DesktopDisplayModeController( context, @@ -292,16 +295,30 @@ class DesktopDisplayModeControllerTest( .isEqualTo(WINDOWING_MODE_UNDEFINED) } + @Test + @EnableFlags(DisplayFlags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + fun externalDisplayWindowingMode() { + externalTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + setExtendedMode(true) + + controller.updateExternalDisplayWindowingMode(EXTERNAL_DISPLAY_ID) + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[externalTDA.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + private fun connectExternalDisplay() { whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID)) - controller.refreshDisplayWindowingMode() + controller.updateDefaultDisplayWindowingMode() } private fun disconnectExternalDisplay() { whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY)) - controller.refreshDisplayWindowingMode() + controller.updateDefaultDisplayWindowingMode() } private fun setExtendedMode(enabled: Boolean) { 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 a222d392848b..d81786b5e6a5 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 @@ -50,6 +50,7 @@ import android.os.Binder import android.os.Bundle import android.os.Handler import android.os.IBinder +import android.os.UserHandle import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -5537,7 +5538,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.removeDesk(deskId = 2) - verify(desksOrganizer).removeDesk(any(), eq(2)) + verify(desksOrganizer).removeDesk(any(), eq(2), any()) verify(desksTransitionsObserver) .addPendingTransition( argThat { @@ -5553,6 +5554,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun removeDesk_multipleDesks_removesRunningTasks() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + val task1 = setUpFreeformTask(deskId = 2) + val task2 = setUpFreeformTask(deskId = 2) + val task3 = setUpFreeformTask(deskId = 2) + + controller.removeDesk(deskId = 2) + + val wct = getLatestWct(TRANSIT_CLOSE) + wct.assertRemove(task1.token) + wct.assertRemove(task2.token) + wct.assertRemove(task3.token) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun removeDesk_multipleDesks_removesRecentTasks() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + val task1 = setUpFreeformTask(deskId = 2, background = true) + val task2 = setUpFreeformTask(deskId = 2, background = true) + val task3 = setUpFreeformTask(deskId = 2, background = true) + + controller.removeDesk(deskId = 2) + + verify(recentTasksController).removeBackgroundTask(task1.taskId) + verify(recentTasksController).removeBackgroundTask(task2.taskId) + verify(recentTasksController).removeBackgroundTask(task3.taskId) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun activateDesk_multipleDesks_addsPendingTransition() { val deskId = 0 val transition = Binder() @@ -7509,11 +7553,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testCreateDesk() { val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY) - whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation -> - (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) + whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any(), any())).thenAnswer { + invocation -> + (invocation.arguments[2] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) } - controller.createDesk(DEFAULT_DISPLAY) + controller.createDesk(DEFAULT_DISPLAY, taskRepository.userId) assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1) } @@ -7523,7 +7568,17 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun testCreateDesk_invalidDisplay_dropsRequest() { controller.createDesk(INVALID_DISPLAY) - verify(desksOrganizer, never()).createDesk(any(), any()) + verify(desksOrganizer, never()).createDesk(any(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testCreateDesk_systemUser_dropsRequest() { + assumeTrue(UserManager.isHeadlessSystemUserMode()) + + controller.createDesk(DEFAULT_DISPLAY, UserHandle.USER_SYSTEM) + + verify(desksOrganizer, never()).createDesk(any(), any(), any()) } @Test @@ -8256,6 +8311,15 @@ private fun WindowContainerTransaction.assertReorderSequenceInRange( .inOrder() } +private fun WindowContainerTransaction.assertRemove(token: WindowContainerToken) { + assertThat( + hierarchyOps.any { hop -> + hop.container == token.asBinder() && hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK + } + ) + .isTrue() +} + private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { assertIndexInBounds(index) val op = hierarchyOps[index] diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index e57fc38e3607..34f832b4fba4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -19,7 +19,7 @@ import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner -import android.view.Display +import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo @@ -41,9 +41,9 @@ import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRo import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.suspendCoroutine import kotlin.test.assertNotNull import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test @@ -86,11 +86,21 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { organizer.setOnDesktopTaskInfoChangedListener(taskInfoChangedListener) } - @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() } + @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDeskSuspending() } + + @Test + fun testCreateDesk_rootExistsForOtherUser_reusesRoot() = runTest { + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) + + val deskId = + organizer.createDeskSuspending(displayId = DEFAULT_DISPLAY, userId = SECONDARY_USER_ID) + + assertThat(deskId).isEqualTo(desk.deskRoot.deskId) + } @Test fun testCreateMinimizationRoot_marksHidden() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() verify(mockShellTaskOrganizer) .applyTransaction( @@ -115,7 +125,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskAppeared_duplicateRoot_throws() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThrows(Exception::class.java) { organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl()) @@ -124,7 +134,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThrows(Exception::class.java) { organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl()) @@ -133,7 +143,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -142,7 +152,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesMinimizedRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() organizer.onTaskVanished(desk.deskRoot.taskInfo) organizer.onTaskVanished(desk.minimizationRoot.taskInfo) @@ -152,7 +162,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowAppearsInDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -162,7 +172,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -172,7 +182,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowMovesToMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -185,7 +195,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowDisappearsFromDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -196,7 +206,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -206,11 +216,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testRemoveDesk_disablesAsLaunchRoot() = runTest { + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) + val wct = WindowContainerTransaction() + organizer.activateDesk(wct, desk.deskRoot.deskId) + assertThat(desk.deskRoot.isLaunchRootRequested).isTrue() + + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) + + assertThat(desk.deskRoot.isLaunchRootRequested).isFalse() + } + + @Test fun testRemoveDesk_removesDeskRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskRoot.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) assertThat( wct.hierarchyOps.any { hop -> @@ -223,10 +245,10 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testRemoveDesk_removesMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending(userId = PRIMARY_USER_ID) val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskRoot.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId, userId = PRIMARY_USER_ID) assertThat( wct.hierarchyOps.any { hop -> @@ -238,8 +260,27 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testRemoveDesk_rootUsedByOtherUser_keepsDeskRoot() = runTest { + val primaryUserDesk = createDeskSuspending(userId = PRIMARY_USER_ID) + val secondaryUserDesk = createDeskSuspending(userId = SECONDARY_USER_ID) + assertThat(primaryUserDesk).isEqualTo(secondaryUserDesk) + + val wct = WindowContainerTransaction() + organizer.removeDesk(wct, primaryUserDesk.deskRoot.deskId, userId = PRIMARY_USER_ID) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == primaryUserDesk.deskRoot.token.asBinder() + } + ) + .isFalse() + assertThat(primaryUserDesk.deskRoot.users).containsExactly(SECONDARY_USER_ID) + } + + @Test fun testActivateDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val wct = WindowContainerTransaction() organizer.activateDesk(wct, desk.deskRoot.deskId) @@ -271,7 +312,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testMoveTaskToDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() @@ -308,7 +349,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val endDesk = @@ -321,7 +362,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val endDesk = @@ -334,7 +375,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testIsDeskActiveAtEnd() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val isActive = organizer.isDeskActiveAtEnd( @@ -352,7 +393,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun deactivateDesk_clearsLaunchRoot() = runTest { val wct = WindowContainerTransaction() - val desk = createDesk() + val desk = createDeskSuspending() organizer.activateDesk(wct, desk.deskRoot.deskId) organizer.deactivateDesk(wct, desk.deskRoot.deskId) @@ -370,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_forDeskId() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -385,7 +426,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_forDeskId_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -403,7 +444,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_anyDesk() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -417,7 +458,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun isDeskChange_anyDesk_inMinimizationRoot() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() assertThat( organizer.isDeskChange( @@ -434,7 +475,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -447,7 +488,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask_alreadyMinimized_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -459,8 +500,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun minimizeTask_inDifferentDesk_noOp() = runTest { - val desk = createDesk() - val otherDesk = createDesk() + val desk = createDeskSuspending() + val otherDesk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -472,7 +513,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -489,7 +530,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask_alreadyUnminimized_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) @@ -503,7 +544,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun unminimizeTask_notInDesk_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -514,7 +555,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -534,7 +575,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront_notInDesk_noOp() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask() val wct = WindowContainerTransaction() @@ -553,7 +594,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest { - val desk = createDesk() + val desk = createDeskSuspending() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() organizer.onTaskAppeared(task, SurfaceControl()) @@ -578,7 +619,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) + createDeskSuspending(visible = true) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() } @@ -587,7 +628,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - createDesk(visible = false) + createDeskSuspending(visible = false) assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() } @@ -596,8 +637,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - createDesk(visible = false) + createDeskSuspending(visible = true) + createDeskSuspending(visible = false) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() } @@ -606,7 +647,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - val desk = createDesk(visible = false) + val desk = createDeskSuspending(visible = false) desk.deskRoot.taskInfo.isVisible = true organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -617,7 +658,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - val desk = createDesk(visible = true) + val desk = createDeskSuspending(visible = true) desk.deskRoot.taskInfo.isVisible = false organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -628,8 +669,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - val desk = createDesk(visible = true) + createDeskSuspending(visible = true) + val desk = createDeskSuspending(visible = true) desk.deskRoot.taskInfo.isVisible = false organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) @@ -640,7 +681,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = false - val desk = createDesk(visible = true) + val desk = createDeskSuspending(visible = true) organizer.onTaskVanished(desk.deskRoot.taskInfo) assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() @@ -650,8 +691,8 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest { launchAdjacentController.launchAdjacentEnabled = true - createDesk(visible = true) - val desk = createDesk(visible = true) + createDeskSuspending(visible = true) + val desk = createDeskSuspending(visible = true) organizer.onTaskVanished(desk.deskRoot.taskInfo) assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() @@ -659,7 +700,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun onTaskInfoChanged_taskNotRoot_invokesListener() = runTest { - createDesk() + createDeskSuspending() val task = createFreeformTask().apply { taskId = TEST_CHILD_TASK_ID } organizer.onTaskInfoChanged(task) @@ -669,7 +710,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun onTaskInfoChanged_isDeskRoot_doesNotInvokeListener() = runTest { - val deskRoot = createDesk().deskRoot + val deskRoot = createDeskSuspending().deskRoot organizer.onTaskInfoChanged(deskRoot.taskInfo) @@ -678,7 +719,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun onTaskInfoChanged_isMinimizationRoot_doesNotInvokeListener() = runTest { - val minimizationRoot = createDesk().minimizationRoot + val minimizationRoot = createDeskSuspending().minimizationRoot organizer.onTaskInfoChanged(minimizationRoot.taskInfo) @@ -690,7 +731,10 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val minimizationRoot: DeskMinimizationRoot, ) - private suspend fun createDesk(visible: Boolean = true): DeskRoots { + private suspend fun createDeskSuspending( + visible: Boolean = true, + userId: Int = PRIMARY_USER_ID, + ): DeskRoots { val freeformRootTask = createFreeformTask().apply { parentTaskId = -1 @@ -701,7 +745,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { Mockito.reset(mockShellTaskOrganizer) whenever( mockShellTaskOrganizer.createRootTask( - Display.DEFAULT_DISPLAY, + DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM, organizer, true, @@ -715,13 +759,9 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val listener = (invocation.arguments[2] as TaskListener) listener.onTaskAppeared(minimizationRootTask, SurfaceControl()) } - val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY) - assertEquals(freeformRootTask.taskId, deskId) - val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId)) - val minimizationRoot = - assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId]) - assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId) - assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + val deskId = organizer.createDeskSuspending(DEFAULT_DISPLAY, userId) + val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(deskId)) + val minimizationRoot = assertNotNull(organizer.deskMinimizationRootsByDeskId[deskId]) return DeskRoots(deskRoot, minimizationRoot) } @@ -746,7 +786,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { hop.toTop } + private suspend fun DesksOrganizer.createDeskSuspending(displayId: Int, userId: Int): Int = + suspendCoroutine { cont -> + createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) } + } + companion object { + private const val PRIMARY_USER_ID = 10 + private const val SECONDARY_USER_ID = 11 private const val TEST_CHILD_TASK_ID = 100 } } diff --git a/location/api/system-current.txt b/location/api/system-current.txt index 47984745fafc..8026d4662cb9 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -32,7 +32,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class BeidouAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -48,7 +48,7 @@ package android.location { ctor public BeidouAssistance.Builder(); method @NonNull public android.location.BeidouAssistance build(); method @NonNull public android.location.BeidouAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.BeidouAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.BeidouAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.BeidouAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -176,7 +176,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GalileoAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.GalileoIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -192,7 +192,7 @@ package android.location { ctor public GalileoAssistance.Builder(); method @NonNull public android.location.GalileoAssistance build(); method @NonNull public android.location.GalileoAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.GalileoIonosphericModel); method @NonNull public android.location.GalileoAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -346,7 +346,8 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GlonassAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GlonassAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); method @NonNull public java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections> getSatelliteCorrections(); method @NonNull public java.util.List<android.location.GlonassSatelliteEphemeris> getSatelliteEphemeris(); method @NonNull public java.util.List<android.location.TimeModel> getTimeModels(); @@ -359,7 +360,8 @@ package android.location { ctor public GlonassAssistance.Builder(); method @NonNull public android.location.GlonassAssistance build(); method @NonNull public android.location.GlonassAssistance.Builder setAlmanac(@Nullable android.location.GlonassAlmanac); - method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); + method @NonNull public android.location.GlonassAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); method @NonNull public android.location.GlonassAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); method @NonNull public android.location.GlonassAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GlonassSatelliteEphemeris>); method @NonNull public android.location.GlonassAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); @@ -717,7 +719,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GpsAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -733,7 +735,7 @@ package android.location { ctor public GpsAssistance.Builder(); method @NonNull public android.location.GpsAssistance build(); method @NonNull public android.location.GpsAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.GpsAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.GpsAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.GpsAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); @@ -1253,7 +1255,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class QzssAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); - method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); + method @NonNull public java.util.List<android.location.AuxiliaryInformation> getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -1269,7 +1271,7 @@ package android.location { ctor public QzssAssistance.Builder(); method @NonNull public android.location.QzssAssistance build(); method @NonNull public android.location.QzssAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); - method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@NonNull java.util.List<android.location.AuxiliaryInformation>); method @NonNull public android.location.QzssAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.QzssAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.QzssAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); diff --git a/location/java/android/location/BeidouAssistance.java b/location/java/android/location/BeidouAssistance.java index e35493ed1007..274332dab9a8 100644 --- a/location/java/android/location/BeidouAssistance.java +++ b/location/java/android/location/BeidouAssistance.java @@ -50,8 +50,8 @@ public final class BeidouAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class BeidouAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class BeidouAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -178,7 +183,7 @@ public final class BeidouAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -196,7 +201,7 @@ public final class BeidouAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(BeidouSatelliteEphemeris.CREATOR)) @@ -219,7 +224,7 @@ public final class BeidouAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<BeidouSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -253,10 +258,10 @@ public final class BeidouAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/GalileoAssistance.java b/location/java/android/location/GalileoAssistance.java index 7f81ccdf346f..f73ce400dd9d 100644 --- a/location/java/android/location/GalileoAssistance.java +++ b/location/java/android/location/GalileoAssistance.java @@ -50,8 +50,8 @@ public final class GalileoAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class GalileoAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class GalileoAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -161,7 +166,7 @@ public final class GalileoAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -196,7 +201,7 @@ public final class GalileoAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GalileoSatelliteEphemeris.CREATOR)) @@ -219,7 +224,7 @@ public final class GalileoAssistance implements Parcelable { private GalileoIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GalileoSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -253,10 +258,10 @@ public final class GalileoAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/GlonassAssistance.java b/location/java/android/location/GlonassAssistance.java index c7ed1c52b403..8c5ddbb10a07 100644 --- a/location/java/android/location/GlonassAssistance.java +++ b/location/java/android/location/GlonassAssistance.java @@ -44,8 +44,8 @@ public final class GlonassAssistance implements Parcelable { /** The UTC model. */ @Nullable private final UtcModel mUtcModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -56,10 +56,18 @@ public final class GlonassAssistance implements Parcelable { /** The list of Glonass satellite corrections. */ @NonNull private final List<GnssSatelliteCorrections> mSatelliteCorrections; + /** The list of real time integrity models. */ + @NonNull private final List<RealTimeIntegrityModel> mRealTimeIntegrityModels; + private GlonassAssistance(Builder builder) { mAlmanac = builder.mAlmanac; mUtcModel = builder.mUtcModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -77,6 +85,12 @@ public final class GlonassAssistance implements Parcelable { } else { mSatelliteCorrections = new ArrayList<>(); } + if (builder.mRealTimeIntegrityModels != null) { + mRealTimeIntegrityModels = + Collections.unmodifiableList(new ArrayList<>(builder.mRealTimeIntegrityModels)); + } else { + mRealTimeIntegrityModels = new ArrayList<>(); + } } /** Returns the Glonass almanac. */ @@ -109,9 +123,15 @@ public final class GlonassAssistance implements Parcelable { return mSatelliteCorrections; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of real time integrity models. */ + @NonNull + public List<RealTimeIntegrityModel> getRealTimeIntegrityModels() { + return mRealTimeIntegrityModels; + } + + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -124,10 +144,11 @@ public final class GlonassAssistance implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mAlmanac, flags); dest.writeTypedObject(mUtcModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mSatelliteCorrections); + dest.writeTypedList(mRealTimeIntegrityModels); } @Override @@ -140,6 +161,7 @@ public final class GlonassAssistance implements Parcelable { builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", satelliteCorrections = ").append(mSatelliteCorrections); + builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); builder.append("]"); return builder.toString(); } @@ -152,12 +174,14 @@ public final class GlonassAssistance implements Parcelable { .setAlmanac(in.readTypedObject(GlonassAlmanac.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GlonassSatelliteEphemeris.CREATOR)) .setSatelliteCorrections( in.createTypedArrayList(GnssSatelliteCorrections.CREATOR)) + .setRealTimeIntegrityModels( + in.createTypedArrayList(RealTimeIntegrityModel.CREATOR)) .build(); } @@ -171,10 +195,11 @@ public final class GlonassAssistance implements Parcelable { public static final class Builder { private GlonassAlmanac mAlmanac; private UtcModel mUtcModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GlonassSatelliteEphemeris> mSatelliteEphemeris; private List<GnssSatelliteCorrections> mSatelliteCorrections; + private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; /** Sets the Glonass almanac. */ @NonNull @@ -190,10 +215,10 @@ public final class GlonassAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } @@ -221,6 +246,14 @@ public final class GlonassAssistance implements Parcelable { return this; } + /** Sets the list of real time integrity models. */ + @NonNull + public Builder setRealTimeIntegrityModels( + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { + mRealTimeIntegrityModels = realTimeIntegrityModels; + return this; + } + /** Builds the {@link GlonassAssistance}. */ @NonNull public GlonassAssistance build() { diff --git a/location/java/android/location/GpsAssistance.java b/location/java/android/location/GpsAssistance.java index 5a8802f057e2..45b13b2f97f6 100644 --- a/location/java/android/location/GpsAssistance.java +++ b/location/java/android/location/GpsAssistance.java @@ -51,8 +51,8 @@ public final class GpsAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -71,7 +71,12 @@ public final class GpsAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -121,9 +126,9 @@ public final class GpsAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -163,7 +168,7 @@ public final class GpsAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GpsSatelliteEphemeris.CREATOR)) @@ -191,7 +196,7 @@ public final class GpsAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -221,7 +226,7 @@ public final class GpsAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GpsSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -256,10 +261,10 @@ public final class GpsAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/location/java/android/location/QzssAssistance.java b/location/java/android/location/QzssAssistance.java index 27c34370316e..75a267f2dd2a 100644 --- a/location/java/android/location/QzssAssistance.java +++ b/location/java/android/location/QzssAssistance.java @@ -50,8 +50,8 @@ public final class QzssAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; - /** The auxiliary information. */ - @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of auxiliary informations. */ + @NonNull private final List<AuxiliaryInformation> mAuxiliaryInformation; /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -70,7 +70,12 @@ public final class QzssAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; - mAuxiliaryInformation = builder.mAuxiliaryInformation; + if (builder.mAuxiliaryInformation != null) { + mAuxiliaryInformation = + Collections.unmodifiableList(new ArrayList<>(builder.mAuxiliaryInformation)); + } else { + mAuxiliaryInformation = new ArrayList<>(); + } if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -120,9 +125,9 @@ public final class QzssAssistance implements Parcelable { return mLeapSecondsModel; } - /** Returns the auxiliary information. */ - @Nullable - public AuxiliaryInformation getAuxiliaryInformation() { + /** Returns the list of auxiliary informations. */ + @NonNull + public List<AuxiliaryInformation> getAuxiliaryInformation() { return mAuxiliaryInformation; } @@ -162,7 +167,7 @@ public final class QzssAssistance implements Parcelable { .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( - in.readTypedObject(AuxiliaryInformation.CREATOR)) + in.createTypedArrayList(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(QzssSatelliteEphemeris.CREATOR)) @@ -190,7 +195,7 @@ public final class QzssAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); - dest.writeTypedObject(mAuxiliaryInformation, flags); + dest.writeTypedList(mAuxiliaryInformation); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -220,7 +225,7 @@ public final class QzssAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; - private AuxiliaryInformation mAuxiliaryInformation; + private List<AuxiliaryInformation> mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<QzssSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -254,10 +259,10 @@ public final class QzssAssistance implements Parcelable { return this; } - /** Sets the auxiliary information. */ + /** Sets the list of auxiliary informations. */ @NonNull public Builder setAuxiliaryInformation( - @Nullable AuxiliaryInformation auxiliaryInformation) { + @NonNull List<AuxiliaryInformation> auxiliaryInformation) { mAuxiliaryInformation = auxiliaryInformation; return this; } diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp index 564c3985264d..8701d3d8daae 100644 --- a/packages/SettingsLib/Metadata/Android.bp +++ b/packages/SettingsLib/Metadata/Android.bp @@ -19,4 +19,7 @@ android_library { "androidx.fragment_fragment", ], kotlincflags: ["-Xjvm-default=all"], + optimize: { + proguard_flags_files: ["proguard.pgcfg"], + }, } diff --git a/packages/SettingsLib/Metadata/proguard.pgcfg b/packages/SettingsLib/Metadata/proguard.pgcfg new file mode 100644 index 000000000000..3a137732a229 --- /dev/null +++ b/packages/SettingsLib/Metadata/proguard.pgcfg @@ -0,0 +1,8 @@ +# Preserve names for IPC codec to support unmarshalling Parcelable +-keepnames class com.android.settingslib.metadata.PreferenceCoordinate { + public static final ** CREATOR; +} + +-keepnames class com.android.settingslib.metadata.PreferenceScreenCoordinate { + public static final ** CREATOR; +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 5580d2e3211b..6dd5e371d8a6 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -37,6 +37,7 @@ import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.ui.AnnotatedText +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers @@ -153,13 +154,15 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp override val changeable = { isChangeable } override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) } } - RestrictedSwitchPreference( - model = switchModel, - restrictions = getRestrictions(userId, packageName, isAllowed()), - ifBlockedByAdminOverrideCheckedValueTo = switchifBlockedByAdminOverrideCheckedValueTo, - restrictionsProviderFactory = restrictionsProviderFactory, - ) - InfoPageAdditionalContent(record, isAllowed) + Category { + RestrictedSwitchPreference( + model = switchModel, + restrictions = getRestrictions(userId, packageName, isAllowed()), + ifBlockedByAdminOverrideCheckedValueTo = + switchifBlockedByAdminOverrideCheckedValueTo, + restrictionsProviderFactory = restrictionsProviderFactory, + ) + } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index 771eb85ee21a..a3e4aa0420ff 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -91,9 +91,6 @@ interface TogglePermissionAppListModel<T : AppRecord> { * Sets whether the permission is allowed for the given app. */ fun setAllowed(record: T, newAllowed: Boolean) - - @Composable - fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?) {} } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index b0f379605f5e..3ec4bb80b9cf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -107,6 +107,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public static final int BROADCAST_STATE_UNKNOWN = 0; public static final int BROADCAST_STATE_ON = 1; public static final int BROADCAST_STATE_OFF = 2; + private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27; @Retention(RetentionPolicy.SOURCE) @IntDef( @@ -1116,13 +1117,17 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private String getDefaultValueOfBroadcastName() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); - return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; + String name = BluetoothAdapter.getDefaultAdapter().getName(); + return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, + BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } private String getDefaultValueOfProgramInfo() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); - return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; + String name = BluetoothAdapter.getDefaultAdapter().getName(); + return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, + BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } private byte[] getDefaultValueOfBroadcastCode() { diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java index 985599c952d1..0d5f66794e3b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java @@ -97,6 +97,12 @@ public class DisplayDensityUtils { @Nullable private final int[] mValues; + /** + * The density values before rounding to an integer. + */ + @Nullable + private final float[] mFloatValues; + private final int mDefaultDensity; private final int mCurrentIndex; @@ -124,6 +130,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "Cannot fetch display info for the default display"); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -154,6 +161,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "No display satisfies the predicate"); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -165,6 +173,7 @@ public class DisplayDensityUtils { Log.w(LOG_TAG, "Cannot fetch default density for display " + idOfSmallestDisplay); mEntries = null; mValues = null; + mFloatValues = null; mDefaultDensity = 0; mCurrentIndex = -1; return; @@ -197,18 +206,25 @@ public class DisplayDensityUtils { String[] entries = new String[1 + numSmaller + numLarger]; int[] values = new int[entries.length]; + float[] valuesFloat = new float[entries.length]; int curIndex = 0; if (numSmaller > 0) { final float interval = (1 - minScale) / numSmaller; for (int i = numSmaller - 1; i >= 0; i--) { + // Save the float density value before rounding to be used to set the density ratio + // of overridden density to default density in WM. + final float densityFloat = defaultDensity * (1 - (i + 1) * interval); // Round down to a multiple of 2 by truncating the low bit. - final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1; + // LINT.IfChange + final int density = ((int) densityFloat) & ~1; + // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) if (currentDensity == density) { currentDensityIndex = curIndex; } - entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]); values[curIndex] = density; + valuesFloat[curIndex] = densityFloat; + entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]); curIndex++; } } @@ -217,18 +233,25 @@ public class DisplayDensityUtils { currentDensityIndex = curIndex; } values[curIndex] = defaultDensity; + valuesFloat[curIndex] = (float) defaultDensity; entries[curIndex] = res.getString(SUMMARY_DEFAULT); curIndex++; if (numLarger > 0) { final float interval = (maxScale - 1) / numLarger; for (int i = 0; i < numLarger; i++) { + // Save the float density value before rounding to be used to set the density ratio + // of overridden density to default density in WM. + final float densityFloat = defaultDensity * (1 + (i + 1) * interval); // Round down to a multiple of 2 by truncating the low bit. - final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1; + // LINT.IfChange + final int density = ((int) densityFloat) & ~1; + // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) if (currentDensity == density) { currentDensityIndex = curIndex; } values[curIndex] = density; + valuesFloat[curIndex] = densityFloat; entries[curIndex] = res.getString(SUMMARIES_LARGER[i]); curIndex++; } @@ -244,6 +267,9 @@ public class DisplayDensityUtils { values = Arrays.copyOf(values, newLength); values[curIndex] = currentDensity; + valuesFloat = Arrays.copyOf(valuesFloat, newLength); + valuesFloat[curIndex] = (float) currentDensity; + entries = Arrays.copyOf(entries, newLength); entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity); @@ -254,6 +280,7 @@ public class DisplayDensityUtils { mCurrentIndex = displayIndex; mEntries = entries; mValues = values; + mFloatValues = valuesFloat; } @Nullable @@ -348,7 +375,14 @@ public class DisplayDensityUtils { } final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); - wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId); + // Only set the ratio for external displays as Settings uses + // ScreenResolutionFragment to handle density update for internal display. + if (info.type == Display.TYPE_EXTERNAL) { + wm.setForcedDisplayDensityRatio(displayId, + mFloatValues[index] / mDefaultDensity, userId); + } else { + wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId); + } } } catch (RemoteException exc) { Log.w(LOG_TAG, "Unable to save forced display density setting"); diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index 60625f4fc703..db2fbd96408c 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -143,7 +143,7 @@ public class A11yMenuOverlayLayout { final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY); final Context uiContext = mService.createWindowContext( display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null); - final WindowManager windowManager = uiContext.getSystemService(WindowManager.class); + final WindowManager windowManager = WindowManagerUtils.getWindowManager(uiContext); mLayout = new A11yMenuFrameLayout(uiContext); updateLayoutPosition(uiContext); inflateLayoutAndSetOnTouchListener(mLayout, uiContext); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 08efe34d568c..ed87a7d35f25 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2129,6 +2129,14 @@ flag { } flag { + name: "lockscreen_font" + namespace: "systemui" + description: "Read-only flag for lockscreen font" + bug: "393610165" + is_fixed_read_only: true +} + +flag { name: "always_compose_qs_ui_fragment" namespace: "systemui" description: "Have QQS and QS scenes in the Compose fragment always composed, not just when it should be visible." diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt index c88c4ebb1a8d..6a620b3b9c34 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt @@ -35,6 +35,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALW import com.android.app.animation.Interpolators import com.android.internal.jank.Cuj.CujType import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import com.android.systemui.util.maybeForceFullscreen import com.android.systemui.util.registerAnimationOnBackInvoked import java.util.concurrent.Executor @@ -932,26 +933,29 @@ private class AnimatedDialog( } override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { - // onLaunchAnimationEnd is called by an Animator at the end of the animation, - // on a Choreographer animation tick. The following calls will move the animated - // content from the dialog overlay back to its original position, and this - // change must be reflected in the next frame given that we then sync the next - // frame of both the content and dialog ViewRoots. However, in case that content - // is rendered by Compose, whose compositions are also scheduled on a - // Choreographer frame, any state change made *right now* won't be reflected in - // the next frame given that a Choreographer frame can't schedule another and - // have it happen in the same frame. So we post the forwarded calls to - // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring - // that the move of the content back to its original window will be reflected in - // the next frame right after [onLaunchAnimationEnd] is called. - // - // TODO(b/330672236): Move this to TransitionAnimator. - dialog.context.mainExecutor.execute { + val onEnd = { startController.onTransitionAnimationEnd(isExpandingFullyAbove) endController.onTransitionAnimationEnd(isExpandingFullyAbove) - onLaunchAnimationEnd() } + if (Flags.sceneContainer()) { + onEnd() + } else { + // onLaunchAnimationEnd is called by an Animator at the end of the + // animation, on a Choreographer animation tick. The following calls will + // move the animated content from the dialog overlay back to its original + // position, and this change must be reflected in the next frame given that + // we then sync the next frame of both the content and dialog ViewRoots. + // However, in case that content is rendered by Compose, whose compositions + // are also scheduled on a Choreographer frame, any state change made *right + // now* won't be reflected in the next frame given that a Choreographer + // frame can't schedule another and have it happen in the same frame. So we + // post the forwarded calls to [Controller.onLaunchAnimationEnd], leaving + // this Choreographer frame, ensuring that the move of the content back to + // its original window will be reflected in the next frame right after + // [onLaunchAnimationEnd] is called. + dialog.context.mainExecutor.execute { onEnd() } + } } override fun onTransitionAnimationProgress( diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java index 9e08317d2c6b..041ccb567146 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java @@ -199,8 +199,10 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner info.releaseAllSurfaces(); // Make sure that the transition leashes created are not leaked. for (SurfaceControl leash : leashMap.values()) { - if (leash.isValid()) { + try { finishTransaction.reparent(leash, null); + } catch (Exception e) { + Log.e(TAG, "Failed to reparent leash", e); } } // Don't release here since launcher might still be using them. Instead diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index a4a96d19e8bb..8886b9e5e275 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -39,6 +39,7 @@ import com.android.app.animation.Interpolators.LINEAR import com.android.internal.annotations.VisibleForTesting import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce +import com.android.systemui.Flags import com.android.systemui.Flags.moveTransitionAnimationLayer import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived @@ -1014,11 +1015,7 @@ class TransitionAnimator( openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) } } - // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not - // flicker with Flexiglass enabled. - if (controller.isLaunching) { - onEnd() - } else { + if (Flags.sceneContainer() || !controller.isLaunching) { // onAnimationEnd is called at the end of the animation, on a Choreographer animation // tick. During dialog launches, the following calls will move the animated content from // the dialog overlay back to its original position, and this change must be reflected @@ -1032,6 +1029,8 @@ class TransitionAnimator( // Choreographer frame, ensuring that any state change applied by // onTransitionAnimationEnd() will be reflected in the same frame. mainExecutor.execute { onEnd() } + } else { + onEnd() } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 05958a212f47..9ba74749639a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.util.lerp +import com.android.compose.animation.scene.Element.Companion.SizeUnspecified import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.transformation.CustomPropertyTransformation @@ -105,6 +106,13 @@ internal class Element(val key: ElementKey) { var targetSize by mutableStateOf(SizeUnspecified) var targetOffset by mutableStateOf(Offset.Unspecified) + /** + * The *approach* state of this element in this content, i.e. the intermediate layout state + * during transitions, used for smooth animation. Note: These values are computed before + * measuring the children. + */ + var approachSize by mutableStateOf(SizeUnspecified) + /** The last state this element had in this content. */ var lastOffset = Offset.Unspecified var lastSize = SizeUnspecified @@ -340,7 +348,11 @@ internal class ElementNode( override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { // TODO(b/324191441): Investigate whether making this check more complex (checking if this // element is shared or transformed) would lead to better performance. - return isAnyStateTransitioning() + val isTransitioning = isAnyStateTransitioning() + if (!isTransitioning) { + stateInContent.approachSize = SizeUnspecified + } + return isTransitioning } override fun Placeable.PlacementScope.isPlacementApproachInProgress( @@ -392,6 +404,7 @@ internal class ElementNode( // sharedElement isn't part of either but the element is still rendered as part of // the underlying scene that is currently not being transitioned. val currentState = currentTransitionStates.last().last() + stateInContent.approachSize = Element.SizeUnspecified val shouldPlaceInThisContent = elementContentWhenIdle( layoutImpl, @@ -409,7 +422,14 @@ internal class ElementNode( val transition = elementState as? TransitionState.Transition val placeable = - measure(layoutImpl, element, transition, stateInContent, measurable, constraints) + approachMeasure( + layoutImpl = layoutImpl, + element = element, + transition = transition, + stateInContent = stateInContent, + measurable = measurable, + constraints = constraints, + ) stateInContent.lastSize = placeable.size() return layout(placeable.width, placeable.height) { place(elementState, placeable) } } @@ -1183,7 +1203,7 @@ private fun interruptedAlpha( ) } -private fun measure( +private fun approachMeasure( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, @@ -1214,6 +1234,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero + stateInContent.approachSize = Element.SizeUnspecified return placeable } @@ -1236,6 +1257,10 @@ private fun measure( ) }, ) + + // Important: Set approachSize before child measurement. Could be used for their calculations. + stateInContent.approachSize = interruptedSize + return measurable.measure( Constraints.fixed( interruptedSize.width.coerceAtLeast(0), diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 22688d310b44..cfd59fd316d3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -160,6 +160,13 @@ interface ElementStateScope { fun ElementKey.targetSize(content: ContentKey): IntSize? /** + * Return the *approaching* size of [this] element in the given [content], i.e. thethe size the + * element when is transitioning, or `null` if the element is not composed and measured in that + * content (yet). + */ + fun ElementKey.approachSize(content: ContentKey): IntSize? + + /** * Return the *target* offset of [this] element in the given [content], i.e. the size of the * element when idle, or `null` if the element is not composed and placed in that content (yet). */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 5d4232d8a8b7..0d5ae81c501d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -31,6 +31,12 @@ internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayo } } + override fun ElementKey.approachSize(content: ContentKey): IntSize? { + return layoutImpl.elements[this]?.stateByContent?.get(content)?.approachSize.takeIf { + it != Element.SizeUnspecified + } + } + override fun ElementKey.targetOffset(content: ContentKey): Offset? { return layoutImpl.elements[this]?.stateByContent?.get(content)?.targetOffset.takeIf { it != Offset.Unspecified diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 86cbfe4f1a8b..aff5aa097a8e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -2312,4 +2312,76 @@ class ElementTest { assertThat(compositions).isEqualTo(1) } + + @Test + fun measureElementApproachSizeBeforeChildren() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutStateForTests(SceneA, SceneTransitions.Empty) + } + + lateinit var fooHeight: () -> Dp? + val fooHeightPreChildMeasure = mutableListOf<Dp?>() + + val scope = + rule.setContentAndCreateMainScope { + val density = LocalDensity.current + SceneTransitionLayoutForTesting(state) { + scene(SceneA) { + fooHeight = { + with(density) { TestElements.Foo.approachSize(SceneA)?.height?.toDp() } + } + Box(Modifier.element(TestElements.Foo).size(200.dp)) { + Box( + Modifier.approachLayout( + isMeasurementApproachInProgress = { false }, + approachMeasure = { measurable, constraints -> + fooHeightPreChildMeasure += fooHeight() + measurable.measure(constraints).run { + layout(width, height) {} + } + }, + ) + ) + } + } + scene(SceneB) { Box(Modifier.element(TestElements.Foo).size(100.dp)) } + } + } + + var progress by mutableFloatStateOf(0f) + val transition = transition(from = SceneA, to = SceneB, progress = { progress }) + var countApproachPass = fooHeightPreChildMeasure.size + + // Idle state: Scene A. + assertThat(state.isTransitioning()).isFalse() + assertThat(fooHeight()).isNull() + + // Start transition: Scene A -> Scene B (progress 0%). + scope.launch { state.startTransition(transition) } + rule.waitForIdle() + assertThat(state.isTransitioning()).isTrue() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(200f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + // progress 50%: height is going from 200dp to 100dp, so 150dp is expected now. + progress = 0.5f + rule.waitForIdle() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(150f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + progress = 1f + rule.waitForIdle() + assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(100f) + assertThat(fooHeight()).isNotNull() + countApproachPass = fooHeightPreChildMeasure.size + + transition.finish() + rule.waitForIdle() + assertThat(state.isTransitioning()).isFalse() + assertThat(fooHeight()).isNull() + assertThat(fooHeightPreChildMeasure.size).isEqualTo(countApproachPass) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index d0f8e7863537..81bc94943b71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify; import android.content.pm.ActivityInfo; import android.testing.TestableLooper; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -31,6 +32,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.After; import org.junit.Before; @@ -56,13 +58,15 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @Mock private SecureSettings mSecureSettings; + @Mock + private WindowManagerProvider mWindowManagerProvider; @Before public void setUp() { MockitoAnnotations.initMocks(this); mMagnificationSettingsController = new MagnificationSettingsController( mContext, mSfVsyncFrameProvider, - mMagnificationSettingControllerCallback, mSecureSettings, + mMagnificationSettingControllerCallback, mSecureSettings, mWindowManagerProvider, mWindowMagnificationSettings); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt index d0762a3797c0..807cab7cf1b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt @@ -43,12 +43,15 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.whenever @SmallTest @@ -59,6 +62,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { private lateinit var underTest: WindowManagerLockscreenVisibilityManager private lateinit var executor: FakeExecutor + private lateinit var uiBgExecutor: FakeExecutor @Mock private lateinit var activityTaskManagerService: IActivityTaskManager @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -74,10 +78,12 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) + uiBgExecutor = FakeExecutor(FakeSystemClock()) underTest = WindowManagerLockscreenVisibilityManager( executor = executor, + uiBgExecutor = uiBgExecutor, activityTaskManagerService = activityTaskManagerService, keyguardStateController = keyguardStateController, keyguardSurfaceBehindAnimator = keyguardSurfaceBehindAnimator, @@ -93,8 +99,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testLockscreenVisible_andAodVisible_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, true) verifyNoMoreInteractions(activityTaskManagerService) @@ -104,8 +112,10 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testLockscreenVisible_andAodVisible_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, true) verifyNoMoreInteractions(keyguardTransitions) @@ -115,13 +125,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, true) verifyNoMoreInteractions(activityTaskManagerService) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).keyguardGoingAway(anyInt()) verifyNoMoreInteractions(activityTaskManagerService) @@ -131,13 +144,16 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setAodVisible(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, true) verifyNoMoreInteractions(keyguardTransitions) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) verifyNoMoreInteractions(keyguardTransitions) @@ -148,11 +164,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_without_keyguard_shell_transitions() { underTest.setLockscreenShown(false) underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(false, false) verifyNoMoreInteractions(activityTaskManagerService) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(activityTaskManagerService) } @@ -162,11 +180,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_with_keyguard_shell_transitions() { underTest.setLockscreenShown(false) underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) verifyNoMoreInteractions(keyguardTransitions) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @@ -175,9 +195,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_without_keyguard_shell_transitions() { underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(activityTaskManagerService) underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).setLockScreenShown(true, false) verifyNoMoreInteractions(activityTaskManagerService) } @@ -186,9 +208,11 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_with_keyguard_shell_transitions() { underTest.setAodVisible(false) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) verifyNoMoreInteractions(activityTaskManagerService) } @@ -197,10 +221,13 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(activityTaskManagerService).keyguardGoingAway(0) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @@ -208,22 +235,31 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(false, false) underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() verifyNoMoreInteractions(keyguardTransitions) } @Test @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_without_keyguard_shell_transitions() { - // Show the surface behind, then hide it. underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() + verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + + // Show the surface behind, then hide it. underTest.setSurfaceBehindVisibility(true) + uiBgExecutor.runAllReady() underTest.setSurfaceBehindVisibility(false) - verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + uiBgExecutor.runAllReady() + + verify(activityTaskManagerService, times(2)).setLockScreenShown(eq(true), any()) } @Test @@ -233,6 +269,7 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { underTest.setLockscreenShown(true) underTest.setSurfaceBehindVisibility(true) underTest.setSurfaceBehindVisibility(false) + uiBgExecutor.runAllReady() verify(keyguardTransitions).startKeyguardTransition(eq(true), any()) } @@ -258,4 +295,33 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { verify(mockedCallback).onAnimationFinished() verifyNoMoreInteractions(mockedCallback) } + + @Test + fun lockscreenEventuallyShown_ifReshown_afterGoingAwayExecutionDelayed() { + underTest.setLockscreenShown(true) + uiBgExecutor.runAllReady() + clearInvocations(activityTaskManagerService) + + // Trigger keyguardGoingAway, then immediately setLockScreenShown before going away runs on + // the uiBgExecutor. + underTest.setSurfaceBehindVisibility(true) + underTest.setLockscreenShown(true) + + // Next ready should be the keyguardGoingAway call. + uiBgExecutor.runNextReady() + verify(activityTaskManagerService).keyguardGoingAway(anyInt()) + verify(activityTaskManagerService, never()).setLockScreenShown(any(), any()) + clearInvocations(activityTaskManagerService) + + // Then, the setLockScreenShown call, which should have been enqueued when we called + // setLockScreenShown(true) even though keyguardGoingAway() hadn't yet been called. + uiBgExecutor.runNextReady() + verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) + verify(activityTaskManagerService, never()).keyguardGoingAway(anyInt()) + clearInvocations(activityTaskManagerService) + + // Shouldn't be anything left in the queue. + uiBgExecutor.runAllReady() + verifyNoMoreInteractions(activityTaskManagerService) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt index 1f1a74b6c389..63c0f4371c62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt @@ -34,7 +34,6 @@ import androidx.media.utils.MediaConstants import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.graphics.ImageLoader import com.android.systemui.graphics.imageLoader @@ -167,8 +166,6 @@ class MediaDataLoaderTest : SysuiTestCase() { @Test fun loadMediaDataForResumption_returnsMediaData() = testScope.runTest { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - val song = "THIS_IS_A_SONG" val artist = "THIS_IS_AN_ARTIST" val albumArt = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java index 9c4d93c17d00..f7298dd0bf36 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java @@ -67,6 +67,7 @@ import java.util.concurrent.Executor; import java.util.stream.Collectors; @SmallTest +@DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN) @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class MediaOutputAdapterLegacyTest extends SysuiTestCase { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt new file mode 100644 index 000000000000..70adfd324e94 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt @@ -0,0 +1,763 @@ +/* + * 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.media.dialog + +import android.content.Context +import android.graphics.drawable.Icon +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.TestableLooper.RunWithLooper +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.LinearLayout +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.graphics.drawable.IconCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.RecyclerView +import com.android.media.flags.Flags +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_GROUPING +import com.android.settingslib.media.MediaDevice +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE +import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.dialog.MediaItem.MediaItemType +import com.android.systemui.media.dialog.MediaItem.createDeviceMediaItem +import com.android.systemui.media.dialog.MediaOutputAdapter.MediaDeviceViewHolder +import com.android.systemui.media.dialog.MediaOutputAdapter.MediaGroupDividerViewHolder +import com.android.systemui.res.R +import com.google.android.material.slider.Slider +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN) +@RunWith(AndroidJUnit4::class) +@RunWithLooper(setAsMainLooper = true) +class MediaOutputAdapterTest : SysuiTestCase() { + private val mMediaSwitchingController = mock<MediaSwitchingController>() + private val mMediaDevice1: MediaDevice = mock<MediaDevice>() + private val mMediaDevice2: MediaDevice = mock<MediaDevice>() + private val mIcon: Icon = mock<Icon>() + private val mIconCompat: IconCompat = mock<IconCompat>() + private lateinit var mMediaOutputAdapter: MediaOutputAdapter + private val mMediaItems: MutableList<MediaItem> = ArrayList() + + @Before + fun setUp() { + mMediaSwitchingController.stub { + on { getMediaItemList(false) } doReturn mMediaItems + on { hasAdjustVolumeUserRestriction() } doReturn false + on { isAnyDeviceTransferring } doReturn false + on { currentConnectedMediaDevice } doReturn mMediaDevice1 + on { connectedSpeakersExpandableGroupDivider } + .doReturn( + MediaItem.createExpandableGroupDividerMediaItem( + mContext.getString(R.string.media_output_group_title_connected_speakers) + ) + ) + on { sessionVolumeMax } doReturn TEST_MAX_VOLUME + on { sessionVolume } doReturn TEST_CURRENT_VOLUME + on { sessionName } doReturn TEST_SESSION_NAME + on { colorSchemeLegacy } doReturn mock<MediaOutputColorSchemeLegacy>() + on { colorScheme } doReturn mock<MediaOutputColorScheme>() + } + + mIconCompat.stub { on { toIcon(mContext) } doReturn mIcon } + + mMediaDevice1 + .stub { + on { id } doReturn TEST_DEVICE_ID_1 + on { name } doReturn TEST_DEVICE_NAME_1 + } + .also { + whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat + } + + mMediaDevice2 + .stub { + on { id } doReturn TEST_DEVICE_ID_2 + on { name } doReturn TEST_DEVICE_NAME_2 + } + .also { + whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat + } + + mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) + } + + @Test + fun getItemCount_returnsMediaItemSize() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size) + } + + @Test + fun getItemId_forDifferentItemsTypes_returnCorrespondingHashCode() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + assertThat(mMediaOutputAdapter.getItemId(0)) + .isEqualTo(mMediaItems[0].mediaDevice.get().id.hashCode()) + } + + @Test + fun getItemId_invalidPosition_returnPosition() { + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + val invalidPosition = mMediaItems.size + 1 + + assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(RecyclerView.NO_ID) + } + + @Test + fun onBindViewHolder_bindDisconnectedDevice_verifyView() { + mMediaDevice2.stub { on { state } doReturn STATE_DISCONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleIcon.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_bindConnectedDevice_verifyView() { + mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleIcon.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_isMutingExpectedDevice_verifyView() { + mMediaDevice1.stub { + on { isMutingExpectedDevice } doReturn true + on { state } doReturn STATE_DISCONNECTED + } + mMediaSwitchingController.stub { on { isCurrentConnectedDeviceRemote } doReturn false } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() { + mMediaDevice1.stub { + on { isMutingExpectedDevice } doReturn true + on { state } doReturn STATE_CONNECTED + } + mMediaSwitchingController.stub { + on { hasMutingExpectedDevice() } doReturn true + on { isCurrentConnectedDeviceRemote } doReturn false + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + } + } + + @Test + fun onBindViewHolder_initSeekbar_setsVolume() { + mMediaDevice1.stub { + on { state } doReturn STATE_CONNECTED + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME) + assertThat(mSlider.valueFrom).isEqualTo(0) + assertThat(mSlider.valueTo).isEqualTo(TEST_MAX_VOLUME) + } + } + + @Test + fun onBindViewHolder_dragSeekbar_adjustsVolume() { + mMediaDevice1.stub { + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + + var sliderChangeListener: Slider.OnChangeListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnChangeListener(listener: OnChangeListener) { + sliderChangeListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 0) + sliderChangeListener?.onValueChange(viewHolder.mSlider, 5f, true) + + verify(mMediaSwitchingController).adjustVolume(mMediaDevice1, 5) + } + + @Test + fun onBindViewHolder_dragSeekbar_logsInteraction() { + mMediaDevice1 + .stub { + on { maxVolume } doReturn TEST_MAX_VOLUME + on { currentVolume } doReturn TEST_CURRENT_VOLUME + } + .also { mMediaItems.add(createDeviceMediaItem(it)) } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + + var sliderTouchListener: Slider.OnSliderTouchListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnSliderTouchListener(listener: OnSliderTouchListener) { + sliderTouchListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 0) + sliderTouchListener?.onStopTrackingTouch(viewHolder.mSlider) + + verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1) + } + + @Test + fun onBindViewHolder_bindSelectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + assertThat(mDivider.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_add_device_to_group)) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + + mGroupButton.performClick() + } + verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2) + } + + @Test + fun onBindViewHolder_bindDeselectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectedMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2) + on { deselectableMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_remove_device_from_group)) + mGroupButton.performClick() + } + + verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice2) + } + + @Test + fun onBindViewHolder_bindNonDeselectableDevice_verifyView() { + mMediaSwitchingController.stub { + on { selectedMediaDevice } doReturn listOf(mMediaDevice1) + on { deselectableMediaDevice } doReturn ArrayList() + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_bindFailedStateDevice_verifyView() { + mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING_FAILED } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mStatusIcon.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.text.toString()) + .isEqualTo(mContext.getText(R.string.media_output_dialog_connect_failed).toString()) + } + } + + @Test + fun onBindViewHolder_deviceHasSubtext_displaySubtitle() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { hasSubtext() } doReturn true + on { subtextString } doReturn TEST_CUSTOM_SUBTEXT + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSubTitleText.text.toString()).isEqualTo(TEST_CUSTOM_SUBTEXT) + } + } + + @Test + fun onBindViewHolder_deviceWithOngoingSession_displaysGoToAppButton() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { hasOngoingSession() } doReturn true + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mOngoingSessionButton.visibility).isEqualTo(VISIBLE) + assertThat(mOngoingSessionButton.contentDescription) + .isEqualTo(mContext.getString(R.string.accessibility_open_application)) + mOngoingSessionButton.performClick() + } + + verify(mMediaSwitchingController) + .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mOngoingSessionButton) + } + + @Test + fun onItemClick_selectionBehaviorTransfer_connectsDevice() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @Test + fun onItemClick_selectionBehaviorTransferAndSessionHost_showsEndSessionDialog() { + mMediaSwitchingController.stub { + on { isCurrentOutputDeviceHasSessionOngoing() } doReturn true + } + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE, + ) as MediaDeviceViewHolder + val spyMediaDeviceViewHolder = spy(viewHolder) + doNothing().whenever(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2) + + mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0) + spyMediaDeviceViewHolder.mMainContent.performClick() + + verify(mMediaSwitchingController, never()).connectDevice(ArgumentMatchers.any()) + verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2) + } + + @Test + fun onItemClick_selectionBehaviorGoToApp_sendsLaunchIntent() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_GO_TO_APP + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + val viewHolder = + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + verify(mMediaSwitchingController) + .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mMainContent) + } + + @Test + fun onItemClick_selectionBehaviorNone_doesNothing() { + mMediaDevice2.stub { + on { state } doReturn STATE_DISCONNECTED + on { selectionBehavior } doReturn SELECTION_BEHAVIOR_NONE + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + + verify(mMediaSwitchingController, never()).tryToLaunchInAppRoutingIntent(any(), any()) + verify(mMediaSwitchingController, never()).connectDevice(any()) + } + + @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOff_verifyConnectDevice() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() { + mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn true } + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { transferableMediaDevices } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController).connectDevice(mMediaDevice2) + } + + @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT) + @Test + fun clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() { + mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn false } + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { transferableMediaDevices } doReturn listOf() + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + mMainContent.performClick() + } + verify(mMediaSwitchingController, never()).connectDevice(any()) + } + + @Test + fun onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() { + mMediaSwitchingController.stub { on { isAnyDeviceTransferring() } doReturn true } + mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + // Connected device, looks like disconnected during transfer + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(GONE) + } + + // Connecting device + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onBindViewHolder_bindGroupingDevice_verifyView() { + mMediaDevice1.stub { on { state } doReturn STATE_GROUPING } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(GONE) + assertThat(mSubTitleText.visibility).isEqualTo(GONE) + assertThat(mGroupButton.visibility).isEqualTo(GONE) + assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE) + } + } + + @Test + fun onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() { + mMediaSwitchingController.stub { + on { hasMutingExpectedDevice() } doReturn true + on { isCurrentConnectedDeviceRemote() } doReturn false + } + mMediaDevice1.stub { on { isMutingExpectedDevice } doReturn false } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() } + verify(mMediaSwitchingController).cancelMuteAwaitConnection() + } + + @Test + fun onGroupActionTriggered_clicksSelectableDevice_triggerGrouping() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + } + updateAdapterWithDevices(listOf(mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() } + verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2) + } + + @Test + fun onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn listOf(mMediaDevice2) + on { selectedMediaDevice } doReturn listOf(mMediaDevice1) + on { deselectableMediaDevice } doReturn listOf(mMediaDevice1) + on { isCurrentConnectedDeviceRemote } doReturn true + } + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + + createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() } + verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1) + } + + @Test + fun onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() { + mMediaSwitchingController.stub { + on { isCurrentConnectedDeviceRemote } doReturn true + on { hasAdjustVolumeUserRestriction() } doReturn true + } + mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(GONE) + } + } + + @Test + fun onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() { + mMediaSwitchingController.stub { + on { isVolumeControlEnabled(mMediaDevice1) } doReturn false + } + mMediaDevice1.stub { + on { state } doReturn STATE_CONNECTED + on { currentVolume } doReturn TEST_CURRENT_VOLUME + on { maxVolume } doReturn TEST_MAX_VOLUME + } + updateAdapterWithDevices(listOf(mMediaDevice1)) + + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.isEnabled).isFalse() + } + + mMediaSwitchingController.stub { + on { isVolumeControlEnabled(mMediaDevice1) } doReturn true + } + createAndBindDeviceViewHolder(position = 0).apply { + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.isEnabled).isTrue() + } + } + + @Test + fun updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() { + mMediaOutputAdapter.updateItems() + val updatedList: MutableList<MediaItem> = ArrayList() + updatedList.add(MediaItem.createDeviceGroupMediaItem()) + whenever(mMediaSwitchingController.getMediaItemList(false)).doReturn(updatedList) + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size) + + mMediaOutputAdapter.updateItems() + assertThat(mMediaOutputAdapter.itemCount).isEqualTo(updatedList.size) + } + + @Test + fun multipleSelectedDevices_listCollapsed_verifyItemTypes() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + with(mMediaOutputAdapter) { + assertThat(itemCount).isEqualTo(2) + assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER) + assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP) + } + } + + @Test + fun multipleSelectedDevices_listCollapsed_verifySessionControl() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME) + } + + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_DEVICE_GROUP, + ) as MediaDeviceViewHolder + + var sliderChangeListener: Slider.OnChangeListener? = null + viewHolder.mSlider = + object : Slider(contextWithTheme(mContext)) { + override fun addOnChangeListener(listener: OnChangeListener) { + sliderChangeListener = listener + } + } + mMediaOutputAdapter.onBindViewHolder(viewHolder, 1) + sliderChangeListener?.onValueChange(viewHolder.mSlider, 7f, true) + + verify(mMediaSwitchingController).adjustSessionVolume(7) + } + + @Test + fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() { + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true } + initializeSession() + + val groupDividerViewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + MediaItemType.TYPE_GROUP_DIVIDER, + ) as MediaGroupDividerViewHolder + mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0) + + mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false } + groupDividerViewHolder.mExpandButton.performClick() + + createAndBindDeviceViewHolder(position = 1).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + } + + createAndBindDeviceViewHolder(position = 2).apply { + assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2) + assertThat(mSlider.visibility).isEqualTo(VISIBLE) + assertThat(mTitleText.visibility).isEqualTo(VISIBLE) + assertThat(mGroupButton.visibility).isEqualTo(VISIBLE) + } + } + + private fun contextWithTheme(context: Context) = + ContextThemeWrapper( + context, + com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight, + ) + + private fun updateAdapterWithDevices(deviceList: List<MediaDevice>) { + for (device in deviceList) { + mMediaItems.add(createDeviceMediaItem(device)) + } + mMediaOutputAdapter.updateItems() + } + + private fun createAndBindDeviceViewHolder(position: Int): MediaDeviceViewHolder { + val viewHolder = + mMediaOutputAdapter.onCreateViewHolder( + LinearLayout(mContext), + mMediaOutputAdapter.getItemViewType(position), + ) + if (viewHolder is MediaDeviceViewHolder) { + mMediaOutputAdapter.onBindViewHolder(viewHolder, position) + return viewHolder + } else { + throw RuntimeException("ViewHolder for position $position is not MediaDeviceViewHolder") + } + } + + private fun initializeSession() { + val selectedDevices = listOf(mMediaDevice1, mMediaDevice2) + mMediaSwitchingController.stub { + on { selectableMediaDevice } doReturn selectedDevices + on { selectedMediaDevice } doReturn selectedDevices + on { deselectableMediaDevice } doReturn selectedDevices + } + mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController) + updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2)) + } + + companion object { + private const val TEST_DEVICE_NAME_1 = "test_device_name_1" + private const val TEST_DEVICE_NAME_2 = "test_device_name_2" + private const val TEST_DEVICE_ID_1 = "test_device_id_1" + private const val TEST_DEVICE_ID_2 = "test_device_id_2" + private const val TEST_SESSION_NAME = "test_session_name" + private const val TEST_CUSTOM_SUBTEXT = "custom subtext" + + private const val TEST_MAX_VOLUME = 20 + private const val TEST_CURRENT_VOLUME = 10 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt index 72d21f1064af..81f2bc94a307 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar +import android.content.res.Resources import android.telephony.ServiceState import android.telephony.SubscriptionInfo import android.telephony.TelephonyManager import android.telephony.telephonyManager +import android.testing.TestableResources import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.keyguardUpdateMonitor @@ -70,10 +72,18 @@ class OperatorNameViewControllerTest : SysuiTestCase() { private val airplaneModeRepository = FakeAirplaneModeRepository() private val connectivityRepository = FakeConnectivityRepository() + @Mock private lateinit var resources: Resources + private lateinit var testableResources: TestableResources + @Before fun setup() { MockitoAnnotations.initMocks(this) + testableResources = mContext.getOrCreateTestableResources() + testableResources.addOverride( + com.android.internal.R.integer.config_showOperatorNameDefault, + 1) + airplaneModeInteractor = AirplaneModeInteractor( airplaneModeRepository, diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index e7d6b2fe08f4..877360594fc6 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -137,7 +137,13 @@ <item name="android:gravity">start</item> <item name="android:ellipsize">end</item> <item name="android:maxLines">2</item> - <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.lockscreen_font"> + @*android:string/config_headlineFontFamily + </item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.lockscreen_font"> + variable-title-small + </item> + <item name="android:fontFamily"></item> <item name="android:shadowColor">@color/keyguard_shadow_color</item> <item name="android:shadowRadius">?attr/shadowRadius</item> </style> diff --git a/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml new file mode 100644 index 000000000000..467a3813e461 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <path + android:pathData="M11,13V16C11,16.283 11.092,16.525 11.275,16.725C11.475,16.908 11.717,17 12,17C12.283,17 12.517,16.908 12.7,16.725C12.9,16.525 13,16.283 13,16V13H16C16.283,13 16.517,12.908 16.7,12.725C16.9,12.525 17,12.283 17,12C17,11.717 16.9,11.483 16.7,11.3C16.517,11.1 16.283,11 16,11H13V8C13,7.717 12.9,7.483 12.7,7.3C12.517,7.1 12.283,7 12,7C11.717,7 11.475,7.1 11.275,7.3C11.092,7.483 11,7.717 11,8V11H8C7.717,11 7.475,11.1 7.275,11.3C7.092,11.483 7,11.717 7,12C7,12.283 7.092,12.525 7.275,12.725C7.475,12.908 7.717,13 8,13H11ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22ZM12,20C14.233,20 16.125,19.225 17.675,17.675C19.225,16.125 20,14.233 20,12C20,9.767 19.225,7.875 17.675,6.325C16.125,4.775 14.233,4 12,4C9.767,4 7.875,4.775 6.325,6.325C4.775,7.875 4,9.767 4,12C4,14.233 4.775,16.125 6.325,17.675C7.875,19.225 9.767,20 12,20Z" + android:fillColor="#ffffff"/> + </group> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml new file mode 100644 index 000000000000..935733c3333d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <path + android:pathData="M10.6,13.8L8.45,11.65C8.267,11.467 8.033,11.375 7.75,11.375C7.467,11.375 7.233,11.467 7.05,11.65C6.867,11.833 6.775,12.067 6.775,12.35C6.775,12.633 6.867,12.867 7.05,13.05L9.9,15.9C10.1,16.1 10.333,16.2 10.6,16.2C10.867,16.2 11.1,16.1 11.3,15.9L16.95,10.25C17.133,10.067 17.225,9.833 17.225,9.55C17.225,9.267 17.133,9.033 16.95,8.85C16.767,8.667 16.533,8.575 16.25,8.575C15.967,8.575 15.733,8.667 15.55,8.85L10.6,13.8ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z" + android:fillColor="#ffffff"/> + </group> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml new file mode 100644 index 000000000000..5570fcfdab28 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + <path + android:pathData="M8,5.25C8.097,5.25 8.194,5.264 8.292,5.292C8.375,5.333 8.451,5.389 8.521,5.458L12.479,9.417C12.632,9.569 12.708,9.743 12.708,9.938C12.694,10.146 12.611,10.326 12.458,10.479C12.306,10.632 12.132,10.708 11.938,10.708C11.729,10.708 11.549,10.632 11.396,10.479L8,7.063L4.583,10.479C4.431,10.632 4.257,10.701 4.063,10.688C3.854,10.688 3.674,10.611 3.521,10.458C3.368,10.306 3.292,10.125 3.292,9.917C3.292,9.722 3.368,9.549 3.521,9.396L7.479,5.458C7.549,5.389 7.632,5.333 7.729,5.292C7.813,5.264 7.903,5.25 8,5.25Z" + android:fillColor="#ffffffff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml new file mode 100644 index 000000000000..dec620e54995 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportHeight="16" + android:viewportWidth="16"> + <path + android:pathData="M8,10.75C7.903,10.75 7.806,10.736 7.708,10.708C7.625,10.667 7.549,10.611 7.479,10.542L3.521,6.583C3.368,6.431 3.292,6.257 3.292,6.063C3.306,5.854 3.389,5.674 3.542,5.521C3.694,5.368 3.868,5.292 4.063,5.292C4.271,5.292 4.451,5.368 4.604,5.521L8,8.938L11.417,5.521C11.569,5.368 11.743,5.299 11.938,5.313C12.146,5.313 12.326,5.389 12.479,5.542C12.632,5.694 12.708,5.875 12.708,6.083C12.708,6.278 12.632,6.451 12.479,6.604L8.521,10.542C8.451,10.611 8.368,10.667 8.271,10.708C8.188,10.736 8.097,10.75 8,10.75Z" + android:fillColor="#ffffffff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml new file mode 100644 index 000000000000..f78212b44828 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/media_output_dialog_corner_radius" /> + <solid android:color="@color/media_dialog_surface_container" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml new file mode 100644 index 000000000000..2d27ac1612a9 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml @@ -0,0 +1,22 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/media_output_dialog_corner_radius" + android:bottomRightRadius="@dimen/media_output_dialog_corner_radius" /> + <solid android:color="@color/media_dialog_surface_container" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml new file mode 100644 index 000000000000..38db2da37f7c --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="20dp" /> + <solid android:color="@color/media_dialog_primary" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml new file mode 100644 index 000000000000..d23c1837c501 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple android:color="?android:colorControlHighlight" + xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/white" /> + <corners android:radius="20dp" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml new file mode 100644 index 000000000000..8fc8744a3827 --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners + android:radius="@dimen/media_output_item_expand_icon_height"/> + <size + android:width="@dimen/media_output_item_expand_icon_width" + android:height="@dimen/media_output_item_expand_icon_height" /> + <solid android:color="@color/media_dialog_on_surface" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index 9b629ace76af..15657284030d 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -97,6 +97,23 @@ </LinearLayout> </LinearLayout> + <LinearLayout + android:id="@+id/quick_access_shelf" + android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/connect_device" + app:icon="@drawable/ic_add" + style="@style/MediaOutput.Dialog.QuickAccessButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/media_output_dialog_button_connect_device" + android:layout_marginBottom="8dp"/> + </LinearLayout> + <ViewStub android:id="@+id/broadcast_qrcode" android:layout="@layout/media_output_broadcast_area" @@ -123,13 +140,15 @@ </androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout + android:id="@+id/dialog_footer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginStart="@dimen/dialog_side_padding" - android:layout_marginEnd="@dimen/dialog_side_padding" - android:layout_marginBottom="@dimen/dialog_bottom_padding" - android:orientation="horizontal"> + android:paddingTop="4dp" + android:paddingStart="@dimen/dialog_side_padding" + android:paddingEnd="@dimen/dialog_side_padding" + android:paddingBottom="@dimen/dialog_bottom_padding" + android:orientation="horizontal" + android:gravity="end"> <Button android:id="@+id/stop" @@ -140,6 +159,7 @@ android:visibility="gone"/> <Space + android:id="@+id/footer_spacer" android:layout_weight="1" android:layout_width="0dp" android:layout_height="match_parent"/> diff --git a/packages/SystemUI/res/layout/media_output_list_item_device.xml b/packages/SystemUI/res/layout/media_output_list_item_device.xml new file mode 100644 index 000000000000..29d5bfcc1743 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_list_item_device.xml @@ -0,0 +1,141 @@ +<?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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/item_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:baselineAligned="false" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/main_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start|center_vertical" + android:background="?android:attr/selectableItemBackground" + android:focusable="true" + android:orientation="horizontal" + android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:paddingVertical="@dimen/media_output_item_content_vertical_margin"> + + <ImageView + android:id="@+id/title_icon" + style="@style/MediaOutput.Item.Icon" + android:layout_marginEnd="@dimen/media_output_item_horizontal_gap" + android:importantForAccessibility="no" + tools:src="@drawable/ic_smartphone" + tools:visibility="visible"/> + + <LinearLayout + android:id="@+id/text_container" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:minHeight="@dimen/media_output_item_icon_size" + android:layout_gravity="start" + android:gravity="center_vertical|start" + android:orientation="vertical"> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="variable-title-small" + android:ellipsize="end" + android:maxLines="1" /> + + <TextView + android:id="@+id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="variable-title-small" + android:alpha="@dimen/media_output_item_subtitle_alpha" + android:maxLines="1" + android:singleLine="true" /> + </LinearLayout> + + <ImageView + android:id="@+id/status_icon" + style="@style/MediaOutput.Item.Icon" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:importantForAccessibility="no" + android:visibility="gone" + app:tint="@color/media_dialog_on_surface_variant" + tools:src="@drawable/media_output_status_failed" + tools:visibility="visible" /> + + <ProgressBar + android:id="@+id/loading_indicator" + style="?android:attr/progressBarStyleSmallTitle" + android:layout_width="@dimen/media_output_item_icon_size" + android:layout_height="@dimen/media_output_item_icon_size" + android:padding="@dimen/media_output_item_icon_padding" + android:scaleType="fitCenter" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:indeterminate="true" + android:indeterminateOnly="true" + android:visibility="gone" + tools:indeterminateTint="@color/media_dialog_on_surface_variant" + tools:visibility="visible" /> + + <View + android:id="@+id/divider" + android:layout_width="1dp" + android:layout_height="@dimen/media_output_item_icon_size" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:background="@color/media_dialog_outline" + android:visibility="visible" + /> + + <ImageButton + android:id="@+id/ongoing_session_button" + style="@style/MediaOutput.Item.Icon" + android:src="@drawable/ic_sound_bars_anim" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" + android:contentDescription="@string/accessibility_open_application" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:visibility="gone" + tools:visibility="visible"/> + + <ImageButton + android:id="@+id/group_button" + style="@style/MediaOutput.Item.Icon" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:src="@drawable/ic_add_circle_rounded" + android:background="@drawable/media_output_dialog_round_button_ripple" + android:focusable="true" + android:contentDescription="@null" + android:visibility="gone" + tools:visibility="visible"/> + </LinearLayout> + + <com.google.android.material.slider.Slider + android:id="@+id/volume_seekbar" + android:layout_width="match_parent" + android:layout_height="44dp" + android:layout_marginVertical="3dp" + android:theme="@style/Theme.Material3.DynamicColors.DayNight" + app:labelBehavior="gone" + app:tickVisible="false" + app:trackCornerSize="12dp" + app:trackHeight="32dp" + app:trackIconSize="20dp" + app:trackStopIndicatorSize="0dp" /> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml new file mode 100644 index 000000000000..f8c6c1f9f616 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml @@ -0,0 +1,70 @@ +<?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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:orientation="vertical"> + + <View + android:id="@+id/top_separator" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginVertical="8dp" + android:background="@color/media_dialog_outline_variant" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="40dp" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <TextView + android:id="@+id/title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:accessibilityHeading="true" + android:ellipsize="end" + android:maxLines="1" + android:fontFamily="variable-label-large-emphasized" + android:gravity="center_vertical|start" /> + + <FrameLayout + android:id="@+id/expand_button" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginStart="@dimen/media_output_item_horizontal_gap" + android:contentDescription="@string/accessibility_open_application" + android:focusable="true" + android:visibility="gone"> + + <ImageView + android:id="@+id/expand_button_icon" + android:layout_width="@dimen/media_output_item_expand_icon_width" + android:layout_height="@dimen/media_output_item_expand_icon_height" + android:layout_gravity="center" + android:background="@drawable/media_output_item_expandable_button_background" + android:contentDescription="@null" + android:focusable="false" + android:scaleType="centerInside" /> + </FrameLayout> + </LinearLayout> +</LinearLayout> + diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 85182a02faaf..3dd01996afb8 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -76,6 +76,16 @@ <color name="media_dialog_seekbar_progress">@color/material_dynamic_secondary40</color> <color name="media_dialog_button_background">@color/material_dynamic_primary70</color> <color name="media_dialog_solid_button_text">@color/material_dynamic_secondary20</color> + <color name="media_dialog_primary">@android:color/system_primary_dark</color> + <color name="media_dialog_on_primary">@android:color/system_on_primary_dark</color> + <color name="media_dialog_secondary">@android:color/system_secondary_dark</color> + <color name="media_dialog_secondary_container">@android:color/system_secondary_container_dark</color> + <color name="media_dialog_surface_container">@android:color/system_surface_container_dark</color> + <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_dark</color> + <color name="media_dialog_on_surface">@android:color/system_on_surface_dark</color> + <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_dark</color> + <color name="media_dialog_outline">@android:color/system_outline_dark</color> + <color name="media_dialog_outline_variant">@android:color/system_outline_variant_dark</color> <!-- Biometric dialog colors --> <color name="biometric_dialog_gray">#ffcccccc</color> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index fe65f32c6eb0..cb656ca0a108 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -210,6 +210,16 @@ <color name="media_dialog_seekbar_progress">@android:color/system_accent1_200</color> <color name="media_dialog_button_background">@color/material_dynamic_primary40</color> <color name="media_dialog_solid_button_text">@color/material_dynamic_neutral95</color> + <color name="media_dialog_primary">@android:color/system_primary_light</color> + <color name="media_dialog_on_primary">@android:color/system_on_primary_light</color> + <color name="media_dialog_secondary">@android:color/system_secondary_light</color> + <color name="media_dialog_secondary_container">@android:color/system_secondary_container_light</color> + <color name="media_dialog_surface_container">@android:color/system_surface_container_light</color> + <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_light</color> + <color name="media_dialog_on_surface">@android:color/system_on_surface_light</color> + <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_light</color> + <color name="media_dialog_outline">@android:color/system_outline_light</color> + <color name="media_dialog_outline_variant">@android:color/system_outline_variant_light</color> <!-- controls --> <color name="control_primary_text">#E6FFFFFF</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ca984881713b..f062bd1d4990 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1567,8 +1567,20 @@ <dimen name="media_output_dialog_item_height">64dp</dimen> <dimen name="media_output_dialog_margin_horizontal">16dp</dimen> <dimen name="media_output_dialog_list_padding_top">8dp</dimen> + <dimen name="media_output_dialog_app_icon_size">16dp</dimen> + <dimen name="media_output_dialog_app_icon_bottom_margin">11dp</dimen> <dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen> <dimen name="media_output_dialog_icon_right_radius">0dp</dimen> + <dimen name="media_output_dialog_corner_radius">20dp</dimen> + <dimen name="media_output_dialog_button_gap">8dp</dimen> + <dimen name="media_output_item_content_vertical_margin">8dp</dimen> + <dimen name="media_output_item_content_vertical_margin_active">4dp</dimen> + <dimen name="media_output_item_horizontal_gap">12dp</dimen> + <dimen name="media_output_item_icon_size">40dp</dimen> + <dimen name="media_output_item_icon_padding">8dp</dimen> + <dimen name="media_output_item_expand_icon_width">28dp</dimen> + <dimen name="media_output_item_expand_icon_height">20dp</dimen> + <item name="media_output_item_subtitle_alpha" format="float" type="dimen">0.8</item> <!-- Distance that the full shade transition takes in order to complete by tapping on a button like "expand". --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6cf1017e0495..bbf56936d560 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -592,6 +592,9 @@ <!-- Content description of the button to expand the group of devices. [CHAR LIMIT=NONE] --> <string name="accessibility_expand_group">Expand group.</string> + <!-- Content description of the button to collapse the group of devices. [CHAR LIMIT=NONE] --> + <string name="accessibility_collapse_group">Collapse group.</string> + <!-- Content description of the button to add a device to a group. [CHAR LIMIT=NONE] --> <string name="accessibility_add_device_to_group">Add device to group.</string> @@ -3293,6 +3296,8 @@ <string name="media_output_dialog_connect_failed">Can\'t switch. Tap to try again.</string> <!-- Title for connecting item [CHAR LIMIT=60] --> <string name="media_output_dialog_pairing_new">Connect a device</string> + <!-- Button text for connecting a new device [CHAR LIMIT=60] --> + <string name="media_output_dialog_button_connect_device">Connect Device</string> <!-- App name when can't get app name [CHAR LIMIT=60] --> <string name="media_output_dialog_unknown_launch_app_name">Unknown app</string> <!-- Button text for stopping casting [CHAR LIMIT=60] --> @@ -3303,6 +3308,8 @@ <string name="media_output_dialog_accessibility_seekbar">Volume</string> <!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] --> <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string> + <!-- Title for Connected speakers expandable group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title_connected_speakers">Connected speakers</string> <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index bde750145ff7..fb72123a0a3b 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -708,6 +708,33 @@ <item name="android:colorBackground">@color/media_dialog_background</item> </style> + <style name="MediaOutput" /> + <style name="MediaOutput.Dialog" /> + <style name="MediaOutput.Dialog.QuickAccessButton" parent="@style/Widget.Material3.Button.OutlinedButton.Icon"> + <item name="theme">@style/Theme.Material3.DynamicColors.DayNight</item> + <item name="android:paddingTop">6dp</item> + <item name="android:minHeight">32dp</item> + <item name="android:paddingBottom">6dp</item> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingEnd">12dp</item> + <item name="android:insetTop">0dp</item> + <item name="android:insetBottom">0dp</item> + <item name="android:textColor">@color/media_dialog_on_surface_variant</item> + <item name="iconSize">18dp</item> + <item name="iconTint">@color/media_dialog_primary</item> + <item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.Small</item> + <item name="strokeColor">@color/media_dialog_outline_variant</item> + </style> + + <style name="MediaOutput.Item" /> + <style name="MediaOutput.Item.Icon"> + <item name="android:layout_width">@dimen/media_output_item_icon_size</item> + <item name="android:layout_height">@dimen/media_output_item_icon_size</item> + <item name="android:padding">@dimen/media_output_item_icon_padding</item> + <item name="android:scaleType">fitCenter</item> + <item name="tint">@color/media_dialog_on_surface</item> + </style> + <style name="MediaOutputItemInactiveTitle"> <item name="android:textSize">16sp</item> <item name="android:textColor">@color/media_dialog_item_main_content</item> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index c6071a006408..63d56e662a50 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -53,6 +53,7 @@ import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -96,17 +97,19 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final WindowMagnifierCallback mWindowMagnifierCallback; private final SysUiState mSysUiState; private final SecureSettings mSecureSettings; + private final WindowManagerProvider mWindowManagerProvider; WindowMagnificationControllerSupplier(Context context, Handler handler, WindowMagnifierCallback windowMagnifierCallback, DisplayManager displayManager, SysUiState sysUiState, - SecureSettings secureSettings) { + SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mHandler = handler; mWindowMagnifierCallback = windowMagnifierCallback; mSysUiState = sysUiState; mSecureSettings = secureSettings; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -114,8 +117,9 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks final Context windowContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); + final WindowManager windowManager = mWindowManagerProvider + .getWindowManager(windowContext); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); - final WindowManager windowManager = windowContext.getSystemService(WindowManager.class); Supplier<SurfaceControlViewHost> scvhSupplier = () -> new SurfaceControlViewHost(mContext, @@ -146,17 +150,20 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Executor mExecutor; private final DisplayManager mDisplayManager; private final IWindowManager mIWindowManager; + private final WindowManagerProvider mWindowManagerProvider; FullscreenMagnificationControllerSupplier(Context context, DisplayManager displayManager, Handler handler, - Executor executor, IWindowManager iWindowManager) { + Executor executor, IWindowManager iWindowManager, + WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mHandler = handler; mExecutor = executor; mDisplayManager = displayManager; mIWindowManager = iWindowManager; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -172,7 +179,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mExecutor, mDisplayManager, windowContext.getSystemService(AccessibilityManager.class), - windowContext.getSystemService(WindowManager.class), + mWindowManagerProvider.getWindowManager(windowContext), mIWindowManager, scvhSupplier); } @@ -188,15 +195,17 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Context mContext; private final MagnificationSettingsController.Callback mSettingsControllerCallback; private final SecureSettings mSecureSettings; + private final WindowManagerProvider mWindowManagerProvider; SettingsSupplier(Context context, MagnificationSettingsController.Callback settingsControllerCallback, DisplayManager displayManager, - SecureSettings secureSettings) { + SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mSettingsControllerCallback = settingsControllerCallback; mSecureSettings = secureSettings; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -204,12 +213,12 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks final Context windowContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); - return new MagnificationSettingsController( windowContext, new SfVsyncFrameCallbackProvider(), mSettingsControllerCallback, - mSecureSettings); + mSecureSettings, + mWindowManagerProvider); } } @@ -223,10 +232,12 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks SysUiState sysUiState, LauncherProxyService launcherProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, - IWindowManager iWindowManager, AccessibilityManager accessibilityManager) { + IWindowManager iWindowManager, AccessibilityManager accessibilityManager, + WindowManagerProvider windowManagerProvider) { this(context, mainHandler.getLooper(), executor, commandQueue, modeSwitchesController, sysUiState, launcherProxyService, secureSettings, - displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager); + displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager, + windowManagerProvider); } @VisibleForTesting @@ -236,7 +247,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, - AccessibilityManager accessibilityManager) { + AccessibilityManager accessibilityManager, + WindowManagerProvider windowManagerProvider) { mHandler = new Handler(looper) { @Override public void handleMessage(@NonNull Message msg) { @@ -255,11 +267,13 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mA11yLogger = a11yLogger; mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context, mHandler, mWindowMagnifierCallback, - displayManager, sysUiState, secureSettings); + displayManager, sysUiState, secureSettings, windowManagerProvider); mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier( - context, displayManager, mHandler, mExecutor, iWindowManager); + context, displayManager, mHandler, mExecutor, iWindowManager, + windowManagerProvider); mMagnificationSettingsSupplier = new SettingsSupplier(context, - mMagnificationSettingsControllerCallback, displayManager, secureSettings); + mMagnificationSettingsControllerCallback, displayManager, secureSettings, + windowManagerProvider); mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index 5af34f4ddb34..95206b88f6aa 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -30,6 +30,7 @@ import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; /** * A class to control {@link WindowMagnificationSettings} and receive settings panel callbacks by @@ -60,8 +61,10 @@ public class MagnificationSettingsController implements ComponentCallbacks { @UiContext Context context, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, - SecureSettings secureSettings) { - this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, null); + SecureSettings secureSettings, + WindowManagerProvider windowManagerProvider) { + this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, + windowManagerProvider, null); } @VisibleForTesting @@ -70,6 +73,7 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, + WindowManagerProvider windowManagerProvider, WindowMagnificationSettings windowMagnificationSettings) { mContext = context.createWindowContext( context.getDisplay(), @@ -82,10 +86,10 @@ public class MagnificationSettingsController implements ComponentCallbacks { if (windowMagnificationSettings != null) { mWindowMagnificationSettings = windowMagnificationSettings; } else { - WindowManager wm = mContext.getSystemService(WindowManager.class); + WindowManager windowManager = windowManagerProvider.getWindowManager(mContext); mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, - sfVsyncFrameProvider, secureSettings, wm); + sfVsyncFrameProvider, secureSettings, windowManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java index 6d58443d5c8c..7a60cce63a33 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -36,6 +36,7 @@ import com.android.systemui.clipboardoverlay.IntentCreator; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import dagger.Lazy; import dagger.Module; @@ -85,8 +86,9 @@ public interface ClipboardOverlayModule { */ @Provides @OverlayWindowContext - static WindowManager provideWindowManager(@OverlayWindowContext Context context) { - return context.getSystemService(WindowManager.class); + static WindowManager provideWindowManager(@OverlayWindowContext Context context, + WindowManagerProvider windowManagerProvider) { + return windowManagerProvider.getWindowManager(context); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt index 792d3288e96a..aaaaacef001a 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt @@ -31,6 +31,7 @@ import com.android.systemui.display.shared.model.DisplayWindowProperties import com.android.systemui.res.R import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.utils.windowmanager.WindowManagerUtils import com.google.common.collect.HashBasedTable import com.google.common.collect.Table import java.io.PrintWriter @@ -110,7 +111,7 @@ constructor( return null } @SuppressLint("NonInjectedService") // Need to manually get the service - val windowManager = context.getSystemService(WindowManager::class.java) + val windowManager = WindowManagerUtils.getWindowManager(context) val layoutInflater = LayoutInflater.from(context) DisplayWindowProperties(displayId, windowType, context, windowManager, layoutInflater) } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 0640351c8149..d9f9a3ea1032 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -121,16 +121,8 @@ constructor( InputManager.KeyGestureEventListener { event -> // Only store keyboard shortcut time for gestures providing keyboard // education - val shortcutType = - when (event.keyGestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, - KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS - - else -> null - } - - if (shortcutType != null) { - trySendWithFailureLogging(shortcutType, TAG) + if (event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) { + trySendWithFailureLogging(ALL_APPS, TAG) } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 84bb23140ae7..9a37439e7486 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -200,9 +200,6 @@ object Flags { // TODO(b/266157412): Tracking Bug val MEDIA_RETAIN_SESSIONS = unreleasedFlag("media_retain_sessions") - // TODO(b/267007629): Tracking Bug - val MEDIA_RESUME_PROGRESS = releasedFlag("media_resume_progress") - // TODO(b/270437894): Tracking Bug val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt index 51b953ef290c..979c7ceb239b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt @@ -25,6 +25,7 @@ import android.view.WindowManager import com.android.internal.widget.LockPatternUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardShowWhileAwakeInteractor import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier @@ -44,6 +45,7 @@ class WindowManagerLockscreenVisibilityManager @Inject constructor( @Main private val executor: Executor, + @UiBackground private val uiBgExecutor: Executor, private val activityTaskManagerService: IActivityTaskManager, private val keyguardStateController: KeyguardStateController, private val keyguardSurfaceBehindAnimator: KeyguardSurfaceBehindParamsApplier, @@ -144,11 +146,15 @@ constructor( isKeyguardGoingAway = true return } - // Make the surface behind the keyguard visible by calling keyguardGoingAway. The - // lockscreen is still showing as well, allowing us to animate unlocked. - Log.d(TAG, "ActivityTaskManagerService#keyguardGoingAway()") - activityTaskManagerService.keyguardGoingAway(0) + isKeyguardGoingAway = true + Log.d(TAG, "Enqueuing ATMS#keyguardGoingAway() on uiBgExecutor") + uiBgExecutor.execute { + // Make the surface behind the keyguard visible by calling keyguardGoingAway. The + // lockscreen is still showing as well, allowing us to animate unlocked. + Log.d(TAG, "ATMS#keyguardGoingAway()") + activityTaskManagerService.keyguardGoingAway(0) + } } else if (isLockscreenShowing == true) { // Re-show the lockscreen if the surface was visible and we want to make it invisible, // and the lockscreen is currently showing (this is the usual case of the going away @@ -273,32 +279,44 @@ constructor( return } - if (this.isLockscreenShowing == lockscreenShowing && this.isAodVisible == aodVisible) { + if ( + this.isLockscreenShowing == lockscreenShowing && + this.isAodVisible == aodVisible && + !this.isKeyguardGoingAway + ) { Log.d( TAG, "#setWmLockscreenState: lockscreenShowing=$lockscreenShowing and " + - "isAodVisible=$aodVisible were both unchanged, not forwarding to ATMS.", + "isAodVisible=$aodVisible were both unchanged and we're not going away, not " + + "forwarding to ATMS.", ) return } + this.isLockscreenShowing = lockscreenShowing + this.isAodVisible = aodVisible Log.d( TAG, - "ATMS#setLockScreenShown(" + - "isLockscreenShowing=$lockscreenShowing, " + - "aodVisible=$aodVisible).", + "Enqueuing ATMS#setLockScreenShown($lockscreenShowing, $aodVisible) " + + "on uiBgExecutor", ) - if (enableNewKeyguardShellTransitions) { - startKeyguardTransition(lockscreenShowing, aodVisible) - } else { - try { - activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible) - } catch (e: RemoteException) { - Log.e(TAG, "Remote exception", e) + uiBgExecutor.execute { + Log.d( + TAG, + "ATMS#setLockScreenShown(" + + "isLockscreenShowing=$lockscreenShowing, " + + "aodVisible=$aodVisible).", + ) + if (enableNewKeyguardShellTransitions) { + startKeyguardTransition(lockscreenShowing, aodVisible) + } else { + try { + activityTaskManagerService.setLockScreenShown(lockscreenShowing, aodVisible) + } catch (e: RemoteException) { + Log.e(TAG, "Remote exception", e) + } } } - this.isLockscreenShowing = lockscreenShowing - this.isAodVisible = aodVisible } private fun startKeyguardTransition(keyguardShowing: Boolean, aodShowing: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index ef1fe49372b2..6249b8006083 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -33,9 +33,11 @@ import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAwakeInState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.shared.model.Scenes @@ -137,19 +139,20 @@ constructor( repository.biometricUnlockState, repository.canIgnoreAuthAndReturnToGone, transitionInteractor.currentKeyguardState, - ) { - keyguardEnabled, - shouldSuppressKeyguard, - biometricUnlockState, - canIgnoreAuthAndReturnToGone, - currentState -> + transitionInteractor.startedKeyguardTransitionStep, + ) { values -> + val keyguardEnabled = values[0] as Boolean + val shouldSuppressKeyguard = values[1] as Boolean + val biometricUnlockState = values[2] as BiometricUnlockModel + val canIgnoreAuthAndReturnToGone = values[3] as Boolean + val currentState = values[4] as KeyguardState + val startedStep = values[5] as TransitionStep (!keyguardEnabled || shouldSuppressKeyguard) || BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) || canIgnoreAuthAndReturnToGone || (currentState == KeyguardState.DREAMING && keyguardInteractor.isKeyguardDismissible.value) || - (currentState == KeyguardState.GONE && - transitionInteractor.getStartedState() == KeyguardState.GONE) + (currentState == KeyguardState.GONE && startedStep.to == KeyguardState.GONE) } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 9968bc95a5ba..751674afa745 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.MathUtils +import com.android.systemui.Flags import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.dagger.SysUISingleton @@ -109,15 +110,14 @@ constructor( ) } - private fun createBouncerWindowBlurFlow( - willRunAnimationOnKeyguard: () -> Boolean - ): Flow<Float> { + private fun createBouncerWindowBlurFlow(): Flow<Float> { return transitionAnimation.sharedFlow( duration = TO_GONE_SHORT_DURATION, - onStart = { willRunDismissFromKeyguard = willRunAnimationOnKeyguard() }, + onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() }, onStep = { - if (willRunDismissFromKeyguard) { - blurConfig.minBlurRadiusPx + if (leaveShadeOpen && Flags.notificationShadeBlur()) { + // Going back to shade from bouncer after keyguard dismissal + blurConfig.maxBlurRadiusPx } else { transitionProgressToBlurRadius( starBlurRadius = blurConfig.maxBlurRadiusPx, @@ -158,15 +158,7 @@ constructor( ) } - override val windowBlurRadius: Flow<Float> = - if (ComposeBouncerFlags.isEnabled) { - keyguardDismissActionInteractor - .get() - .willAnimateDismissActionOnLockscreen - .flatMapLatest { createBouncerWindowBlurFlow { it } } - } else { - createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard) - } + override val windowBlurRadius: Flow<Float> = createBouncerWindowBlurFlow() override val notificationBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0.0f) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index c2efc7559487..ab4467e87a3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -875,11 +875,7 @@ class LegacyMediaDataManagerImpl( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index a7c5a36b804a..1a4687b59dbd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -339,11 +339,7 @@ constructor( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) return MediaDataLoaderResult( appName = appName, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index ca4a65953cba..7dfa69efc155 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -719,11 +719,7 @@ class MediaDataProcessor( desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - + val progress = MediaDataUtils.getDescriptionProgress(desc.extras) val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 172998e09266..8ad10ba2a240 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -46,9 +46,6 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass */ fun isRetainingPlayersEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RETAIN_SESSIONS) - /** Check whether to get progress information for resume players */ - fun isResumeProgressEnabled() = featureFlags.isEnabled(FlagsClassic.MEDIA_RESUME_PROGRESS) - /** Check whether we allow remote media to generate resume controls */ fun isRemoteResumeAllowed() = featureFlags.isEnabled(FlagsClassic.MEDIA_REMOTE_RESUME) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java index 7b1c62e2a0e5..78e66235112a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java @@ -37,16 +37,21 @@ public class MediaItem { @MediaItemType private final int mMediaItemType; private final boolean mIsFirstDeviceInGroup; + private final boolean mIsExpandableDivider; + private final boolean mHasTopSeparator; @Retention(RetentionPolicy.SOURCE) @IntDef({ MediaItemType.TYPE_DEVICE, MediaItemType.TYPE_GROUP_DIVIDER, - MediaItemType.TYPE_PAIR_NEW_DEVICE}) + MediaItemType.TYPE_PAIR_NEW_DEVICE, + MediaItemType.TYPE_DEVICE_GROUP + }) public @interface MediaItemType { int TYPE_DEVICE = 0; int TYPE_GROUP_DIVIDER = 1; int TYPE_PAIR_NEW_DEVICE = 2; + int TYPE_DEVICE_GROUP = 3; } /** @@ -70,6 +75,18 @@ public class MediaItem { } /** + * Returns a new {@link MediaItemType#TYPE_DEVICE_GROUP} {@link MediaItem}. This items controls + * the volume of the group session. + */ + public static MediaItem createDeviceGroupMediaItem() { + return new MediaItem( + /* device */ null, + /* title */ null, + /* type */ MediaItemType.TYPE_DEVICE_GROUP, + /* misFirstDeviceInGroup */ false); + } + + /** * Returns a new {@link MediaItemType#TYPE_PAIR_NEW_DEVICE} {@link MediaItem} with both {@link * #getMediaDevice() media device} and title set to {@code null}. */ @@ -93,15 +110,58 @@ public class MediaItem { /* misFirstDeviceInGroup */ false); } + /** + * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified + * title and a {@code null} {@link #getMediaDevice() media device}. This item needs to be + * rendered with a separator above it. + */ + public static MediaItem createGroupDividerWithSeparatorMediaItem(@Nullable String title) { + return new MediaItem( + /* device */ null, + title, + MediaItemType.TYPE_GROUP_DIVIDER, + /* isFirstDeviceInGroup */ false, + /* isExpandableDivider */ false, + /* hasTopSeparator */ true); + } + + /** + * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified + * title and a {@code null} {@link #getMediaDevice() media device}. The item serves as a toggle + * for expanding/collapsing the group of devices. + */ + public static MediaItem createExpandableGroupDividerMediaItem(@Nullable String title) { + return new MediaItem( + /* device */ null, + title, + MediaItemType.TYPE_GROUP_DIVIDER, + /* isFirstDeviceInGroup */ false, + /* isExpandableDivider */ true, + /* hasTopSeparator */ false); + } + private MediaItem( @Nullable MediaDevice device, @Nullable String title, @MediaItemType int type, boolean isFirstDeviceInGroup) { + this(device, title, type, isFirstDeviceInGroup, /* isExpandableDivider */ + false, /* hasTopSeparator */ false); + } + + private MediaItem( + @Nullable MediaDevice device, + @Nullable String title, + @MediaItemType int type, + boolean isFirstDeviceInGroup, + boolean isExpandableDivider, + boolean hasTopSeparator) { this.mMediaDeviceOptional = Optional.ofNullable(device); this.mTitle = title; this.mMediaItemType = type; this.mIsFirstDeviceInGroup = isFirstDeviceInGroup; + this.mIsExpandableDivider = isExpandableDivider; + this.mHasTopSeparator = hasTopSeparator; } public Optional<MediaDevice> getMediaDevice() { @@ -133,4 +193,14 @@ public class MediaItem { public boolean isFirstDeviceInGroup() { return mIsFirstDeviceInGroup; } + + /** Returns whether a group divider has a button that expands group device list */ + public boolean isExpandableDivider() { + return mIsExpandableDivider; + } + + /** Returns whether a group divider has a border at the top */ + public boolean hasTopSeparator() { + return mHasTopSeparator; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt new file mode 100644 index 000000000000..4c34250c9653 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt @@ -0,0 +1,688 @@ +/* + * 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.media.dialog + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import com.android.settingslib.media.InputMediaDevice +import com.android.settingslib.media.MediaDevice +import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED +import com.android.systemui.FontStyles.GSF_TITLE_SMALL +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP +import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTED +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTING +import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.DISCONNECTED +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.getOrNull +import com.google.android.material.slider.Slider + +/** A RecyclerView adapter for the legacy UI media output dialog device list. */ +class MediaOutputAdapter(controller: MediaSwitchingController) : + MediaOutputAdapterBase(controller) { + private val mGroupSelectedItems = mController.selectedMediaDevice.size > 1 + + /** Refreshes the RecyclerView dataset and forces re-render. */ + override fun updateItems() { + val newList = + mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList() + + addSeparatorForTheFirstGroupDivider(newList) + coalesceSelectedDevices(newList) + + mMediaItemList.clear() + mMediaItemList.addAll(newList) + + notifyDataSetChanged() + } + + private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) { + for ((i, item) in newList.withIndex()) { + if (item.mediaItemType == TYPE_GROUP_DIVIDER) { + newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title) + break + } + } + } + + /** + * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and + * displays a single session control instead of individual device controls. + */ + private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) { + val selectedDevices = newList.filter { this.isSelectedDevice(it) } + + if (mGroupSelectedItems && selectedDevices.size > 1) { + newList.removeAll(selectedDevices.toSet()) + if (mController.isGroupListCollapsed) { + newList.add(0, MediaItem.createDeviceGroupMediaItem()) + } else { + newList.addAll(0, selectedDevices) + } + newList.add(0, mController.connectedSpeakersExpandableGroupDivider) + } + } + + private fun isSelectedDevice(mediaItem: MediaItem): Boolean { + return mediaItem.mediaDevice.getOrNull()?.let { device -> + isDeviceIncluded(mController.selectedMediaDevice, device) + } ?: false + } + + override fun getItemId(position: Int): Long { + if (position >= mMediaItemList.size) { + Log.e(TAG, "Item position exceeds list size: $position") + return RecyclerView.NO_ID + } + val currentMediaItem = mMediaItemList[position] + return when (currentMediaItem.mediaItemType) { + TYPE_DEVICE -> + currentMediaItem.mediaDevice.getOrNull()?.id?.hashCode()?.toLong() + ?: RecyclerView.NO_ID + TYPE_GROUP_DIVIDER -> currentMediaItem.title.hashCode().toLong() + TYPE_DEVICE_GROUP -> currentMediaItem.hashCode().toLong() + else -> RecyclerView.NO_ID + } + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val context = viewGroup.context + return when (viewType) { + TYPE_GROUP_DIVIDER -> { + val holderView = + LayoutInflater.from(context) + .inflate(R.layout.media_output_list_item_group_divider, viewGroup, false) + MediaGroupDividerViewHolder(holderView, context) + } + + TYPE_DEVICE, + TYPE_DEVICE_GROUP -> { + val holderView = + LayoutInflater.from(context) + .inflate(R.layout.media_output_list_item_device, viewGroup, false) + MediaDeviceViewHolder(holderView, context) + } + + else -> throw IllegalArgumentException("Invalid view type: $viewType") + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + require(position < itemCount) { "Invalid position: $position, list size: $itemCount" } + val currentMediaItem = mMediaItemList[position] + when (currentMediaItem.mediaItemType) { + TYPE_GROUP_DIVIDER -> + (viewHolder as MediaGroupDividerViewHolder).onBind( + groupDividerTitle = currentMediaItem.title, + isExpandableDivider = currentMediaItem.isExpandableDivider, + hasTopSeparator = currentMediaItem.hasTopSeparator(), + ) + + TYPE_DEVICE -> + (viewHolder as MediaDeviceViewHolder).onBindDevice( + mediaItem = currentMediaItem, + position = position, + ) + + TYPE_DEVICE_GROUP -> (viewHolder as MediaDeviceViewHolder).onBindDeviceGroup() + else -> + throw IllegalArgumentException( + "Invalid item type ${currentMediaItem.mediaItemType} for position: $position" + ) + } + } + + val controller: MediaSwitchingController + get() = mController + + /** ViewHolder for binding device view. */ + inner class MediaDeviceViewHolder(view: View, context: Context?) : + MediaDeviceViewHolderBase(view, context) { + @VisibleForTesting val mMainContent: LinearLayout = view.requireViewById(R.id.main_content) + + @VisibleForTesting val mItemLayout: LinearLayout = view.requireViewById(R.id.item_layout) + + @VisibleForTesting val mTitleText: TextView = view.requireViewById(R.id.title) + + @VisibleForTesting val mSubTitleText: TextView = view.requireViewById(R.id.subtitle) + + @VisibleForTesting val mTitleIcon: ImageView = view.requireViewById(R.id.title_icon) + + @VisibleForTesting + val mLoadingIndicator: ProgressBar = view.requireViewById(R.id.loading_indicator) + + @VisibleForTesting val mStatusIcon: ImageView = view.requireViewById(R.id.status_icon) + + @VisibleForTesting val mGroupButton: ImageButton = view.requireViewById(R.id.group_button) + + @VisibleForTesting val mDivider: View = view.requireViewById(R.id.divider) + + @VisibleForTesting + val mOngoingSessionButton: ImageButton = view.requireViewById(R.id.ongoing_session_button) + + @VisibleForTesting var mSlider: Slider = view.requireViewById(R.id.volume_seekbar) + private var mLatestUpdateVolume = NO_VOLUME_SET + + private val mInactivePadding = + mContext.resources.getDimension(R.dimen.media_output_item_content_vertical_margin) + private val mActivePadding = + mContext.resources.getDimension( + R.dimen.media_output_item_content_vertical_margin_active + ) + private val mSubtitleAlpha = + mContext.resources.getFloat(R.dimen.media_output_item_subtitle_alpha) + + fun onBindDevice(mediaItem: MediaItem, position: Int) { + resetViewState() + renderItem(mediaItem, position) + } + + fun onBindDeviceGroup() { + resetViewState() + renderDeviceGroupItem() + } + + private fun resetViewState() { + mItemLayout.visibility = VISIBLE + mGroupButton.visibility = GONE + mOngoingSessionButton.visibility = GONE + mStatusIcon.visibility = GONE + mLoadingIndicator.visibility = GONE + mDivider.visibility = GONE + mSubTitleText.visibility = GONE + mMainContent.setOnClickListener(null) + } + + override fun renderDeviceItem( + hideGroupItem: Boolean, + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + groupStatus: GroupStatus?, + ongoingSessionStatus: OngoingSessionStatus?, + clickListener: View.OnClickListener?, + deviceDisabled: Boolean, + subtitle: String?, + deviceStatusIcon: Drawable?, + ) { + val fixedVolumeConnected = connectionState == CONNECTED && restrictVolumeAdjustment + val colorTheme = ColorTheme(fixedVolumeConnected, deviceDisabled) + + updateTitle(device.name, connectionState, colorTheme) + updateTitleIcon(device, connectionState, restrictVolumeAdjustment, colorTheme) + updateSubtitle(subtitle, colorTheme) + updateSeekBar(device, connectionState, restrictVolumeAdjustment, colorTheme) + updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus, colorTheme) + updateLoadingIndicator(connectionState, colorTheme) + updateDeviceStatusIcon(deviceStatusIcon, colorTheme) + updateContentBackground(fixedVolumeConnected, colorTheme) + updateContentClickListener(clickListener) + } + + override fun renderDeviceGroupItem() { + mTitleIcon.visibility = GONE + val colorTheme = ColorTheme() + updateTitle( + title = mController.sessionName ?: "", + connectionState = CONNECTED, + colorTheme = colorTheme, + ) + updateGroupSeekBar(colorTheme) + } + + private fun updateTitle( + title: CharSequence, + connectionState: ConnectionState, + colorTheme: ColorTheme, + ) { + mTitleText.text = title + val fontFamilyName: String = + if (connectionState == CONNECTED) GSF_TITLE_MEDIUM_EMPHASIZED else GSF_TITLE_SMALL + mTitleText.typeface = Typeface.create(fontFamilyName, Typeface.NORMAL) + mTitleText.setTextColor(colorTheme.titleColor) + mTitleText.alpha = colorTheme.contentAlpha + } + + private fun updateContentBackground(fixedVolumeConnected: Boolean, colorTheme: ColorTheme) { + if (fixedVolumeConnected) { + mMainContent.backgroundTintList = + ColorStateList.valueOf(colorTheme.containerRestrictedVolumeBackground) + mMainContent.background = + AppCompatResources.getDrawable( + mContext, + R.drawable.media_output_dialog_item_fixed_volume_background, + ) + } else { + mMainContent.background = null + mMainContent.setBackgroundColor(Color.TRANSPARENT) + } + } + + private fun updateContentPadding(verticalPadding: Float) { + mMainContent.setPadding(0, verticalPadding.toInt(), 0, verticalPadding.toInt()) + } + + private fun updateLayoutForSlider(showSlider: Boolean) { + updateContentPadding(if (showSlider) mActivePadding else mInactivePadding) + mSlider.visibility = if (showSlider) VISIBLE else GONE + mSlider.alpha = if (showSlider) 1f else 0f + } + + private fun updateSeekBar( + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + colorTheme: ColorTheme, + ) { + val showSlider = connectionState == CONNECTED && !restrictVolumeAdjustment + if (showSlider) { + updateLayoutForSlider(showSlider = true) + initSeekbar( + volumeChangeCallback = { volume: Int -> + mController.adjustVolume(device, volume) + }, + settleCallback = { mController.logInteractionAdjustVolume(device) }, + deviceDrawable = mController.getDeviceIconDrawable(device), + isInputDevice = device is InputMediaDevice, + isVolumeControlAllowed = mController.isVolumeControlEnabled(device), + currentVolume = device.currentVolume, + maxVolume = device.maxVolume, + colorTheme = colorTheme, + ) + } else { + updateLayoutForSlider(showSlider = false) + } + } + + private fun updateGroupSeekBar(colorTheme: ColorTheme) { + mSlider.visibility = VISIBLE + updateContentPadding(mActivePadding) + val groupDrawable = + AppCompatResources.getDrawable( + mContext, + com.android.settingslib.R.drawable.ic_media_group_device, + ) + initSeekbar( + volumeChangeCallback = { volume: Int -> mController.adjustSessionVolume(volume) }, + deviceDrawable = groupDrawable, + isVolumeControlAllowed = mController.isVolumeControlEnabledForSession, + currentVolume = mController.sessionVolume, + maxVolume = mController.sessionVolumeMax, + colorTheme = colorTheme, + ) + } + + private fun updateSubtitle(subtitle: String?, colorTheme: ColorTheme) { + if (subtitle.isNullOrEmpty()) { + mSubTitleText.visibility = GONE + } else { + mSubTitleText.text = subtitle + mSubTitleText.setTextColor(colorTheme.subtitleColor) + mSubTitleText.alpha = mSubtitleAlpha * colorTheme.contentAlpha + mSubTitleText.visibility = VISIBLE + } + } + + private fun updateLoadingIndicator( + connectionState: ConnectionState, + colorTheme: ColorTheme, + ) { + if (connectionState == CONNECTING) { + mLoadingIndicator.visibility = VISIBLE + mLoadingIndicator.indeterminateDrawable.setTintList( + ColorStateList.valueOf(colorTheme.statusIconColor) + ) + } else { + mLoadingIndicator.visibility = GONE + } + } + + private fun initializeSeekbarVolume(currentVolume: Int) { + tryResolveVolumeUserRequest(currentVolume) + if (!isDragging && hasNoPendingVolumeRequests()) { + mSlider.value = currentVolume.toFloat() + } + } + + private fun tryResolveVolumeUserRequest(currentVolume: Int) { + if (currentVolume == mLatestUpdateVolume) { + mLatestUpdateVolume = NO_VOLUME_SET + } + } + + private fun hasNoPendingVolumeRequests(): Boolean { + return mLatestUpdateVolume == NO_VOLUME_SET + } + + private fun setLatestVolumeRequest(volume: Int) { + mLatestUpdateVolume = volume + } + + private fun initSeekbar( + volumeChangeCallback: (Int) -> Unit, + settleCallback: () -> Unit = {}, + deviceDrawable: Drawable?, + isInputDevice: Boolean = false, + isVolumeControlAllowed: Boolean, + currentVolume: Int, + maxVolume: Int, + colorTheme: ColorTheme, + ) { + if (maxVolume == 0) { + Log.e(TAG, "Invalid maxVolume value") + // Slider doesn't allow valueFrom == valueTo, return to prevent crash. + return + } + + mSlider.isEnabled = isVolumeControlAllowed + mSlider.valueFrom = 0f + mSlider.valueTo = maxVolume.toFloat() + mSlider.stepSize = 1f + mSlider.thumbTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor) + mSlider.trackActiveTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor) + mSlider.trackInactiveTintList = ColorStateList.valueOf(colorTheme.sliderInactiveColor) + mSlider.trackIconActiveColor = ColorStateList.valueOf(colorTheme.sliderActiveIconColor) + mSlider.trackIconInactiveColor = + ColorStateList.valueOf(colorTheme.sliderInactiveIconColor) + val muteDrawable = getMuteDrawable(isInputDevice) + updateSliderIconsVisibility( + deviceDrawable = deviceDrawable, + muteDrawable = muteDrawable, + isMuted = currentVolume == 0, + ) + initializeSeekbarVolume(currentVolume) + + mSlider.clearOnChangeListeners() // Prevent adding multiple listeners + mSlider.addOnChangeListener { _: Slider, value: Float, fromUser: Boolean -> + if (fromUser) { + val seekBarVolume = value.toInt() + updateSliderIconsVisibility( + deviceDrawable = deviceDrawable, + muteDrawable = muteDrawable, + isMuted = seekBarVolume == 0, + ) + if (seekBarVolume != currentVolume) { + setLatestVolumeRequest(seekBarVolume) + volumeChangeCallback(seekBarVolume) + } + } + } + + mSlider.clearOnSliderTouchListeners() // Prevent adding multiple listeners + mSlider.addOnSliderTouchListener( + object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + setIsDragging(true) + } + + override fun onStopTrackingTouch(slider: Slider) { + setIsDragging(false) + settleCallback() + } + } + ) + } + + private fun getMuteDrawable(isInputDevice: Boolean): Drawable? { + return AppCompatResources.getDrawable( + mContext, + if (isInputDevice) R.drawable.ic_mic_off + else R.drawable.media_output_icon_volume_off, + ) + } + + private fun updateSliderIconsVisibility( + deviceDrawable: Drawable?, + muteDrawable: Drawable?, + isMuted: Boolean, + ) { + mSlider.trackIconInactiveStart = if (isMuted) muteDrawable else null + // A workaround for the slider glitch that sometimes shows the active icon in inactive + // state. + mSlider.trackIconActiveStart = if (isMuted) null else deviceDrawable + } + + private fun updateTitleIcon( + device: MediaDevice, + connectionState: ConnectionState, + restrictVolumeAdjustment: Boolean, + colorTheme: ColorTheme, + ) { + if (connectionState == CONNECTED && !restrictVolumeAdjustment) { + mTitleIcon.visibility = GONE + } else { + mTitleIcon.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + val drawable = mController.getDeviceIconDrawable(device) + mTitleIcon.setImageDrawable(drawable) + mTitleIcon.visibility = VISIBLE + mTitleIcon.alpha = colorTheme.contentAlpha + } + } + + private fun updateDeviceStatusIcon(deviceStatusIcon: Drawable?, colorTheme: ColorTheme) { + if (deviceStatusIcon == null) { + mStatusIcon.visibility = GONE + } else { + mStatusIcon.setImageDrawable(deviceStatusIcon) + mStatusIcon.alpha = colorTheme.contentAlpha + mStatusIcon.imageTintList = ColorStateList.valueOf(colorTheme.statusIconColor) + mStatusIcon.visibility = VISIBLE + } + } + + private fun updateEndArea( + device: MediaDevice, + connectionState: ConnectionState, + groupStatus: GroupStatus?, + ongoingSessionStatus: OngoingSessionStatus?, + colorTheme: ColorTheme, + ) { + var showDivider = false + + if (ongoingSessionStatus != null) { + showDivider = true + mOngoingSessionButton.visibility = VISIBLE + updateOngoingSessionButton(device, ongoingSessionStatus.host, colorTheme) + } + + if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { + showDivider = true + mGroupButton.visibility = VISIBLE + updateGroupButton(device, groupStatus, colorTheme) + } + + mDivider.visibility = + if (showDivider && connectionState == DISCONNECTED) VISIBLE else GONE + mDivider.setBackgroundColor(mController.colorScheme.getOutline()) + } + + private fun shouldShowGroupCheckbox(groupStatus: GroupStatus): Boolean { + val disabled = groupStatus.selected && !groupStatus.deselectable + return !disabled + } + + private fun updateOngoingSessionButton( + device: MediaDevice, + isHost: Boolean, + colorTheme: ColorTheme, + ) { + val iconDrawableId = + if (isHost) R.drawable.media_output_status_edit_session + else R.drawable.ic_sound_bars_anim + mOngoingSessionButton.setOnClickListener { v: View? -> + mController.tryToLaunchInAppRoutingIntent(device.id, v) + } + val drawable = AppCompatResources.getDrawable(mContext, iconDrawableId) + mOngoingSessionButton.setImageDrawable(drawable) + mOngoingSessionButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + if (drawable is AnimatedVectorDrawable) { + drawable.start() + } + } + + private fun updateGroupButton( + device: MediaDevice, + groupStatus: GroupStatus, + colorTheme: ColorTheme, + ) { + mGroupButton.contentDescription = + mContext.getString( + if (groupStatus.selected) R.string.accessibility_remove_device_from_group + else R.string.accessibility_add_device_to_group + ) + mGroupButton.setImageResource( + if (groupStatus.selected) R.drawable.ic_check_circle_filled + else R.drawable.ic_add_circle_rounded + ) + mGroupButton.setOnClickListener { + onGroupActionTriggered(!groupStatus.selected, device) + } + mGroupButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor) + } + + private fun updateContentClickListener(listener: View.OnClickListener?) { + mMainContent.setOnClickListener(listener) + if (listener == null) { + mMainContent.isClickable = false // clickable is not removed automatically. + } + } + + override fun disableSeekBar() { + mSlider.isEnabled = false + } + } + + inner class MediaGroupDividerViewHolder(itemView: View, val mContext: Context) : + RecyclerView.ViewHolder(itemView) { + private val mTopSeparator: View = itemView.requireViewById(R.id.top_separator) + private val mTitleText: TextView = itemView.requireViewById(R.id.title) + @VisibleForTesting + val mExpandButton: ViewGroup = itemView.requireViewById(R.id.expand_button) + private val mExpandButtonIcon: ImageView = itemView.requireViewById(R.id.expand_button_icon) + + fun onBind( + groupDividerTitle: String?, + isExpandableDivider: Boolean, + hasTopSeparator: Boolean, + ) { + mTitleText.text = groupDividerTitle + mTitleText.setTextColor(mController.colorScheme.getPrimary()) + if (hasTopSeparator) { + mTopSeparator.visibility = VISIBLE + mTopSeparator.setBackgroundColor(mController.colorScheme.getOutlineVariant()) + } else { + mTopSeparator.visibility = GONE + } + updateExpandButton(isExpandableDivider) + } + + private fun updateExpandButton(isExpandableDivider: Boolean) { + if (!isExpandableDivider) { + mExpandButton.visibility = GONE + return + } + val isCollapsed = mController.isGroupListCollapsed + mExpandButtonIcon.setImageDrawable( + AppCompatResources.getDrawable( + mContext, + if (isCollapsed) R.drawable.ic_expand_more_rounded + else R.drawable.ic_expand_less_rounded, + ) + ) + mExpandButtonIcon.contentDescription = + mContext.getString( + if (isCollapsed) R.string.accessibility_expand_group + else R.string.accessibility_collapse_group + ) + mExpandButton.visibility = VISIBLE + mExpandButton.setOnClickListener { toggleGroupList() } + mExpandButtonIcon.backgroundTintList = + ColorStateList.valueOf(mController.colorScheme.getOnSurface()) + .withAlpha((255 * 0.1).toInt()) + mExpandButtonIcon.imageTintList = + ColorStateList.valueOf(mController.colorScheme.getOnSurface()) + } + + private fun toggleGroupList() { + mController.isGroupListCollapsed = !mController.isGroupListCollapsed + updateItems() + } + } + + private inner class ColorTheme( + isConnectedWithFixedVolume: Boolean = false, + deviceDisabled: Boolean = false, + ) { + private val colorScheme: MediaOutputColorScheme = mController.colorScheme + + val titleColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurface() + } + val subtitleColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurfaceVariant() + } + val iconColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurface() + } + val statusIconColor = + if (isConnectedWithFixedVolume) { + colorScheme.getOnPrimary() + } else { + colorScheme.getOnSurfaceVariant() + } + val sliderActiveColor = colorScheme.getPrimary() + val sliderActiveIconColor = colorScheme.getOnPrimary() + val sliderInactiveColor = colorScheme.getSecondaryContainer() + val sliderInactiveIconColor = colorScheme.getOnSurface() + val containerRestrictedVolumeBackground = colorScheme.getPrimary() + val contentAlpha = if (deviceDisabled) DEVICE_DISABLED_ALPHA else DEVICE_ACTIVE_ALPHA + } + + companion object { + private const val TAG = "MediaOutputAdapter" + private const val DEVICE_DISABLED_ALPHA = 0.5f + private const val DEVICE_ACTIVE_ALPHA = 1f + private const val NO_VOLUME_SET = -1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index e3990d25f94e..d46cca2736da 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import android.content.Context; import android.graphics.drawable.Drawable; @@ -46,11 +47,11 @@ import java.util.concurrent.CopyOnWriteArrayList; * manipulate the layout directly. */ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> { - record OngoingSessionStatus(boolean host) {} + public record OngoingSessionStatus(boolean host) {} - record GroupStatus(Boolean selected, Boolean deselectable) {} + public record GroupStatus(Boolean selected, Boolean deselectable) {} - enum ConnectionState { + public enum ConnectionState { CONNECTED, CONNECTING, DISCONNECTED, @@ -138,7 +139,7 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl return mMediaItemList.size(); } - abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { + public abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { Context mContext; @@ -211,7 +212,8 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl clickListener = v -> cancelMuteAwaitConnection(); } else if (device.getState() == MediaDeviceState.STATE_GROUPING) { connectionState = ConnectionState.CONNECTING; - } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices() + } else if (!enableOutputSwitcherRedesign() && mShouldGroupSelectedMediaItems + && hasMultipleSelectedDevices() && isSelected) { if (mediaItem.isFirstDeviceInGroup()) { isDeviceGroup = true; diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 49d09cf64c8e..7f9370ca671d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -19,16 +19,23 @@ package com.android.systemui.media.dialog; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.statusBars; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; +import static com.android.systemui.FontStyles.GSF_LABEL_LARGE; +import static com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED; +import static com.android.systemui.FontStyles.GSF_TITLE_SMALL; + import android.annotation.NonNull; import android.app.WallpaperColors; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -49,6 +56,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.VisibleForTesting; +import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.IconCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -57,6 +65,8 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.google.android.material.button.MaterialButton; + import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -71,7 +81,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); - private final RecyclerView.LayoutManager mLayoutManager; + private final LinearLayoutManager mLayoutManager; final Context mContext; final MediaSwitchingController mMediaSwitchingController; @@ -93,8 +103,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private ImageView mBroadcastIcon; private RecyclerView mDevicesRecyclerView; private ViewGroup mDeviceListLayout; + private ViewGroup mQuickAccessShelf; + private MaterialButton mConnectDeviceButton; private LinearLayout mMediaMetadataSectionLayout; private Button mDoneButton; + private ViewGroup mDialogFooter; + private View mFooterSpacer; private Button mStopButton; private WallpaperColors mWallpaperColors; private boolean mShouldLaunchLeBroadcastDialog; @@ -229,7 +243,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mHeaderTitle = mDialogView.requireViewById(R.id.header_title); mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); mHeaderIcon = mDialogView.requireViewById(R.id.header_icon); + mQuickAccessShelf = mDialogView.requireViewById(R.id.quick_access_shelf); + mConnectDeviceButton = mDialogView.requireViewById(R.id.connect_device); mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result); + mDialogFooter = mDialogView.requireViewById(R.id.dialog_footer); + mFooterSpacer = mDialogView.requireViewById(R.id.footer_spacer); mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section); mDeviceListLayout = mDialogView.requireViewById(R.id.device_list); mDoneButton = mDialogView.requireViewById(R.id.done); @@ -252,6 +270,49 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } mDismissing = false; + + if (enableOutputSwitcherRedesign()) { + // Reduce radius of dialog background. + mDialogView.setBackground(AppCompatResources.getDrawable(mContext, + R.drawable.media_output_dialog_background_reduced_radius)); + // Set non-transparent footer background to change it color on scroll. + mDialogFooter.setBackground(AppCompatResources.getDrawable(mContext, + R.drawable.media_output_dialog_footer_background)); + // Right-align the footer buttons. + LinearLayout.LayoutParams layoutParams = + (LinearLayout.LayoutParams) mFooterSpacer.getLayoutParams(); + layoutParams.width = (int) mContext.getResources().getDimension( + R.dimen.media_output_dialog_button_gap); + mFooterSpacer.setLayoutParams(layoutParams); + layoutParams.weight = 0; + // Update font family to Google Sans Flex. + Typeface buttonTypeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL); + mDoneButton.setTypeface(buttonTypeface); + mStopButton.setTypeface(buttonTypeface); + mHeaderTitle + .setTypeface(Typeface.create(GSF_TITLE_MEDIUM_EMPHASIZED, Typeface.NORMAL)); + mHeaderSubtitle + .setTypeface(Typeface.create(GSF_TITLE_SMALL, Typeface.NORMAL)); + // Reduce the size of the app icon. + float appIconSize = mContext.getResources().getDimension( + R.dimen.media_output_dialog_app_icon_size); + float appIconBottomMargin = mContext.getResources().getDimension( + R.dimen.media_output_dialog_app_icon_bottom_margin); + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mAppResourceIcon.getLayoutParams(); + params.bottomMargin = (int) appIconBottomMargin; + params.width = (int) appIconSize; + params.height = (int) appIconSize; + mAppResourceIcon.setLayoutParams(params); + // Change footer background color on scroll. + mDevicesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + changeFooterColorForScroll(); + } + }); + } } @Override @@ -366,6 +427,18 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } } + if (enableOutputSwitcherRedesign()) { + if (mMediaSwitchingController.getConnectNewDeviceItem() != null) { + mQuickAccessShelf.setVisibility(View.VISIBLE); + mConnectDeviceButton.setVisibility(View.VISIBLE); + mConnectDeviceButton.setOnClickListener( + mMediaSwitchingController::launchBluetoothPairing); + } else { + mQuickAccessShelf.setVisibility(View.GONE); + mConnectDeviceButton.setVisibility(View.GONE); + } + } + // Show when remote media session is available or // when the device supports BT LE audio + media is playing mStopButton.setVisibility(getStopButtonVisibility()); @@ -390,21 +463,48 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog } private void updateButtonBackgroundColorFilter() { - ColorFilter buttonColorFilter = - new PorterDuffColorFilter( - mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(), - PorterDuff.Mode.SRC_IN); - mDoneButton.getBackground().setColorFilter(buttonColorFilter); - mStopButton.getBackground().setColorFilter(buttonColorFilter); - mDoneButton.setTextColor( - mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText()); + if (enableOutputSwitcherRedesign()) { + mDoneButton.getBackground().setTint( + mMediaSwitchingController.getColorScheme().getPrimary()); + mDoneButton.setTextColor(mMediaSwitchingController.getColorScheme().getOnPrimary()); + mStopButton.getBackground().setTint( + mMediaSwitchingController.getColorScheme().getOutlineVariant()); + mStopButton.setTextColor(mMediaSwitchingController.getColorScheme().getPrimary()); + mConnectDeviceButton.setTextColor( + mMediaSwitchingController.getColorScheme().getOnSurfaceVariant()); + mConnectDeviceButton.setStrokeColor(ColorStateList.valueOf( + mMediaSwitchingController.getColorScheme().getOutlineVariant())); + mConnectDeviceButton.setIconTint(ColorStateList.valueOf( + mMediaSwitchingController.getColorScheme().getPrimary())); + } else { + ColorFilter buttonColorFilter = new PorterDuffColorFilter( + mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(), + PorterDuff.Mode.SRC_IN); + mDoneButton.getBackground().setColorFilter(buttonColorFilter); + mStopButton.getBackground().setColorFilter(buttonColorFilter); + mDoneButton.setTextColor( + mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText()); + } } private void updateDialogBackgroundColor() { - getDialogView().getBackground().setTint( - mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); - mDeviceListLayout.setBackgroundColor( - mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); + int backgroundColor = enableOutputSwitcherRedesign() + ? mMediaSwitchingController.getColorScheme().getSurfaceContainer() + : mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground(); + getDialogView().getBackground().setTint(backgroundColor); + mDeviceListLayout.setBackgroundColor(backgroundColor); + } + + private void changeFooterColorForScroll() { + int totalItemCount = mLayoutManager.getItemCount(); + int lastVisibleItemPosition = + mLayoutManager.findLastCompletelyVisibleItemPosition(); + boolean hasBottomScroll = + totalItemCount > 0 && lastVisibleItemPosition != totalItemCount - 1; + mDialogFooter.getBackground().setTint( + hasBottomScroll + ? mMediaSwitchingController.getColorScheme().getSurfaceContainerHigh() + : mMediaSwitchingController.getColorScheme().getSurfaceContainer()); } public void handleLeBroadcastStarted() { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt new file mode 100644 index 000000000000..21b92cc11406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog + +import android.content.Context +import com.android.systemui.monet.ColorScheme +import com.android.systemui.res.R + +abstract class MediaOutputColorScheme { + companion object Factory { + @JvmStatic + fun fromDynamicColors(dynamicScheme: ColorScheme): MediaOutputColorScheme { + return MediaOutputColorSchemeDynamic(dynamicScheme) + } + + @JvmStatic + fun fromSystemColors(context: Context): MediaOutputColorScheme { + return MediaOutputColorSchemeSystem(context) + } + } + + abstract fun getPrimary(): Int + + abstract fun getOnPrimary(): Int + + abstract fun getSecondary(): Int + + abstract fun getSecondaryContainer(): Int + + abstract fun getSurfaceContainer(): Int + + abstract fun getSurfaceContainerHigh(): Int + + abstract fun getOnSurface(): Int + + abstract fun getOnSurfaceVariant(): Int + + abstract fun getOutline(): Int + + abstract fun getOutlineVariant(): Int +} + +class MediaOutputColorSchemeDynamic(dynamicScheme: ColorScheme) : MediaOutputColorScheme() { + private val mMaterialScheme = dynamicScheme.materialScheme + + override fun getPrimary() = mMaterialScheme.primary + + override fun getOnPrimary() = mMaterialScheme.onPrimary + + override fun getSecondary() = mMaterialScheme.secondary + + override fun getSecondaryContainer() = mMaterialScheme.secondaryContainer + + override fun getSurfaceContainer() = mMaterialScheme.surfaceContainer + + override fun getSurfaceContainerHigh() = mMaterialScheme.surfaceContainerHigh + + override fun getOnSurface() = mMaterialScheme.onSurface + + override fun getOnSurfaceVariant() = mMaterialScheme.onSurfaceVariant + + override fun getOutline() = mMaterialScheme.outline + + override fun getOutlineVariant() = mMaterialScheme.outlineVariant +} + +class MediaOutputColorSchemeSystem(private val mContext: Context) : MediaOutputColorScheme() { + override fun getPrimary() = mContext.getColor(R.color.media_dialog_primary) + + override fun getOnPrimary() = mContext.getColor(R.color.media_dialog_on_primary) + + override fun getSecondary() = mContext.getColor(R.color.media_dialog_secondary) + + override fun getSecondaryContainer() = + mContext.getColor(R.color.media_dialog_secondary_container) + + override fun getSurfaceContainer() = mContext.getColor(R.color.media_dialog_surface_container) + + override fun getSurfaceContainerHigh() = + mContext.getColor(R.color.media_dialog_surface_container_high) + + override fun getOnSurface() = mContext.getColor(R.color.media_dialog_on_surface) + + override fun getOnSurfaceVariant() = mContext.getColor(R.color.media_dialog_on_surface_variant) + + override fun getOutline() = mContext.getColor(R.color.media_dialog_outline) + + override fun getOutlineVariant() = mContext.getColor(R.color.media_dialog_outline_variant) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index 163ff248b9df..225ad724ce71 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -17,6 +17,7 @@ package com.android.systemui.media.dialog; import static com.android.settingslib.flags.Flags.legacyLeAudioSharing; +import static com.android.media.flags.Flags.enableOutputSwitcherRedesign; import android.content.Context; import android.os.Bundle; @@ -57,8 +58,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, - backgroundExecutor); + mAdapter = enableOutputSwitcherRedesign() + ? new MediaOutputAdapter(mMediaSwitchingController) + : new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, + backgroundExecutor); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 4f86257e3870..0b4a9321618d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -171,7 +171,9 @@ public class MediaSwitchingController private FeatureFlags mFeatureFlags; private UserTracker mUserTracker; private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; + @NonNull private MediaOutputColorScheme mMediaOutputColorScheme; @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy; + private boolean mIsGroupListCollapsed = true; public enum BroadcastNotifyDialog { ACTION_FIRST_LAUNCH, @@ -229,6 +231,7 @@ public class MediaSwitchingController mOutputMediaItemListProxy = new OutputMediaItemListProxy(context); mDialogTransitionAnimator = dialogTransitionAnimator; mNearbyMediaDevicesManager = nearbyMediaDevicesManager; + mMediaOutputColorScheme = MediaOutputColorScheme.fromSystemColors(mContext); mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); if (enableInputRouting()) { @@ -499,7 +502,7 @@ public class MediaSwitchingController return getNotificationIcon(); } - IconCompat getDeviceIconCompat(MediaDevice device) { + Drawable getDeviceIconDrawable(MediaDevice device) { Drawable drawable = device.getIcon(); if (drawable == null) { if (DEBUG) { @@ -509,7 +512,19 @@ public class MediaSwitchingController // Use default Bluetooth device icon to handle getIcon() is null case. drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); } - return BluetoothUtils.createIconWithDrawable(drawable); + return drawable; + } + + IconCompat getDeviceIconCompat(MediaDevice device) { + return BluetoothUtils.createIconWithDrawable(getDeviceIconDrawable(device)); + } + + public void setGroupListCollapsed(boolean isCollapsed) { + mIsGroupListCollapsed = isCollapsed; + } + + public boolean isGroupListCollapsed() { + return mIsGroupListCollapsed; } boolean isActiveItem(MediaDevice device) { @@ -560,10 +575,16 @@ public class MediaSwitchingController void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { ColorScheme currentColorScheme = new ColorScheme(wallpaperColors, isDarkTheme); + mMediaOutputColorScheme = MediaOutputColorScheme.fromDynamicColors( + currentColorScheme); mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors( currentColorScheme, isDarkTheme); } + MediaOutputColorScheme getColorScheme() { + return mMediaOutputColorScheme; + } + MediaOutputColorSchemeLegacy getColorSchemeLegacy() { return mMediaOutputColorSchemeLegacy; } @@ -786,8 +807,14 @@ public class MediaSwitchingController } } + @NonNull + MediaItem getConnectedSpeakersExpandableGroupDivider() { + return MediaItem.createExpandableGroupDividerMediaItem( + mContext.getString(R.string.media_output_group_title_connected_speakers)); + } + @Nullable - private MediaItem getConnectNewDeviceItem() { + MediaItem getConnectNewDeviceItem() { boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1; if (enableInputRouting()) { // When input routing is enabled, there are expected to be at least 2 total selected diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java index c12742eed169..2a9a47d83dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java @@ -79,6 +79,7 @@ import com.android.systemui.res.R; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import javax.inject.Inject; @@ -105,6 +106,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca private long mShowDelayMs = 0L; private final IBinder mWindowToken = new Binder(); private final CommandQueue mCommandQueue; + private final WindowManagerProvider mWindowManagerProvider; private ClingWindowView mClingWindow; /** The wrapper on the last {@link WindowManager} used to add the confirmation window. */ @@ -131,7 +133,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca @Inject public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue, - SecureSettings secureSettings, @Background Handler backgroundHandler) { + SecureSettings secureSettings, @Background Handler backgroundHandler, + WindowManagerProvider windowManagerProvider) { mSysUiContext = context; final Display display = mSysUiContext.getDisplay(); mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY @@ -139,6 +142,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca mCommandQueue = commandQueue; mSecureSettings = secureSettings; mBackgroundHandler = backgroundHandler; + mWindowManagerProvider = windowManagerProvider; } boolean loadSetting(int currentUserId) { @@ -523,7 +527,7 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca mWindowContextRootDisplayAreaId = rootDisplayAreaId; mWindowContext = mDisplayContext.createWindowContext( IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options); - mWindowManager = mWindowContext.getSystemService(WindowManager.class); + mWindowManager = mWindowManagerProvider.getWindowManager(mWindowContext); return mWindowManager; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java index f5d443443838..60a62d480633 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OperatorNameViewController.java @@ -97,7 +97,13 @@ public class OperatorNameViewController extends ViewController<OperatorNameView> boolean showOperatorName = mCarrierConfigTracker .getShowOperatorNameInStatusBarConfig(defaultSubInfo.getSubId()) - && (mTunerService.getValue(KEY_SHOW_OPERATOR_NAME, 1) != 0); + && (mTunerService.getValue( + KEY_SHOW_OPERATOR_NAME, + mView.getResources() + .getInteger( + com.android.internal.R.integer + .config_showOperatorNameDefault)) + != 0); mView.update( showOperatorName, mTelephonyManager.isDataCapable(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index 9fe3ff4c4bce..ff97bff53a65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -364,8 +364,6 @@ constructor( contentBuilder: PromotedNotificationContentModel.Builder, imageModelProvider: ImageModelProvider, ) { - contentBuilder.personIcon = null // TODO - contentBuilder.personName = null // TODO contentBuilder.verificationIcon = notification.skeletonVerificationIcon(imageModelProvider) contentBuilder.verificationText = notification.verificationText() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index ae6b2cc6cb1f..79081f42c686 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -88,8 +88,6 @@ data class PromotedNotificationContentModel( val style: Style, // for CallStyle: - val personIcon: ImageModel?, - val personName: CharSequence?, val verificationIcon: ImageModel?, val verificationText: CharSequence?, @@ -114,8 +112,6 @@ data class PromotedNotificationContentModel( var colors: Colors = Colors(backgroundColor = 0, primaryTextColor = 0) // for CallStyle: - var personIcon: ImageModel? = null - var personName: CharSequence? = null var verificationIcon: ImageModel? = null var verificationText: CharSequence? = null @@ -140,8 +136,6 @@ data class PromotedNotificationContentModel( oldProgress = oldProgress, colors = colors, style = style, - personIcon = personIcon, - personName = personName, verificationIcon = verificationIcon, verificationText = verificationText, newProgress = newProgress, @@ -200,8 +194,6 @@ data class PromotedNotificationContentModel( "oldProgress=$oldProgress, " + "colors=$colors, " + "style=$style, " + - "personIcon=${personIcon?.toRedactedString()}, " + - "personName=${personName?.toRedactedString()}, " + "verificationIcon=$verificationIcon, " + "verificationText=$verificationText, " + "newProgress=$newProgress)") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index afa988dd8e89..a01e504d47c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -64,7 +64,6 @@ import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; -import android.util.Pair; import android.view.DisplayCutout; import android.view.InputDevice; import android.view.LayoutInflater; @@ -141,7 +140,6 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.policy.ScrollAdapter; import com.android.systemui.statusbar.policy.SplitShadeStateController; -import com.android.systemui.statusbar.ui.SystemBarUtilsProxy; import com.android.systemui.util.Assert; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.DumpUtilsKt; @@ -2253,6 +2251,7 @@ public class NotificationStackScrollLayout } public void setFinishScrollingCallback(Runnable runnable) { + SceneContainerFlag.assertInLegacyMode(); mFinishScrollingCallback = runnable; } @@ -2763,6 +2762,8 @@ public class NotificationStackScrollLayout * which means we want to scroll towards the top. */ protected void fling(int velocityY) { + // Scrolls and flings are handled by the Composables with SceneContainer enabled + SceneContainerFlag.assertInLegacyMode(); if (getChildCount() > 0) { float topAmount = getCurrentOverScrollAmount(true); float bottomAmount = getCurrentOverScrollAmount(false); @@ -3857,7 +3858,10 @@ public class NotificationStackScrollLayout } break; case ACTION_UP: - if (mIsBeingDragged) { + if (SceneContainerFlag.isEnabled() && mIsBeingDragged) { + mActivePointerId = INVALID_POINTER; + endDrag(); + } else if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); @@ -3920,6 +3924,7 @@ public class NotificationStackScrollLayout } boolean isFlingAfterUpEvent() { + SceneContainerFlag.assertInLegacyMode(); return mFlingAfterUpEvent; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 7ac7905c8a48..db56718e9f22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -2091,7 +2091,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { // We log any touches other than down, which will be captured by onTouchEvent. // In the intercept we only start tracing when it's not a down (otherwise that down // would be duplicated when intercepted). - if (mJankMonitor != null && scrollWantsIt + if (!SceneContainerFlag.isEnabled() && mJankMonitor != null && scrollWantsIt && ev.getActionMasked() != MotionEvent.ACTION_DOWN) { mJankMonitor.begin(mView, CUJ_NOTIFICATION_SHADE_SCROLL_FLING); } @@ -2172,7 +2172,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { } mView.setCheckForLeaveBehind(true); } - traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); + if (!SceneContainerFlag.isEnabled()) { + traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); + } return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt || hunWantsIt; } diff --git a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java index 7d1c631e606a..757b2d973312 100644 --- a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java +++ b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java @@ -21,6 +21,8 @@ import android.hardware.display.DisplayManager; import android.view.Display; import android.view.WindowManager; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; + import javax.inject.Inject; /** @@ -29,14 +31,17 @@ import javax.inject.Inject; public class DisplayHelper { private final Context mContext; private final DisplayManager mDisplayManager; + private final WindowManagerProvider mWindowManagerProvider; /** * Default constructor. */ @Inject - public DisplayHelper(Context context, DisplayManager displayManager) { + public DisplayHelper(Context context, DisplayManager displayManager, + WindowManagerProvider windowManagerProvider) { mContext = context; mDisplayManager = displayManager; + mWindowManagerProvider = windowManagerProvider; } @@ -45,9 +50,8 @@ public class DisplayHelper { */ public Rect getMaxBounds(int displayId, int windowContextType) { final Display display = mDisplayManager.getDisplay(displayId); - WindowManager windowManager = mContext - .createDisplayContext(display).createWindowContext(windowContextType, null) - .getSystemService(WindowManager.class); + WindowManager windowManager = mWindowManagerProvider.getWindowManager(mContext + .createDisplayContext(display).createWindowContext(windowContextType, null)); return windowManager.getMaximumWindowMetrics().getBounds(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 8d9d06c8766e..d3d4e24001cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java @@ -49,6 +49,7 @@ import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Test; @@ -92,6 +93,8 @@ public class IMagnificationConnectionTest extends SysuiTestCase { private AccessibilityLogger mA11yLogger; @Mock private IWindowManager mIWindowManager; + @Mock + private WindowManagerProvider mWindowManagerProvider; private IMagnificationConnection mIMagnificationConnection; private MagnificationImpl mMagnification; @@ -113,7 +116,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue, mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), - mA11yLogger, mIWindowManager, mAccessibilityManager); + mA11yLogger, mIWindowManager, mAccessibilityManager, mWindowManagerProvider); mMagnification.mWindowMagnificationControllerSupplier = new FakeWindowMagnificationControllerSupplier( mContext.getSystemService(DisplayManager.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 505432e81b98..ae96e8fe7b8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -56,6 +56,7 @@ import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Test; @@ -96,6 +97,8 @@ public class MagnificationTest extends SysuiTestCase { private AccessibilityLogger mA11yLogger; @Mock private IWindowManager mIWindowManager; + @Mock + private WindowManagerProvider mWindowManagerProvider; @Before public void setUp() throws Exception { @@ -129,7 +132,7 @@ public class MagnificationTest extends SysuiTestCase { mCommandQueue, mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager, - getContext().getSystemService(AccessibilityManager.class)); + getContext().getSystemService(AccessibilityManager.class), mWindowManagerProvider); mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier( mContext.getSystemService(DisplayManager.class), mWindowMagnificationController); mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index 5713ddc8ae9d..5084f318b05d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -52,7 +52,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testDispatcher @@ -288,7 +287,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) @@ -970,8 +968,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasPartialProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with partial progress val progress = 0.5 val extras = @@ -997,8 +993,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasNotPlayedProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have not been played val extras = Bundle().apply { @@ -1022,8 +1016,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasFullProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with progress info val extras = Bundle().apply { @@ -1048,8 +1040,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasNoExtras() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that do not have any extras val desc = MediaDescription.Builder().run { @@ -1066,8 +1056,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasEmptyTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have empty title val desc = MediaDescription.Builder().run { @@ -1099,8 +1087,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa @Test fun testAddResumptionControls_hasBlankTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have a blank title val desc = MediaDescription.Builder().run { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 52e5e1520ec3..5ea669d74cc1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -56,7 +56,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME -import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testDispatcher @@ -312,7 +311,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) @@ -990,8 +988,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasPartialProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with partial progress val progress = 0.5 val extras = @@ -1017,8 +1013,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasNotPlayedProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have not been played val extras = Bundle().apply { @@ -1042,8 +1036,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasFullProgress() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added with progress info val extras = Bundle().apply { @@ -1068,8 +1060,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasNoExtras() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that do not have any extras val desc = MediaDescription.Builder().run { @@ -1086,8 +1076,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasEmptyTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have empty title val desc = MediaDescription.Builder().run { @@ -1119,8 +1107,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun testAddResumptionControls_hasBlankTitle() { - fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) - // WHEN resumption controls are added that have a blank title val desc = MediaDescription.Builder().run { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 07530e1c6f7b..76284fb81814 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -510,8 +510,7 @@ public class InputManagerService extends IInputManager.Stub KeyboardBacklightControllerInterface getKeyboardBacklightController( NativeInputManagerService nativeService) { - return new KeyboardBacklightController(mContext, nativeService, mLooper, - mUEventManager); + return new KeyboardBacklightController(mContext, nativeService, mLooper); } } diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5de432e5849b..b069a87480ad 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -762,7 +762,7 @@ final class KeyGestureController { if (!canceled) { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); } diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 16368c7678d1..083c0006ad65 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -32,7 +32,6 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; -import android.os.UEventObserver; import android.sysprop.InputProperties; import android.text.TextUtils; import android.util.IndentingPrintWriter; @@ -83,8 +82,6 @@ final class KeyboardBacklightController implements private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds(1).toMillis(); - private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; - @VisibleForTesting static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL = new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1]; @@ -93,7 +90,6 @@ final class KeyboardBacklightController implements private final NativeInputManagerService mNative; private final Handler mHandler; private final AnimatorFactory mAnimatorFactory; - private final UEventManager mUEventManager; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off @@ -124,19 +120,18 @@ final class KeyboardBacklightController implements } KeyboardBacklightController(Context context, NativeInputManagerService nativeService, - Looper looper, UEventManager uEventManager) { - this(context, nativeService, looper, ValueAnimator::ofInt, uEventManager); + Looper looper) { + this(context, nativeService, looper, ValueAnimator::ofInt); } @VisibleForTesting KeyboardBacklightController(Context context, NativeInputManagerService nativeService, - Looper looper, AnimatorFactory animatorFactory, UEventManager uEventManager) { + Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mHandler = new Handler(looper, this::handleMessage); mAnimatorFactory = animatorFactory; mAmbientController = new AmbientKeyboardBacklightController(context, looper); - mUEventManager = uEventManager; Resources res = mContext.getResources(); mUserInactivityThresholdMs = res.getInteger( com.android.internal.R.integer.config_keyboardBacklightTimeoutMs); @@ -154,17 +149,6 @@ final class KeyboardBacklightController implements inputManager.getInputDeviceIds()); mHandler.sendMessage(msg); - // Observe UEvents for "kbd_backlight" sysfs nodes. - // We want to observe creation of such LED nodes since they might be created after device - // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to - // backlight not working. - mUEventManager.addListener(new UEventManager.UEventListener() { - @Override - public void onUEvent(UEventObserver.UEvent event) { - onKeyboardBacklightUEvent(event); - } - }, UEVENT_KEYBOARD_BACKLIGHT_TAG); - // Start ambient backlight controller mAmbientController.systemRunning(); } @@ -414,17 +398,6 @@ final class KeyboardBacklightController implements } } - @VisibleForTesting - public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) { - if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase( - event.get("SUBSYSTEM"))) { - final String devPath = event.get("DEVPATH"); - if (isValidBacklightNodePath(devPath)) { - mNative.sysfsNodeChanged("/sys" + devPath); - } - } - } - private void updateAmbientLightListener() { boolean needToListenAmbientLightSensor = false; for (int i = 0; i < mKeyboardBacklights.size(); i++) { diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java index 2c1d68e3dbda..8d664e848ef5 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java @@ -55,15 +55,17 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList @Override public void onInputDeviceAdded(int deviceId) { + if (!mTouchpadVisualizerEnabled) { + return; + } final InputManager inputManager = Objects.requireNonNull( mContext.getSystemService(InputManager.class)); InputDevice inputDevice = inputManager.getInputDevice(deviceId); - - if (Objects.requireNonNull(inputDevice).supportsSource( - InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE) - && mTouchpadVisualizerEnabled) { - showDebugView(deviceId); + if (inputDevice == null || !inputDevice.supportsSource( + InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)) { + return; } + showDebugView(deviceId); } @Override diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index 0dc1b832f5a4..47e19089de92 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -192,9 +192,10 @@ import java.util.function.Consumer; * This is separate from the constructor so that this may be passed into the callback registered * with the HAL. * - * @throws InstantiationException on any failure + * @throws InstantiationException on unexpected failure + * @throws UnsupportedOperationException if not supported by the HAL */ - /* package */ void init() throws InstantiationException { + /* package */ void init() throws InstantiationException, UnsupportedOperationException { if (mSessionIdsValid) { throw new IllegalStateException("Already initialized"); } @@ -214,12 +215,11 @@ import java.util.function.Consumer; if (mHubInterface == null) { throw new IllegalStateException("Received null IEndpointCommunication"); } - } catch (RemoteException | IllegalStateException | ServiceSpecificException - | UnsupportedOperationException e) { + } catch (RemoteException | IllegalStateException | ServiceSpecificException e) { String error = "Failed to register ContextHubService as message hub"; Log.e(TAG, error, e); throw new InstantiationException(error); - } + } // Forward UnsupportedOperationException to caller int[] range = null; try { diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 2c0c55bd8df4..44996350e3f3 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -342,17 +342,19 @@ public class ContextHubService extends IContextHubService.Stub { new ContextHubEndpointManager( mContext, mContextHubWrapper, registry, mTransactionManager); mEndpointManager.init(); - Log.i(TAG, "Enabling generic offload API"); - } catch (InstantiationException e) { + Log.d(TAG, "Enabling generic offload API"); + } catch (InstantiationException | UnsupportedOperationException e) { mEndpointManager = null; registry = null; - Log.w(TAG, "Generic offload API not supported, disabling"); + if (e instanceof UnsupportedOperationException) { + Log.d(TAG, "Generic offload API not supported by HAL"); + } } mHubInfoRegistry = registry; } else { mHubInfoRegistry = null; mEndpointManager = null; - Log.i(TAG, "Disabling generic offload API"); + Log.d(TAG, "Disabling generic offload API due to flag config"); } initDefaultClientMap(); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 7e5ada54c953..38ac0473d1a3 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -5484,6 +5484,9 @@ public class ComputerEngine implements Computer { // For update or already installed case, leverage the existing visibility rule. if (targetAppId != INVALID_UID) { final Object targetSetting = mSettings.getSettingBase(targetAppId); + if (targetSetting == null) { + return false; + } if (targetSetting instanceof PackageSetting) { return !shouldFilterApplication( (PackageSetting) targetSetting, callingUid, userId); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index e8843ac214ec..d3aa0469435c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4158,7 +4158,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, @@ -4311,7 +4310,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: if (complete && isKeyEventForCurrentUser(event.getDisplayId(), event.getKeycodes()[0], "launchAllAppsViaA11y")) { launchAllAppsAction(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 16caec81f5f8..deee44dd7f61 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -47,7 +47,6 @@ import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.STATE_UNKNOWN; -import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.isSuspendedState; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.ROTATION_0; @@ -433,9 +432,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Ratio between overridden display density for current user and the initial display density, - * used only for external displays. + * used for updating the base density when resolution change happens to preserve display size. */ - float mExternalDisplayForcedDensityRatio = 0.0f; + float mForcedDisplayDensityRatio = 0.0f; boolean mIsDensityForced = false; /** @@ -3120,6 +3119,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mBaseRoundedCorners = loadRoundedCorners(baseWidth, baseHeight); } + // Update the base density if there is a forced density ratio. + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() + && mForcedDisplayDensityRatio != 0.0f) { + mBaseDisplayDensity = getBaseDensityFromRatio(); + } + if (mMaxUiWidth > 0 && mBaseDisplayWidth > mMaxUiWidth) { final float ratio = mMaxUiWidth / (float) mBaseDisplayWidth; mBaseDisplayHeight = (int) (mBaseDisplayHeight * ratio); @@ -3137,18 +3142,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp + mBaseDisplayHeight + " on display:" + getDisplayId()); } } - // Update the base density if there is a forced density ratio. - if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() - && mIsDensityForced && mExternalDisplayForcedDensityRatio != 0.0f) { - mBaseDisplayDensity = (int) - (mInitialDisplayDensity * mExternalDisplayForcedDensityRatio + 0.5); - } if (mDisplayReady && !mDisplayPolicy.shouldKeepCurrentDecorInsets()) { mDisplayPolicy.mDecorInsets.invalidate(); } } /** + * Returns the forced density from forcedDensityRatio if the ratio is valid by rounding the + * density down to an even number. Returns the initial density if the ratio is 0. + */ + private int getBaseDensityFromRatio() { + return (mForcedDisplayDensityRatio != 0.0f) + ? ((int) (mInitialDisplayDensity * mForcedDisplayDensityRatio)) & ~1 + : mInitialDisplayDensity; + } + + /** * Forces this display to use the specified density. * * @param density The density in DPI to use. If the value equals to initial density, the setting @@ -3172,15 +3181,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (density == getInitialDisplayDensity()) { density = 0; } - // Save the new density ratio to settings for external displays. - if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() - && mDisplayInfo.type == TYPE_EXTERNAL) { - mExternalDisplayForcedDensityRatio = (float) - mBaseDisplayDensity / getInitialDisplayDensity(); + mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId); + } + + void setForcedDensityRatio(float ratio, int userId) { + // Save the new density ratio to settings and update forced density with the ratio. + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue()) { + mForcedDisplayDensityRatio = ratio; mWmService.mDisplayWindowSettings.setForcedDensityRatio(getDisplayInfo(), - mExternalDisplayForcedDensityRatio); + mForcedDisplayDensityRatio); + + // Set forced density from ratio. + setForcedDensity(getBaseDensityFromRatio(), userId); } - mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId); } /** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */ @@ -3403,6 +3416,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp void removeImmediately() { mDeferredRemoval = false; try { + if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue() + && mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) { + mDisplayPolicy.notifyDisplayRemoveSystemDecorations(); + } mUnknownAppVisibilityController.clear(); mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); mDeviceStateController.unregisterDeviceStateCallback(mDeviceStateConsumer); diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index 56579206566f..2818d79a40ad 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -391,7 +391,7 @@ class DisplayWindowSettings { final int density = hasDensityOverride ? settings.mForcedDensity : dc.getInitialDisplayDensity(); if (hasDensityOverrideRatio) { - dc.mExternalDisplayForcedDensityRatio = settings.mForcedDensityRatio; + dc.mForcedDisplayDensityRatio = settings.mForcedDensityRatio; } dc.updateBaseDisplayMetrics(width, height, density, dc.mBaseDisplayPhysicalXDpi, diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index b9550feeab8a..24b5f618e32b 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -49,6 +49,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -83,6 +84,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.am.ActivityManagerService; +import com.android.window.flags.Flags; import com.google.android.collect.Sets; @@ -1452,9 +1454,10 @@ class RecentTasks { * @return whether the given active task should be presented to the user through SystemUI. */ @VisibleForTesting - boolean isVisibleRecentTask(Task task) { + boolean isVisibleRecentTask(@NonNull Task task) { if (DEBUG_RECENTS_TRIM_TASKS) { Slog.d(TAG, "isVisibleRecentTask: task=" + task + + " isForceExcludedFromRecents=" + task.isForceExcludedFromRecents() + " minVis=" + mMinNumVisibleTasks + " maxVis=" + mMaxNumVisibleTasks + " sessionDuration=" + mActiveTasksSessionDurationMs + " inactiveDuration=" + task.getInactiveDuration() @@ -1464,6 +1467,11 @@ class RecentTasks { + " intentFlags=" + task.getBaseIntent().getFlags()); } + // Ignore the task if it is force excluded from recents. + if (Flags.excludeTaskFromRecents() && task.isForceExcludedFromRecents()) { + return false; + } + switch (task.getActivityType()) { case ACTIVITY_TYPE_HOME: case ACTIVITY_TYPE_RECENTS: diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 3cce17242648..89634707995a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -625,6 +625,9 @@ class Task extends TaskFragment { boolean mAlignActivityLocaleWithTask = false; + /** @see #isForceExcludedFromRecents() */ + private boolean mForceExcludedFromRecents; + private Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent, Intent _affinityIntent, String _affinity, String _rootAffinity, ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset, @@ -3842,7 +3845,8 @@ class Task extends TaskFragment { pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime); pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)"); pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents); - pw.print(" isForceHidden="); pw.println(isForceHidden()); + pw.print(" isForceHidden="); pw.print(isForceHidden()); + pw.print(" isForceExcludedFromRecents="); pw.println(isForceExcludedFromRecents()); if (mLaunchAdjacentDisabled) { pw.println(prefix + "mLaunchAdjacentDisabled=true"); } @@ -4555,11 +4559,45 @@ class Task extends TaskFragment { /** * @return whether this task is always on top without taking visibility into account. + * @deprecated b/388630258 replace hidden bubble tasks with reordering. + * {@link RecentTasks#isVisibleRecentTask} now checks {@link #isForceExcludedFromRecents}. */ - public boolean isAlwaysOnTopWhenVisible() { + @Deprecated + boolean isAlwaysOnTopWhenVisible() { return super.isAlwaysOnTop(); } + /** + * Returns whether this task is forcibly excluded from the Recents list. + * + * <p>This flag is used by {@link RecentTasks#isVisibleRecentTask} to determine + * if the task should be presented to the user through SystemUI. If this method + * returns {@code true}, the task will not be shown in Recents, regardless of other + * visibility criteria. + * + * @return {@code true} if the task is excluded, {@code false} otherwise. + */ + boolean isForceExcludedFromRecents() { + return mForceExcludedFromRecents; + } + + /** + * Sets whether this task should be forcibly excluded from the Recents list. + * + * <p>This method is intended to be used in conjunction with + * {@link android.window.WindowContainerTransaction#setTaskForceExcludedFromRecents} to modify the + * task's exclusion state. + * + * @param excluded {@code true} to exclude the task, {@code false} otherwise. + */ + void setForceExcludedFromRecents(boolean excluded) { + if (!Flags.excludeTaskFromRecents()) { + Slog.w(TAG, "Flag " + Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS + " is not enabled"); + return; + } + mForceExcludedFromRecents = excluded; + } + boolean isForceHiddenForPinnedTask() { return (mForceHiddenFlags & FLAG_FORCE_HIDDEN_FOR_PINNED_TASK) != 0; } diff --git a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java index acb6061de93f..dc6b70d839e4 100644 --- a/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java +++ b/services/core/java/com/android/server/wm/TaskSystemBarsListenerController.java @@ -48,8 +48,10 @@ final class TaskSystemBarsListenerController { int taskId, boolean visible, boolean wereRevealedFromSwipeOnSystemBar) { - HashSet<TaskSystemBarsListener> localListeners; - localListeners = new HashSet<>(mListeners); + if (mListeners.isEmpty()) { + return; + } + final HashSet<TaskSystemBarsListener> localListeners = new HashSet<>(mListeners); mBackgroundExecutor.execute(() -> { for (TaskSystemBarsListener listener : localListeners) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index b1422c20e516..247a51d9fcb3 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3203,7 +3203,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mLocalInsetsSources.valueAt(i).dump(childPrefix, pw); } } - pw.println(prefix + mSafeRegionBounds + " SafeRegionBounds"); + if (mSafeRegionBounds != null) { + pw.println(prefix + "mSafeRegionBounds=" + mSafeRegionBounds); + } } final void updateSurfacePositionNonOrganized() { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index c23dabcd2a48..aa5eb33d8069 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -3161,6 +3161,11 @@ public class WindowManagerService extends IWindowManager.Stub // Reparent the window created for this window context. dc.reParentWindowToken(token); hideUntilNextDraw(token); + // Prevent a race condition where VRI temporarily reverts the context display ID + // before the onDisplayMoved callback arrives. This caused incorrect display IDs + // during configuration changes, breaking SysUI layouts dependent on it. + // Forcing a resize report ensures VRI has the correct ID before the update. + forceReportResizing(token); // This makes sure there is a traversal scheduled that will eventually report // the window resize to the client. dc.setLayoutNeeded(); @@ -3182,6 +3187,14 @@ public class WindowManagerService extends IWindowManager.Stub } } + private void forceReportResizing(@NonNull WindowContainer<?> wc) { + wc.forAllWindows(w -> { + if (!mResizingWindows.contains(w)) { + mResizingWindows.add(w); + } + }, true /* traverseTopToBottom */); + } + private void hideUntilNextDraw(@NonNull WindowToken token) { final WindowState topChild = token.getTopChild(); if (topChild != null) { @@ -6231,6 +6244,10 @@ public class WindowManagerService extends IWindowManager.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + // Clear forced display density ratio + setForcedDisplayDensityRatioInternal(displayId, 0.0f, userId); + + // Clear forced display density final DisplayContent displayContent = mRoot.getDisplayContent(displayId); if (displayContent != null) { displayContent.setForcedDensity(displayContent.getInitialDisplayDensity(), @@ -6255,6 +6272,37 @@ public class WindowManagerService extends IWindowManager.Stub @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @Override + public void setForcedDisplayDensityRatio(int displayId, float ratio, int userId) { + setForcedDisplayDensityRatio_enforcePermission(); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + setForcedDisplayDensityRatioInternal(displayId, ratio, userId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void setForcedDisplayDensityRatioInternal( + int displayId, float ratio, int userId) { + final DisplayContent displayContent = mRoot.getDisplayContent(displayId); + if (displayContent != null) { + displayContent.setForcedDensityRatio(ratio, userId); + return; + } + + final DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId); + if (info == null) { + ProtoLog.e(WM_ERROR, "Failed to get information about logical display %d. " + + "Skip setting forced display density.", displayId); + return; + } + mDisplayWindowSettings.setForcedDensityRatio(info, ratio); + } + + @EnforcePermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @Override public void setConfigurationChangeSettingsForUser( @NonNull List<ConfigurationChangeSetting> settings, int userId) { setConfigurationChangeSettingsForUser_enforcePermission(); diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index cca73c574951..7823b92e4057 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -134,15 +134,6 @@ class WindowToken extends WindowContainer<WindowState> { } /** - * Transforms the window container from the next rotation to the current rotation for - * showing the window in a display with different rotation. - */ - void transform(WindowContainer<?> container) { - // The default implementation assumes shell transition is enabled, so the transform - // is done by getOrCreateFixedRotationLeash(). - } - - /** * Resets the transformation of the window containers which have been rotated. This should * be called when the window has the same rotation as display. */ @@ -158,45 +149,6 @@ class WindowToken extends WindowContainer<WindowState> { } } - private static class FixedRotationTransformStateLegacy extends FixedRotationTransformState { - final SeamlessRotator mRotator; - final ArrayList<WindowContainer<?>> mRotatedContainers = new ArrayList<>(3); - - FixedRotationTransformStateLegacy(DisplayInfo rotatedDisplayInfo, - DisplayFrames rotatedDisplayFrames, Configuration rotatedConfig, - int currentRotation) { - super(rotatedDisplayInfo, rotatedDisplayFrames, rotatedConfig); - // This will use unrotate as rotate, so the new and old rotation are inverted. - mRotator = new SeamlessRotator(rotatedDisplayInfo.rotation, currentRotation, - rotatedDisplayInfo, true /* applyFixedTransformationHint */); - } - - @Override - void transform(WindowContainer<?> container) { - mRotator.unrotate(container.getPendingTransaction(), container); - if (!mRotatedContainers.contains(container)) { - mRotatedContainers.add(container); - } - } - - @Override - void resetTransform() { - for (int i = mRotatedContainers.size() - 1; i >= 0; i--) { - final WindowContainer<?> c = mRotatedContainers.get(i); - // If the window is detached (no parent), its surface may have been released. - if (c.getParent() != null) { - mRotator.finish(c.getPendingTransaction(), c); - } - } - } - - @Override - void disassociate(WindowToken token) { - super.disassociate(token); - mRotatedContainers.remove(token); - } - } - /** * Compares two child window of this token and returns -1 if the first is lesser than the * second in terms of z-order and 1 otherwise. @@ -494,10 +446,7 @@ class WindowToken extends WindowContainer<WindowState> { mFixedRotationTransformState.disassociate(this); } config = new Configuration(config); - mFixedRotationTransformState = mTransitionController.isShellTransitionsEnabled() - ? new FixedRotationTransformState(info, displayFrames, config) - : new FixedRotationTransformStateLegacy(info, displayFrames, config, - mDisplayContent.getRotation()); + mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames, config); mFixedRotationTransformState.mAssociatedTokens.add(this); mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames); onFixedRotationStatePrepared(); @@ -699,15 +648,6 @@ class WindowToken extends WindowContainer<WindowState> { return; } super.updateSurfacePosition(t); - if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) { - final Task rootTask = r != null ? r.getRootTask() : null; - // Don't transform the activity in PiP because the PiP task organizer will handle it. - if (rootTask == null || !rootTask.inPinnedWindowingMode()) { - // The window is laid out in a simulated rotated display but the real display hasn't - // rotated, so here transforms its surface to fit in the real display. - mFixedRotationTransformState.transform(this); - } - } } @Override diff --git a/services/core/jni/gnss/GnssAssistance.cpp b/services/core/jni/gnss/GnssAssistance.cpp index fff396ea126a..e97c7c340e40 100644 --- a/services/core/jni/gnss/GnssAssistance.cpp +++ b/services/core/jni/gnss/GnssAssistance.cpp @@ -206,7 +206,7 @@ jmethodID method_beidouSatelliteClockModelGetTgd2; jmethodID method_beidouSatelliteClockModelGetTimeOfClockSeconds; jmethodID method_beidouSatelliteHealthGetSatH1; jmethodID method_beidouSatelliteHealthGetSvAccur; -jmethodID method_beidouSatelliteEphemerisTimeGetIode; +jmethodID method_beidouSatelliteEphemerisTimeGetAode; jmethodID method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber; jmethodID method_beidouSatelliteEphemerisTimeGetToeSeconds; @@ -710,8 +710,8 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) { // Get the methods of BeidouSatelliteEphemerisTime jclass beidouSatelliteEphemerisTimeClass = env->FindClass( "android/location/BeidouSatelliteEphemeris$BeidouSatelliteEphemerisTime"); - method_beidouSatelliteEphemerisTimeGetIode = - env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getIode", "()I"); + method_beidouSatelliteEphemerisTimeGetAode = + env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getAode", "()I"); method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber = env->GetMethodID(beidouSatelliteEphemerisTimeClass, "getBeidouWeekNumber", "()I"); method_beidouSatelliteEphemerisTimeGetToeSeconds = @@ -723,7 +723,7 @@ void GnssAssistance_class_init_once(JNIEnv* env, jclass clazz) { "()Landroid/location/GnssAlmanac;"); method_galileoAssistanceGetIonosphericModel = env->GetMethodID(galileoAssistanceClass, "getIonosphericModel", - "()Landroid/location/KlobucharIonosphericModel;"); + "()Landroid/location/GalileoIonosphericModel;"); method_galileoAssistanceGetUtcModel = env->GetMethodID(galileoAssistanceClass, "getUtcModel", "()Landroid/location/UtcModel;"); method_galileoAssistanceGetLeapSecondsModel = @@ -1244,8 +1244,7 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist env->CallObjectMethod(galileoAssistanceObj, method_galileoAssistanceGetSatelliteCorrections); setGnssAlmanac(env, galileoAlmanacObj, galileoAssistance.almanac); - setGaliloKlobucharIonosphericModel(env, ionosphericModelObj, - galileoAssistance.ionosphericModel); + setGalileoIonosphericModel(env, ionosphericModelObj, galileoAssistance.ionosphericModel); setUtcModel(env, utcModelObj, galileoAssistance.utcModel); setLeapSecondsModel(env, leapSecondsModelObj, galileoAssistance.leapSecondsModel); setTimeModels(env, timeModelsObj, galileoAssistance.timeModels); @@ -1263,9 +1262,8 @@ void GnssAssistanceUtil::setGalileoAssistance(JNIEnv* env, jobject galileoAssist env->DeleteLocalRef(satelliteCorrectionsObj); } -void GnssAssistanceUtil::setGaliloKlobucharIonosphericModel( - JNIEnv* env, jobject galileoIonosphericModelObj, - GalileoIonosphericModel& ionosphericModel) { +void GnssAssistanceUtil::setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, + GalileoIonosphericModel& ionosphericModel) { if (galileoIonosphericModelObj == nullptr) return; jdouble ai0 = env->CallDoubleMethod(galileoIonosphericModelObj, method_galileoIonosphericModelGetAi0); @@ -1500,14 +1498,14 @@ void GnssAssistanceUtil::setBeidouSatelliteEphemeris( jobject satelliteEphemerisTimeObj = env->CallObjectMethod(beidouSatelliteEphemerisObj, method_beidouSatelliteEphemerisGetSatelliteEphemerisTime); - jint iode = env->CallIntMethod(satelliteEphemerisTimeObj, - method_beidouSatelliteEphemerisTimeGetIode); + jint aode = env->CallIntMethod(satelliteEphemerisTimeObj, + method_beidouSatelliteEphemerisTimeGetAode); jint beidouWeekNumber = env->CallIntMethod(satelliteEphemerisTimeObj, method_beidouSatelliteEphemerisTimeGetBeidouWeekNumber); jint toeSeconds = env->CallDoubleMethod(satelliteEphemerisTimeObj, method_beidouSatelliteEphemerisTimeGetToeSeconds); - beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(iode); + beidouSatelliteEphemeris.satelliteEphemerisTime.aode = static_cast<int32_t>(aode); beidouSatelliteEphemeris.satelliteEphemerisTime.weekNumber = static_cast<int32_t>(beidouWeekNumber); beidouSatelliteEphemeris.satelliteEphemerisTime.toeSeconds = diff --git a/services/core/jni/gnss/GnssAssistance.h b/services/core/jni/gnss/GnssAssistance.h index ee97e19371f8..968e661fbaed 100644 --- a/services/core/jni/gnss/GnssAssistance.h +++ b/services/core/jni/gnss/GnssAssistance.h @@ -94,8 +94,8 @@ struct GnssAssistanceUtil { static void setGalileoSatelliteEphemeris( JNIEnv* env, jobject galileoSatelliteEphemerisObj, std::vector<GalileoSatelliteEphemeris>& galileoSatelliteEphemerisList); - static void setGaliloKlobucharIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, - GalileoIonosphericModel& ionosphericModel); + static void setGalileoIonosphericModel(JNIEnv* env, jobject galileoIonosphericModelObj, + GalileoIonosphericModel& ionosphericModel); static void setGnssAssistance(JNIEnv* env, jobject gnssAssistanceObj, GnssAssistance& gnssAssistance); static void setGpsAssistance(JNIEnv* env, jobject gpsAssistanceObj, diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index 3d740e531e14..a2d6510007bc 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -268,7 +268,7 @@ public class RequestSessionMetric { * @param createOrCredentialType the string type to collect when an entry is tapped by the user */ public void collectChosenClassType(String createOrCredentialType) { - String truncatedType = generateMetricKey(createOrCredentialType, DELTA_EXCEPTION_CUT); + String truncatedType = generateMetricKey(createOrCredentialType, DELTA_RESPONSES_CUT); try { mChosenProviderFinalPhaseMetric.setChosenClassType(truncatedType); } catch (Exception e) { diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index 0b5a95b0e888..c419fd2ecbd7 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -17,6 +17,7 @@ package com.android.server.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_ROLE_HOLDERS; import static android.Manifest.permission.MANAGE_USERS; import static android.Manifest.permission.QUERY_USERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -171,6 +172,44 @@ public class SupervisionService extends ISupervisionManager.Stub { } @Override + public boolean shouldAllowBypassingSupervisionRoleQualification() { + enforcePermission(MANAGE_ROLE_HOLDERS); + + if (hasNonTestDefaultUsers()) { + return false; + } + + synchronized (getLockObject()) { + for (int i = 0; i < mUserData.size(); i++) { + if (mUserData.valueAt(i).supervisionEnabled) { + return false; + } + } + } + + return true; + } + + /** + * Returns true if there are any non-default non-test users. + * + * This excludes the system and main user(s) as those users are created by default. + */ + private boolean hasNonTestDefaultUsers() { + List<UserInfo> users = mInjector.getUserManagerInternal().getUsers(true); + for (var user : users) { + if (!user.isForTesting() && !user.isMain() && !isSystemUser(user)) { + return true; + } + } + return false; + } + + private static boolean isSystemUser(UserInfo userInfo) { + return (userInfo.flags & UserInfo.FLAG_SYSTEM) == UserInfo.FLAG_SYSTEM; + } + + @Override public void onShellCommand( @Nullable FileDescriptor in, @Nullable FileDescriptor out, diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index d3c3178f3513..a99e1b1f28e1 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -63,6 +63,7 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "CtsAccessibilityCommon", "cts-wm-util", "platform-compat-test-rules", "platform-parametric-runner-lib", diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 4531b3948495..ef478e8ff1e1 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -129,6 +129,21 @@ <application android:testOnly="true" android:debuggable="true"> <uses-library android:name="android.test.runner"/> + <service + android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestMagnificationAccessibilityService" + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:exported="true"> + <intent-filter> + <action android:name="android.accessibilityservice.AccessibilityService" /> + <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" /> + </intent-filter> + + <meta-data + android:name="android.accessibilityservice" + android:resource="@xml/test_magnification_a11y_service" /> + </service> + <activity android:name="com.android.server.accessibility.integration.FullScreenMagnificationMouseFollowingTest$TestActivity" /> + <service android:name="com.android.server.accounts.TestAccountType1AuthenticatorService" android:exported="false"> <intent-filter> diff --git a/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml new file mode 100644 index 000000000000..d28cdca6a26a --- /dev/null +++ b/services/tests/servicestests/res/xml/test_magnification_a11y_service.xml @@ -0,0 +1,24 @@ +<?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. +--> +<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" + android:accessibilityEventTypes="typeAllMask" + android:accessibilityFeedbackType="feedbackGeneric" + android:canRetrieveWindowContent="true" + android:canRequestTouchExplorationMode="true" + android:canRequestEnhancedWebAccessibility="true" + android:canRequestFilterKeyEvents="true" + android:canControlMagnification="true" + android:canPerformGestures="true"/>
\ No newline at end of file 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 0f418ab5d19c..900d5ad58719 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 @@ -331,15 +331,7 @@ public class AutoclickControllerTest { injectFakeMouseActionHoverMoveEvent(); // Send hover enter event. - MotionEvent hoverEnter = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_ENTER, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverEnter.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_ENTER); // Verify there is no pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -351,15 +343,7 @@ public class AutoclickControllerTest { injectFakeMouseActionHoverMoveEvent(); // 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify there is a pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -368,39 +352,15 @@ public class AutoclickControllerTest { @Test public void smallJitteryMovement_doesNotTriggerClick() { // Initial hover move to set an anchor point. - MotionEvent initialHoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 40f, - /* metaState= */ 0); - initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE); // Get the initial scheduled click time. long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); // Simulate small, jittery movements (all within the default slop). - MotionEvent jitteryMove1 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 31f, // Small change in x - /* y= */ 41f, // Small change in y - /* metaState= */ 0); - jitteryMove1.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(jitteryMove1, jitteryMove1, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 31f, /* y= */ 41f, MotionEvent.ACTION_HOVER_MOVE); - MotionEvent jitteryMove2 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30.5f, // Small change in x - /* y= */ 39.8f, // Small change in y - /* metaState= */ 0); - jitteryMove2.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(jitteryMove2, jitteryMove2, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30.5f, /* y= */ 39.8f, MotionEvent.ACTION_HOVER_MOVE); // Verify that the scheduled click time has NOT changed. assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) @@ -410,29 +370,13 @@ public class AutoclickControllerTest { @Test public void singleSignificantMovement_triggersClick() { // Initial hover move to set an anchor point. - MotionEvent initialHoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 40f, - /* metaState= */ 0); - initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 40f, MotionEvent.ACTION_HOVER_MOVE); // Get the initial scheduled click time. long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); - // Simulate a single, significant movement (greater than the default slop). - MotionEvent significantMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 60f, // Significant change in x (30f difference) - /* y= */ 70f, // Significant change in y (30f difference) - /* metaState= */ 0); - significantMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(significantMove, significantMove, /* policyFlags= */ 0); + // Significant change in x (30f difference) and y (30f difference) + injectFakeMouseMoveEvent(/* x= */ 60f, /* y= */ 70f, MotionEvent.ACTION_HOVER_MOVE); // Verify that the scheduled click time has changed (click was rescheduled). assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) @@ -459,15 +403,7 @@ public class AutoclickControllerTest { // Move the mouse down, less than customSize radius so a click is not triggered. float moveDownY = customSize - 25; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 0f, - /* y= */ moveDownY, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); } @@ -491,15 +427,7 @@ public class AutoclickControllerTest { // Move the mouse right, greater than customSize radius so a click is triggered. float moveRightX = customSize + 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ moveRightX, - /* y= */ 0, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); } @@ -522,15 +450,7 @@ public class AutoclickControllerTest { // Move the mouse down less than customSize radius but ignore custom movement is not enabled // so a click is triggered. float moveDownY = customSize - 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 150, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 0f, - /* y= */ moveDownY, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 0, /* y= */ moveDownY, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); } @@ -554,15 +474,7 @@ public class AutoclickControllerTest { // After enabling ignore custom movement, move the mouse right, less than customSize radius // so a click won't be triggered. float moveRightX = customSize - 100; - MotionEvent hoverMove = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ moveRightX, - /* y= */ 0, - /* metaState= */ 0); - hoverMove.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ moveRightX, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); } @@ -600,15 +512,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify there is not a pending click. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -624,7 +528,7 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); // Send move again to trigger click and verify there is now a pending click. - mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); } @@ -641,15 +545,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is not triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); @@ -668,15 +564,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -695,15 +583,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); // Verify click is triggered. assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); @@ -781,15 +661,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click sent. @@ -814,15 +686,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify right click sent. @@ -850,15 +714,7 @@ public class AutoclickControllerTest { mController.mAutoclickScrollPanel = mockScrollPanel; // First hover move event. - MotionEvent hoverMove1 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 100, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 30f, - /* y= */ 0f, - /* metaState= */ 0); - hoverMove1.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove1, hoverMove1, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 0, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify scroll panel is shown once. @@ -866,15 +722,7 @@ public class AutoclickControllerTest { assertThat(motionEventCaptor.downEvent).isNull(); // Second significant hover move event to trigger another autoclick. - MotionEvent hoverMove2 = MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 200, - /* action= */ MotionEvent.ACTION_HOVER_MOVE, - /* x= */ 100f, - /* y= */ 100f, - /* metaState= */ 0); - hoverMove2.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(hoverMove2, hoverMove2, /* policyFlags= */ 0); + injectFakeMouseMoveEvent(/* x= */ 100f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify scroll panel is still only shown once (not called again). @@ -918,15 +766,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click is sent due to the mouse hovering the panel. @@ -1039,29 +879,6 @@ public class AutoclickControllerTest { assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); } - private void injectFakeMouseActionHoverMoveEvent() { - MotionEvent event = getFakeMotionHoverMoveEvent(); - event.setSource(InputDevice.SOURCE_MOUSE); - mController.onMotionEvent(event, event, /* policyFlags= */ 0); - } - - private void injectFakeNonMouseActionHoverMoveEvent() { - MotionEvent event = getFakeMotionHoverMoveEvent(); - event.setSource(InputDevice.SOURCE_KEYBOARD); - mController.onMotionEvent(event, event, /* policyFlags= */ 0); - } - - private void injectFakeKeyEvent(int keyCode, int modifiers) { - KeyEvent keyEvent = new KeyEvent( - /* downTime= */ 0, - /* eventTime= */ 0, - /* action= */ KeyEvent.ACTION_DOWN, - /* code= */ keyCode, - /* repeat= */ 0, - /* metaState= */ modifiers); - mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); - } - @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void sendClick_clickType_doubleclick_triggerClickTwice() { @@ -1079,15 +896,7 @@ public class AutoclickControllerTest { 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); + injectFakeMouseMoveEvent(/* x= */ 30f, /* y= */ 100f, MotionEvent.ACTION_HOVER_MOVE); mTestableLooper.processAllMessages(); // Verify left click sent. @@ -1097,6 +906,45 @@ public class AutoclickControllerTest { assertThat(motionEventCaptor.eventCount).isEqualTo(2); } + /** + * ========================================================================= + * Helper Functions + * ========================================================================= + */ + + private void injectFakeMouseActionHoverMoveEvent() { + injectFakeMouseMoveEvent(0, 0, MotionEvent.ACTION_HOVER_MOVE); + } + + private void injectFakeMouseMoveEvent(float x, float y, int action) { + MotionEvent event = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ action, + /* x= */ x, + /* y= */ y, + /* metaState= */ 0); + event.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(event, event, /* policyFlags= */ 0); + } + + private void injectFakeNonMouseActionHoverMoveEvent() { + MotionEvent event = getFakeMotionHoverMoveEvent(); + event.setSource(InputDevice.SOURCE_KEYBOARD); + mController.onMotionEvent(event, event, /* policyFlags= */ 0); + } + + private void injectFakeKeyEvent(int keyCode, int modifiers) { + KeyEvent keyEvent = new KeyEvent( + /* downTime= */ 0, + /* eventTime= */ 0, + /* action= */ KeyEvent.ACTION_DOWN, + /* code= */ keyCode, + /* repeat= */ 0, + /* metaState= */ modifiers); + mController.onKeyEvent(keyEvent, /* policyFlags= */ 0); + } + private MotionEvent getFakeMotionHoverMoveEvent() { return MotionEvent.obtain( /* downTime= */ 0, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt new file mode 100644 index 000000000000..679bba4017fb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/integration/FullScreenMagnificationMouseFollowingTest.kt @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.accessibility.integration + +import android.Manifest +import android.accessibility.cts.common.AccessibilityDumpOnFailureRule +import android.accessibility.cts.common.InstrumentedAccessibilityService +import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityServiceInfo +import android.accessibilityservice.MagnificationConfig +import android.app.Activity +import android.app.Instrumentation +import android.app.UiAutomation +import android.companion.virtual.VirtualDeviceManager +import android.graphics.PointF +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.hardware.input.InputManager +import android.hardware.input.VirtualMouse +import android.hardware.input.VirtualMouseConfig +import android.hardware.input.VirtualMouseRelativeEvent +import android.os.Handler +import android.os.Looper +import android.os.OutcomeReceiver +import android.platform.test.annotations.RequiresFlagsEnabled +import android.testing.PollingCheck +import android.view.Display +import android.view.InputDevice +import android.view.MotionEvent +import android.virtualdevice.cts.common.VirtualDeviceRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.accessibility.Flags +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.math.abs +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +// Convenient extension functions for float. +private const val EPS = 0.00001f +private fun Float.nearEq(other: Float) = abs(this - other) < EPS +private fun PointF.nearEq(other: PointF) = this.x.nearEq(other.x) && this.y.nearEq(other.y) + +/** End-to-end tests for full screen magnification following mouse cursor. */ +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) +class FullScreenMagnificationMouseFollowingTest { + + private lateinit var instrumentation: Instrumentation + private lateinit var uiAutomation: UiAutomation + + private val magnificationAccessibilityServiceRule = + InstrumentedAccessibilityServiceTestRule<TestMagnificationAccessibilityService>( + TestMagnificationAccessibilityService::class.java, false + ) + private lateinit var service: TestMagnificationAccessibilityService + + // virtualDeviceRule tears down `virtualDevice` and `virtualDisplay`. + // Note that CheckFlagsRule is a part of VirtualDeviceRule. See its javadoc. + val virtualDeviceRule: VirtualDeviceRule = + VirtualDeviceRule.withAdditionalPermissions(Manifest.permission.MANAGE_ACTIVITY_TASKS) + private lateinit var virtualDevice: VirtualDeviceManager.VirtualDevice + private lateinit var virtualDisplay: VirtualDisplay + + // Once created, it's our responsibility to close the mouse. + private lateinit var virtualMouse: VirtualMouse + + @get:Rule + val ruleChain: RuleChain = + RuleChain.outerRule(virtualDeviceRule) + .around(magnificationAccessibilityServiceRule) + .around(AccessibilityDumpOnFailureRule()) + + @Before + fun setUp() { + instrumentation = InstrumentationRegistry.getInstrumentation() + uiAutomation = + instrumentation.getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES) + uiAutomation.serviceInfo = + uiAutomation.serviceInfo!!.apply { + flags = flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS + } + + prepareVirtualDevices() + + launchTestActivityFullscreen(virtualDisplay.display.displayId) + + service = magnificationAccessibilityServiceRule.enableService() + service.observingDisplayId = virtualDisplay.display.displayId + } + + @After + fun cleanUp() { + if (this::virtualMouse.isInitialized) { + virtualMouse.close() + } + } + + // Note on continuous movement: + // Assume that the entire display is magnified, and the zoom level is z. + // In continuous movement, mouse speed relative to the unscaled physical display is the same as + // unmagnified speed. While, when a cursor moves from the left edge to the right edge of the + // screen, the magnification center moves from the left bound to the right bound, which is + // (display width) * (z - 1) / z. + // + // Similarly, when the mouse cursor moves by d in unscaled, display coordinates, + // the magnification center moves by d * (z - 1) / z. + + @Test + fun testContinuous_toBottomRight() { + ensureMouseAtCenter() + + val controller = service.getMagnificationController(virtualDisplay.display.displayId) + + scaleTo(controller, 2f) + assertMagnification(controller, scale = 2f, CENTER_X, CENTER_Y) + + // Move cursor by (10, 15) + // This will move magnification center by (5, 7.5) + sendMouseMove(10f, 15f) + assertCursorLocation(CENTER_X + 10, CENTER_Y + 15) + assertMagnification(controller, scale = 2f, CENTER_X + 5, CENTER_Y + 7.5f) + + // Move cursor to the rest of the way to the edge. + sendMouseMove(DISPLAY_WIDTH - 10, DISPLAY_HEIGHT - 15) + assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1) + assertMagnification(controller, scale = 2f, DISPLAY_WIDTH * 3 / 4, DISPLAY_HEIGHT * 3 / 4) + + // Move cursor further won't move the magnification. + sendMouseMove(100f, 100f) + assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1) + } + + @Test + fun testContinuous_toTopLeft() { + ensureMouseAtCenter() + + val controller = service.getMagnificationController(virtualDisplay.display.displayId) + + scaleTo(controller, 3f) + assertMagnification(controller, scale = 3f, CENTER_X, CENTER_Y) + + // Move cursor by (-30, -15) + // This will move magnification center by (-20, -10) + sendMouseMove(-30f, -15f) + assertCursorLocation(CENTER_X - 30, CENTER_Y - 15) + assertMagnification(controller, scale = 3f, CENTER_X - 20, CENTER_Y - 10) + + // Move cursor to the rest of the way to the edge. + sendMouseMove(-CENTER_X + 30, -CENTER_Y + 15) + assertCursorLocation(0f, 0f) + assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6) + + // Move cursor further won't move the magnification. + sendMouseMove(-100f, -100f) + assertCursorLocation(0f, 0f) + assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6) + } + + private fun ensureMouseAtCenter() { + val displayCenter = PointF(320f, 240f) + val cursorLocation = virtualMouse.cursorPosition + if (!cursorLocation.nearEq(displayCenter)) { + sendMouseMove(displayCenter.x - cursorLocation.x, displayCenter.y - cursorLocation.y) + assertCursorLocation(320f, 240f) + } + } + + private fun sendMouseMove(dx: Float, dy: Float) { + virtualMouse.sendRelativeEvent( + VirtualMouseRelativeEvent.Builder().setRelativeX(dx).setRelativeY(dy).build() + ) + } + + /** + * Asserts that the cursor location is at the specified coordinates. The coordinates + * are in the non-scaled, display coordinates. + */ + private fun assertCursorLocation(x: Float, y: Float) { + PollingCheck.check("Wait for the cursor at ($x, $y)", CURSOR_TIMEOUT.inWholeMilliseconds) { + service.lastObservedCursorLocation?.let { it.x.nearEq(x) && it.y.nearEq(y) } ?: false + } + } + + private fun scaleTo(controller: AccessibilityService.MagnificationController, scale: Float) { + val config = + MagnificationConfig.Builder() + .setActivated(true) + .setMode(MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) + .setScale(scale) + .build() + val setResult = BooleanArray(1) + service.runOnServiceSync { setResult[0] = controller.setMagnificationConfig(config, false) } + assertThat(setResult[0]).isTrue() + } + + private fun assertMagnification( + controller: AccessibilityService.MagnificationController, + scale: Float = Float.NaN, centerX: Float = Float.NaN, centerY: Float = Float.NaN + ) { + PollingCheck.check( + "Wait for the magnification to scale=$scale, centerX=$centerX, centerY=$centerY", + MAGNIFICATION_TIMEOUT.inWholeMilliseconds + ) check@{ + val actual = controller.getMagnificationConfig() ?: return@check false + actual.isActivated && + (actual.mode == MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) && + (scale.isNaN() || scale.nearEq(actual.scale)) && + (centerX.isNaN() || centerX.nearEq(actual.centerX)) && + (centerY.isNaN() || centerY.nearEq(actual.centerY)) + } + } + + /** + * Sets up a virtual display and a virtual mouse for the test. The virtual mouse is associated + * with the virtual display. + */ + private fun prepareVirtualDevices() { + val deviceLatch = CountDownLatch(1) + val im = instrumentation.context.getSystemService(InputManager::class.java) + val inputDeviceListener = + object : InputManager.InputDeviceListener { + override fun onInputDeviceAdded(deviceId: Int) { + onInputDeviceChanged(deviceId) + } + + override fun onInputDeviceRemoved(deviceId: Int) {} + + override fun onInputDeviceChanged(deviceId: Int) { + val device = im.getInputDevice(deviceId) ?: return + if (device.vendorId == VIRTUAL_MOUSE_VENDOR_ID && + device.productId == VIRTUAL_MOUSE_PRODUCT_ID + ) { + deviceLatch.countDown() + } + } + } + im.registerInputDeviceListener(inputDeviceListener, Handler(Looper.getMainLooper())) + + virtualDevice = virtualDeviceRule.createManagedVirtualDevice() + virtualDisplay = + virtualDeviceRule.createManagedVirtualDisplay( + virtualDevice, + VirtualDeviceRule + .createDefaultVirtualDisplayConfigBuilder( + DISPLAY_WIDTH.toInt(), + DISPLAY_HEIGHT.toInt() + ) + .setFlags( + DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC + or DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED + or DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + ) + )!! + virtualMouse = + virtualDevice.createVirtualMouse( + VirtualMouseConfig.Builder() + .setVendorId(VIRTUAL_MOUSE_VENDOR_ID) + .setProductId(VIRTUAL_MOUSE_PRODUCT_ID) + .setAssociatedDisplayId(virtualDisplay.display.displayId) + .setInputDeviceName("VirtualMouse") + .build() + ) + + deviceLatch.await(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS) + im.unregisterInputDeviceListener(inputDeviceListener) + } + + /** + * Launches a test (empty) activity and makes it fullscreen on the specified display. This + * ensures that system bars are hidden and the full screen magnification enlarges the entire + * display. + */ + private fun launchTestActivityFullscreen(displayId: Int) { + val future = CompletableFuture<Void?>() + val fullscreenCallback = + object : OutcomeReceiver<Void, Throwable> { + override fun onResult(result: Void?) { + future.complete(null) + } + + override fun onError(error: Throwable) { + future.completeExceptionally(error) + } + } + + val activity = + virtualDeviceRule.startActivityOnDisplaySync<TestActivity>( + displayId, + TestActivity::class.java + ) + instrumentation.runOnMainSync { + activity.requestFullscreenMode( + Activity.FULLSCREEN_MODE_REQUEST_ENTER, + fullscreenCallback + ) + } + future.get(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS) + + uiAutomation.waitForIdle( + UI_IDLE_TIMEOUT.inWholeMilliseconds, UI_IDLE_GLOBAL_TIMEOUT.inWholeMilliseconds + ) + } + + class TestMagnificationAccessibilityService : InstrumentedAccessibilityService() { + private val lock = Any() + + var observingDisplayId = Display.INVALID_DISPLAY + set(v) { + synchronized(lock) { field = v } + } + + var lastObservedCursorLocation: PointF? = null + private set + get() { + synchronized(lock) { + return field + } + } + + override fun onServiceConnected() { + serviceInfo = + getServiceInfo()!!.apply { setMotionEventSources(InputDevice.SOURCE_MOUSE) } + + super.onServiceConnected() + } + + override fun onMotionEvent(event: MotionEvent) { + super.onMotionEvent(event) + + synchronized(lock) { + if (event.displayId == observingDisplayId) { + lastObservedCursorLocation = PointF(event.x, event.y) + } + } + } + } + + class TestActivity : Activity() + + companion object { + private const val VIRTUAL_MOUSE_VENDOR_ID = 123 + private const val VIRTUAL_MOUSE_PRODUCT_ID = 456 + + private val CURSOR_TIMEOUT = 1.seconds + private val MAGNIFICATION_TIMEOUT = 3.seconds + private val UI_IDLE_TIMEOUT = 500.milliseconds + private val UI_IDLE_GLOBAL_TIMEOUT = 5.seconds + + private const val DISPLAY_WIDTH = 640.0f + private const val DISPLAY_HEIGHT = 480.0f + private const val CENTER_X = DISPLAY_WIDTH / 2f + private const val CENTER_Y = DISPLAY_HEIGHT / 2f + } +} diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index c59f0a05c619..02b97442b218 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -29,9 +29,15 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.UserInfo +import android.content.pm.UserInfo.FLAG_FOR_TESTING +import android.content.pm.UserInfo.FLAG_FULL +import android.content.pm.UserInfo.FLAG_MAIN +import android.content.pm.UserInfo.FLAG_SYSTEM import android.os.Handler import android.os.PersistableBundle import android.os.UserHandle +import android.os.UserHandle.MIN_SECONDARY_USER_ID +import android.os.UserHandle.USER_SYSTEM import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -49,6 +55,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any import org.mockito.kotlin.whenever /** @@ -289,6 +296,36 @@ class SupervisionServiceTest { assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull() } + fun shouldAllowBypassingSupervisionRoleQualification_returnsTrue() { + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + addDefaultAndTestUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + } + + @Test + fun shouldAllowBypassingSupervisionRoleQualification_returnsFalse() { + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + addDefaultAndTestUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isTrue() + + // Enabling supervision on any user will disallow bypassing + service.setSupervisionEnabledForUser(USER_ID, true) + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse() + + // Adding non-default users should also disallow bypassing + addDefaultAndFullUsers() + assertThat(service.shouldAllowBypassingSupervisionRoleQualification()).isFalse() + + // Turning off supervision with non-default users should still disallow bypassing + service.setSupervisionEnabledForUser(USER_ID, false) + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + } + private val systemSupervisionPackage: String get() = context.getResources().getString(R.string.config_systemSupervision) @@ -310,10 +347,31 @@ class SupervisionServiceTest { context.sendBroadcastAsUser(intent, UserHandle.of(userId)) } + private fun addDefaultAndTestUsers() { + val userInfos = userData.map { (userId, flags) -> + UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE) + } + whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos) + } + + private fun addDefaultAndFullUsers() { + val userInfos = userData.map { (userId, flags) -> + UserInfo(userId, "user" + userId, USER_ICON, flags, USER_TYPE) + } + UserInfo(USER_ID, "user" + USER_ID, USER_ICON, FLAG_FULL, USER_TYPE) + whenever(mockUserManagerInternal.getUsers(any())).thenReturn(userInfos) + } + private companion object { const val USER_ID = 100 const val APP_UID = USER_ID * UserHandle.PER_USER_RANGE const val SUPERVISING_USER_ID = 10 + const val USER_ICON = "user_icon" + const val USER_TYPE = "fake_user_type" + val userData: Map<Int, Int> = mapOf( + USER_SYSTEM to FLAG_SYSTEM, + MIN_SECONDARY_USER_ID to FLAG_MAIN, + (MIN_SECONDARY_USER_ID + 1) to (FLAG_FULL or FLAG_FOR_TESTING) + ) } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index ed00a9e8e74b..b7c325878a78 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2983,9 +2983,45 @@ public class DisplayContentTests extends WindowTestsBase { assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); } + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testRemove_displayWithSystemDecorations_emitRemoveSystemDecorations() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED); + final DisplayContent dc = createNewDisplay(displayInfo); + spyOn(dc.mDisplay); + doReturn(true).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + final DisplayPolicy displayPolicy = dc.getDisplayPolicy(); + spyOn(displayPolicy); + + dc.remove(); + + verify(displayPolicy).notifyDisplayRemoveSystemDecorations(); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testRemove_displayWithoutSystemDecorations_dontEmitRemoveSystemDecorations() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.flags = (FLAG_ALLOWS_CONTENT_MODE_SWITCH | FLAG_TRUSTED); + final DisplayContent dc = createNewDisplay(displayInfo); + spyOn(dc.mDisplay); + doReturn(false).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + final DisplayPolicy displayPolicy = dc.getDisplayPolicy(); + spyOn(displayPolicy); + + dc.remove(); + + verify(displayPolicy, never()).notifyDisplayRemoveSystemDecorations(); + } + @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test - public void testForcedDensityRatioSetForExternalDisplays_persistDensityScaleFlagEnabled() { + public void testForcedDensityRatioSet_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); displayInfo.displayId = DEFAULT_DISPLAY + 1; displayInfo.type = Display.TYPE_EXTERNAL; @@ -3003,19 +3039,20 @@ public class DisplayContentTests extends WindowTestsBase { baseYDpi); final int forcedDensity = 640; - - // Verify that forcing the density is honored and the size doesn't change. - displayContent.setForcedDensity(forcedDensity, 0 /* userId */); - verifySizes(displayContent, baseWidth, baseHeight, forcedDensity); + displayContent.setForcedDensityRatio( + (float) forcedDensity / baseDensity, 0 /* userId */); // Verify that density ratio is set correctly. assertEquals((float) forcedDensity / baseDensity, - displayContent.mExternalDisplayForcedDensityRatio, 0.01); + displayContent.mForcedDisplayDensityRatio, 0.01); + // Verify that density is set correctly. + assertEquals(forcedDensity, + displayContent.mBaseDisplayDensity); } @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test - public void testForcedDensityUpdateForExternalDisplays_persistDensityScaleFlagEnabled() { + public void testForcedDensityUpdateWithRatio_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); displayInfo.displayId = DEFAULT_DISPLAY + 1; displayInfo.type = Display.TYPE_EXTERNAL; @@ -3033,14 +3070,12 @@ public class DisplayContentTests extends WindowTestsBase { baseYDpi); final int forcedDensity = 640; - - // Verify that forcing the density is honored and the size doesn't change. - displayContent.setForcedDensity(forcedDensity, 0 /* userId */); - verifySizes(displayContent, baseWidth, baseHeight, forcedDensity); + displayContent.setForcedDensityRatio( + (float) forcedDensity / baseDensity, 0 /* userId */); // Verify that density ratio is set correctly. - assertEquals((float) 2.0f, - displayContent.mExternalDisplayForcedDensityRatio, 0.001); + assertEquals(2.0f, + displayContent.mForcedDisplayDensityRatio, 0.001); displayContent.mInitialDisplayDensity = 160; diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index 449ca867b987..9ab20d15acc8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -280,18 +280,24 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testSetForcedDensityRatio() { - mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(), - 300 /* density */, 0 /* userId */); + DisplayInfo info = new DisplayInfo(mDisplayInfo); + info.logicalDensityDpi = 300; + info.type = Display.TYPE_EXTERNAL; + mSecondaryDisplay = createNewDisplay(info); mDisplayWindowSettings.setForcedDensityRatio(mSecondaryDisplay.getDisplayInfo(), 2.0f /* ratio */); mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay); - assertEquals(mSecondaryDisplay.mInitialDisplayDensity * 2.0f, - mSecondaryDisplay.mBaseDisplayDensity, 0.01); + assertEquals((int) (mSecondaryDisplay.mInitialDisplayDensity * 2.0f), + mSecondaryDisplay.mBaseDisplayDensity); + + mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), + 0 /* userId */); - mWm.clearForcedDisplayDensityForUser(mSecondaryDisplay.getDisplayId(), 0 /* userId */); assertEquals(mSecondaryDisplay.mInitialDisplayDensity, mSecondaryDisplay.mBaseDisplayDensity); + assertEquals(mSecondaryDisplay.mForcedDisplayDensityRatio, + 0.0f, 0.001); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 2c6884e7a35a..4458b7330a68 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -81,6 +81,7 @@ import android.window.TaskSnapshot; import androidx.test.filters.MediumTest; import com.android.server.wm.RecentTasks.Callbacks; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Rule; @@ -931,6 +932,20 @@ public class RecentTasksTest extends WindowTestsBase { } @Test + public void testVisibleTask_forceExcludedFromRecents() { + final Task forceExcludedFromRecentsTask = mTasks.getFirst(); + forceExcludedFromRecentsTask.setForceExcludedFromRecents(true); + + final boolean visible = mRecentTasks.isVisibleRecentTask(forceExcludedFromRecentsTask); + + if (Flags.excludeTaskFromRecents()) { + assertFalse(visible); + } else { + assertTrue(visible); + } + } + + @Test public void testFreezeTaskListOrder_reorderExistingTask() { // Add some tasks mRecentTasks.add(mTasks.get(0)); 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 b617f0285606..e57f1144e6e9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -86,6 +86,7 @@ import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.DisplayMetrics; import android.util.Xml; @@ -99,6 +100,7 @@ import androidx.test.filters.MediumTest; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.window.flags.Flags; import org.junit.Assert; import org.junit.Before; @@ -2161,6 +2163,36 @@ public class TaskTests extends WindowTestsBase { } + @Test + public void testIsForceExcludedFromRecents_defaultFalse() { + final Task task = createTask(mDisplayContent); + assertFalse(task.isForceExcludedFromRecents()); + } + + @Test + public void testSetForceExcludedFromRecents() { + final Task task = createTask(mDisplayContent); + + task.setForceExcludedFromRecents(true); + + if (Flags.excludeTaskFromRecents()) { + assertTrue(task.isForceExcludedFromRecents()); + } else { + assertFalse(task.isForceExcludedFromRecents()); + } + } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS) + public void testSetForceExcludedFromRecents_resetsTaskForceExcludedFromRecents() { + final Task task = createTask(mDisplayContent); + task.setForceExcludedFromRecents(true); + + task.setForceExcludedFromRecents(false); + + assertFalse(task.isForceExcludedFromRecents()); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 795273d47230..7836ca7d1b4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -98,6 +98,8 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; +import android.view.Display; +import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1594,6 +1596,60 @@ public class WindowManagerServiceTests extends WindowTestsBase { }); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void setForcedDisplayDensityRatio_forExternalDisplay_setsRatio() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_EXTERNAL; + displayInfo.logicalDensityDpi = 100; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + final float forcedDensityRatio = 2f; + + mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio, + currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio, + currentUserId); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void setForcedDisplayDensityRatio_forInternalDisplay_setsRatio() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_INTERNAL; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + final float forcedDensityRatio = 2f; + + mWm.setForcedDisplayDensityRatio(displayInfo.displayId, forcedDensityRatio, + currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(forcedDensityRatio, + currentUserId); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) + public void clearForcedDisplayDensityRatio_clearsRatioAndDensity() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + displayInfo.type = Display.TYPE_INTERNAL; + mDisplayContent = createNewDisplay(displayInfo); + final int currentUserId = ActivityManager.getCurrentUser(); + + mWm.clearForcedDisplayDensityForUser(displayInfo.displayId, currentUserId); + + verify(mDisplayContent).setForcedDensityRatio(0.0f, + currentUserId); + + assertEquals(mDisplayContent.mBaseDisplayDensity, + mDisplayContent.getInitialDisplayDensity()); + assertEquals(mDisplayContent.mForcedDisplayDensityRatio, 0.0f, 0.001); + } + /** * Simulates IPC transfer by writing the setting to a parcel and reading it back. * diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index c64578e4638f..cdc4256a5fd4 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -980,7 +980,7 @@ class KeyGestureControllerTests { TestData( "META -> Open Apps Drawer", intArrayOf(KeyEvent.KEYCODE_META_LEFT), - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, intArrayOf(KeyEvent.KEYCODE_META_LEFT), 0, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), diff --git a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt index cf09b54753b0..a0cf88809af4 100644 --- a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -26,7 +26,6 @@ import android.hardware.input.IKeyboardBacklightState import android.hardware.input.InputManager import android.hardware.lights.Light import android.os.SystemProperties -import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.util.TypedValue @@ -98,14 +97,12 @@ class KeyboardBacklightControllerTests { @get:Rule val inputManagerRule = MockInputManagerRule() @Mock private lateinit var native: NativeInputManagerService - @Mock private lateinit var uEventManager: UEventManager @Mock private lateinit var resources: Resources private lateinit var keyboardBacklightController: KeyboardBacklightController private lateinit var context: Context private lateinit var testLooper: TestLooper private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null - private var sysfsNodeChanges = 0 private var lastAnimationValues = IntArray(2) @Before @@ -126,7 +123,6 @@ class KeyboardBacklightControllerTests { lightColorMap.getOrDefault(args[1] as Int, 0) } lightColorMap.clear() - `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ } } private fun setupConfig() { @@ -158,13 +154,7 @@ class KeyboardBacklightControllerTests { private fun setupController() { keyboardBacklightController = - KeyboardBacklightController( - context, - native, - testLooper.looper, - FakeAnimatorFactory(), - uEventManager, - ) + KeyboardBacklightController(context, native, testLooper.looper, FakeAnimatorFactory()) } @Test @@ -318,77 +308,6 @@ class KeyboardBacklightControllerTests { } @Test - fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { - setupController() - var counter = sysfsNodeChanges - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::no_backlight\u0000" - ) - ) - assertEquals( - "Should not reload sysfs node if UEvent path doesn't contain kbd_backlight", - counter, - sysfsNodeChanges, - ) - - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=power\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - ) - ) - assertEquals( - "Should not reload sysfs node if UEvent doesn't belong to subsystem LED", - counter, - sysfsNodeChanges, - ) - - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=remove\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - ) - ) - assertEquals( - "Should not reload sysfs node if UEvent doesn't have ACTION(add)", - counter, - sysfsNodeChanges, - ) - - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/pqr/abc::kbd_backlight\u0000" - ) - ) - assertEquals( - "Should not reload sysfs node if UEvent path doesn't belong to leds/ directory", - counter, - sysfsNodeChanges, - ) - - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" - ) - ) - assertEquals( - "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", - ++counter, - sysfsNodeChanges, - ) - - keyboardBacklightController.onKeyboardBacklightUEvent( - UEventObserver.UEvent( - "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc:kbd_backlight:red\u0000" - ) - ) - assertEquals( - "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", - ++counter, - sysfsNodeChanges, - ) - } - - @Test @UiThreadTest fun testKeyboardBacklightAnimation_onChangeLevels() { ExtendedMockito.doReturn("true").`when` { diff --git a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index 34fef25b187c..d30ec1699d03 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -552,7 +552,7 @@ public class ProcessedPerfettoProtoLogImplTest { } final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - assertThrows(java.net.ConnectException.class, reader::readProtoLogTrace); + assertThrows(java.net.SocketException.class, reader::readProtoLogTrace); } @Test |