diff options
8 files changed, 429 insertions, 87 deletions
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 37596182f05b..5e41865cd31e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -17,20 +17,53 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" - android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_width="wrap_content" android:layout_height="@dimen/desktop_mode_maximize_menu_height" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout android:id="@+id/container" - android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_width="wrap_content" android:layout_height="@dimen/desktop_mode_maximize_menu_height" android:orientation="horizontal" android:padding="16dp" android:gravity="center"> <LinearLayout + android:id="@+id/maximize_menu_immersive_toggle_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <Button + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_immersive_toggle_button" + style="?android:attr/buttonBarButtonStyle" + android:stateListAnimator="@null" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" + android:alpha="0"/> + + <TextView + android:id="@+id/maximize_menu_immersive_toggle_button_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:gravity="center" + android:fontFamily="google-sans-text" + android:importantForAccessibility="no" + android:text="@string/desktop_mode_maximize_menu_immersive_button_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> + </LinearLayout> + + <LinearLayout + android:id="@+id/maximize_menu_size_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> @@ -43,7 +76,6 @@ android:stateListAnimator="@null" android:importantForAccessibility="yes" android:contentDescription="@string/desktop_mode_maximize_menu_maximize_button_text" - android:layout_marginRight="8dp" android:layout_marginBottom="4dp" android:alpha="0"/> @@ -62,6 +94,7 @@ </LinearLayout> <LinearLayout + android:id="@+id/maximize_menu_snap_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> @@ -73,6 +106,7 @@ android:padding="4dp" android:background="@drawable/desktop_mode_maximize_menu_layout_background" android:layout_marginBottom="4dp" + android:layout_marginStart="8dp" android:alpha="0"> <Button android:id="@+id/maximize_menu_snap_left_button" @@ -115,7 +149,7 @@ used to monitor input events over the entire menu. --> <View android:id="@+id/maximize_menu_overlay" - android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_width="match_parent" android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index d02c77e831aa..6cd380d1eed9 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -479,8 +479,10 @@ <!-- The default minimum allowed window height when resizing a window in desktop mode. --> <dimen name="desktop_mode_minimum_window_height">352dp</dimen> - <!-- The width of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_width">228dp</dimen> + <!-- The width of the maximize menu in desktop mode, depending on the number of options --> + <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen> + <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen> + <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen> <!-- The height of the maximize menu in desktop mode. --> <dimen name="desktop_mode_maximize_menu_height">114dp</dimen> @@ -502,10 +504,14 @@ <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen> + <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> + <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom">8dp</dimen> <!-- The vertical padding between the outline and fill of the maximize menu restore button. --> <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen> <!-- The horizontal padding between the outline and fill of the maximize menu restore button. --> <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen> + <!-- The padding between the outline and fill of the maximize menu immersive button. --> + <dimen name="desktop_mode_maximize_menu_immersive_button_fill_padding">4dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index afac9f6433a3..ef0386a1b4dd 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -317,7 +317,10 @@ <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> <!-- Snap resizing non-resizable string. --> <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> - <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> + <!-- Accessibility text for the Maximize Menu's immersive button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_immersive_button_text">Immersive</string> + <!-- Accessibility text for the Maximize Menu's immersive restore button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text">Restore</string> <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> <!-- Accessibility text for the Maximize Menu's restore button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_restore_button_text">Restore</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 59d52880f1aa..f404326dda83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -586,6 +586,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return; } mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo); + decoration.closeMaximizeMenu(); } private void onSnapResize(int taskId, boolean left, MotionEvent motionEvent) { @@ -1537,6 +1538,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, touchEventListener.mMotionEvent); return Unit.INSTANCE; }); + windowDecoration.setOnImmersiveOrRestoreClickListener(() -> { + onEnterOrExitImmersive(taskInfo.taskId); + return Unit.INSTANCE; + }); windowDecoration.setOnLeftSnapClickListener(() -> { onSnapResize(taskInfo.taskId, /* isLeft= */ true, touchEventListener.mMotionEvent); return Unit.INSTANCE; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index e35c004d8bba..88651121d73a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -143,6 +143,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private View.OnLongClickListener mOnCaptionLongClickListener; private View.OnGenericMotionListener mOnCaptionGenericMotionListener; private Function0<Unit> mOnMaximizeOrRestoreClickListener; + private Function0<Unit> mOnImmersiveOrRestoreClickListener; private Function0<Unit> mOnLeftSnapClickListener; private Function0<Unit> mOnRightSnapClickListener; private Consumer<DesktopModeTransitionSource> mOnToDesktopClickListener; @@ -293,6 +294,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnMaximizeOrRestoreClickListener = listener; } + /** + * Registers a listener to be called back when one of the tasks' immersive/restore action is + * triggered. + */ + void setOnImmersiveOrRestoreClickListener(Function0<Unit> listener) { + mOnImmersiveOrRestoreClickListener = listener; + } + /** Registers a listener to be called when the decoration's snap-left action is triggered.*/ void setOnLeftSnapClickListener(Function0<Unit> listener) { mOnLeftSnapClickListener = listener; @@ -718,7 +727,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isVisible()) { closeMaximizeMenu(); } else { - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT); + final int menuWidth = calculateMaximizeMenuWidth(); + mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT); } } @@ -939,8 +949,27 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Resources.ID_NULL; } - - private PointF calculateMaximizeMenuPosition() { + private int calculateMaximizeMenuWidth() { + final boolean showImmersive = Flags.enableFullyImmersiveInDesktop() + && TaskInfoKt.getRequestingImmersive(mTaskInfo); + final boolean showMaximize = true; + final boolean showSnaps = mTaskInfo.isResizeable; + int showCount = 0; + if (showImmersive) showCount++; + if (showMaximize) showCount++; + if (showSnaps) showCount++; + return switch (showCount) { + case 1 -> loadDimensionPixelSize(mContext.getResources(), + R.dimen.desktop_mode_maximize_menu_width_one_options); + case 2 -> loadDimensionPixelSize(mContext.getResources(), + R.dimen.desktop_mode_maximize_menu_width_two_options); + case 3 -> loadDimensionPixelSize(mContext.getResources(), + R.dimen.desktop_mode_maximize_menu_width_three_options); + default -> throw new IllegalArgumentException(""); + }; + } + + private PointF calculateMaximizeMenuPosition(int menuWidth) { final PointF position = new PointF(); final Resources resources = mContext.getResources(); final DisplayLayout displayLayout = @@ -956,8 +985,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int[] maximizeButtonLocation = new int[2]; maximizeWindowButton.getLocationInWindow(maximizeButtonLocation); - final int menuWidth = loadDimensionPixelSize( - resources, R.dimen.desktop_mode_maximize_menu_width); final int menuHeight = loadDimensionPixelSize( resources, R.dimen.desktop_mode_maximize_menu_height); @@ -1188,11 +1215,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { + final int menuWidth = calculateMaximizeMenuWidth(); mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, mDisplayController, mTaskInfo, mContext, - calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier); + calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier); mMaximizeMenu.show( + /* isTaskInImmersiveMode= */ Flags.enableFullyImmersiveInDesktop() + && mDesktopRepository.isTaskInFullImmersiveState(mTaskInfo.taskId), + /* menuWidth= */ menuWidth, + /* showImmersiveOption= */ Flags.enableFullyImmersiveInDesktop() + && TaskInfoKt.getRequestingImmersive(mTaskInfo), + /* showSnapOptions= */ mTaskInfo.isResizeable, mOnMaximizeOrRestoreClickListener, + mOnImmersiveOrRestoreClickListener, mOnLeftSnapClickListener, mOnRightSnapClickListener, hovered -> { @@ -1433,19 +1468,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } - /** - * Close an open maximize menu if input is outside of menu coordinates - * - * @param ev the tapped point to compare against - */ - void closeMaximizeMenuIfNeeded(MotionEvent ev) { - if (!isMaximizeMenuActive()) return; - - if (!mMaximizeMenu.isValidMenuInput(ev)) { - closeMaximizeMenu(); - } - } - boolean isFocused() { return mHasGlobalFocus; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 3ae5a1afc7e2..4bb1e7b6cc05 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -36,7 +36,6 @@ import android.graphics.drawable.StateListDrawable import android.graphics.drawable.shapes.RoundRectShape import android.util.StateSet import android.view.LayoutInflater -import android.view.MotionEvent import android.view.MotionEvent.ACTION_HOVER_ENTER import android.view.MotionEvent.ACTION_HOVER_EXIT import android.view.MotionEvent.ACTION_HOVER_MOVE @@ -58,6 +57,8 @@ import android.window.TaskConstants import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener +import androidx.core.view.isGone +import androidx.core.view.isVisible import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController @@ -93,7 +94,6 @@ class MaximizeMenu( private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() - private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width) private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) @@ -105,7 +105,12 @@ class MaximizeMenu( /** Creates and shows the maximize window. */ fun show( + isTaskInImmersiveMode: Boolean, + menuWidth: Int, + showImmersiveOption: Boolean, + showSnapOptions: Boolean, onMaximizeOrRestoreClickListener: () -> Unit, + onImmersiveOrRestoreClickListener: () -> Unit, onLeftSnapClickListener: () -> Unit, onRightSnapClickListener: () -> Unit, onHoverListener: (Boolean) -> Unit, @@ -113,7 +118,12 @@ class MaximizeMenu( ) { if (maximizeMenu != null) return createMaximizeMenu( + isTaskInImmersiveMode = isTaskInImmersiveMode, + menuWidth = menuWidth, + showImmersiveOption = showImmersiveOption, + showSnapOptions = showSnapOptions, onMaximizeClickListener = onMaximizeOrRestoreClickListener, + onImmersiveOrRestoreClickListener = onImmersiveOrRestoreClickListener, onLeftSnapClickListener = onLeftSnapClickListener, onRightSnapClickListener = onRightSnapClickListener, onHoverListener = onHoverListener, @@ -144,7 +154,12 @@ class MaximizeMenu( /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu( + isTaskInImmersiveMode: Boolean, + menuWidth: Int, + showImmersiveOption: Boolean, + showSnapOptions: Boolean, onMaximizeClickListener: () -> Unit, + onImmersiveOrRestoreClickListener: () -> Unit, onLeftSnapClickListener: () -> Unit, onRightSnapClickListener: () -> Unit, onHoverListener: (Boolean) -> Unit, @@ -179,11 +194,20 @@ class MaximizeMenu( maximizeMenuView = MaximizeMenuView( context = decorWindowContext, sizeToggleDirection = getSizeToggleDirection(), + immersiveConfig = if (showImmersiveOption) { + MaximizeMenuView.ImmersiveConfig.Visible( + getImmersiveToggleDirection(isTaskInImmersiveMode) + ) + } else { + MaximizeMenuView.ImmersiveConfig.Hidden + }, + showSnapOptions = showSnapOptions, menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> menuView.bind(taskInfo) menuView.onMaximizeClickListener = onMaximizeClickListener + menuView.onImmersiveOrRestoreClickListener = onImmersiveOrRestoreClickListener menuView.onLeftSnapClickListener = onLeftSnapClickListener menuView.onRightSnapClickListener = onRightSnapClickListener menuView.onMenuHoverListener = onHoverListener @@ -217,6 +241,15 @@ class MaximizeMenu( MaximizeMenuView.SizeToggleDirection.MAXIMIZE } + private fun getImmersiveToggleDirection( + isTaskImmersive: Boolean + ): MaximizeMenuView.ImmersiveToggleDirection = + if (isTaskImmersive) { + MaximizeMenuView.ImmersiveToggleDirection.EXIT + } else { + MaximizeMenuView.ImmersiveToggleDirection.ENTER + } + private fun loadDimensionPixelSize(resourceId: Int): Int { return if (resourceId == Resources.ID_NULL) { 0 @@ -226,33 +259,14 @@ class MaximizeMenu( } /** - * A valid menu input is one of the following: - * An input that happens in the menu views. - * Any input before the views have been laid out. - * - * @param inputPoint the input to compare against. - */ - fun isValidMenuInput(ev: MotionEvent): Boolean { - val x = ev.rawX - val y = ev.rawY - return !viewsLaidOut() || (menuPosition.x <= x && menuPosition.x + menuWidth >= x && - menuPosition.y <= y && menuPosition.y + menuHeight >= y) - } - - /** - * Check if the views for maximize menu can be seen. - */ - private fun viewsLaidOut(): Boolean { - return maximizeMenu?.view?.isLaidOut ?: false - } - - /** * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for * resizing a Task. */ class MaximizeMenuView( - private val context: Context, + context: Context, private val sizeToggleDirection: SizeToggleDirection, + immersiveConfig: ImmersiveConfig, + showSnapOptions: Boolean, private val menuHeight: Int, private val menuPadding: Int ) { @@ -260,10 +274,20 @@ class MaximizeMenu( .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup private val container = requireViewById(R.id.container) private val overlay = requireViewById(R.id.maximize_menu_overlay) + private val immersiveToggleContainer = + requireViewById(R.id.maximize_menu_immersive_toggle_container) as View + private val immersiveToggleButtonText = + requireViewById(R.id.maximize_menu_immersive_toggle_button_text) as TextView + private val immersiveToggleButton = + requireViewById(R.id.maximize_menu_immersive_toggle_button) as Button + private val sizeToggleContainer = + requireViewById(R.id.maximize_menu_size_toggle_container) as View private val sizeToggleButtonText = requireViewById(R.id.maximize_menu_size_toggle_button_text) as TextView private val sizeToggleButton = requireViewById(R.id.maximize_menu_size_toggle_button) as Button + private val snapContainer = + requireViewById(R.id.maximize_menu_snap_container) as View private val snapWindowText = requireViewById(R.id.maximize_menu_snap_window_text) as TextView private val snapRightButton = @@ -282,6 +306,35 @@ class MaximizeMenu( private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) + private val immersiveFillPadding = context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_immersive_button_fill_padding) + private val maximizeFillPaddingDefault = context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) + private val maximizeFillPaddingBottom = context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom) + private val maximizeRestoreFillPaddingVertical = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_maximize_menu_restore_button_fill_vertical_padding) + private val maximizeRestoreFillPaddingHorizontal = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_maximize_menu_restore_button_fill_horizontal_padding) + private val maximizeFillPaddingRect = Rect( + maximizeFillPaddingDefault, + maximizeFillPaddingDefault, + maximizeFillPaddingDefault, + maximizeFillPaddingBottom + ) + private val maximizeRestoreFillPaddingRect = Rect( + maximizeRestoreFillPaddingHorizontal, + maximizeRestoreFillPaddingVertical, + maximizeRestoreFillPaddingHorizontal, + maximizeRestoreFillPaddingVertical, + ) + private val immersiveFillPaddingRect = Rect( + immersiveFillPadding, + immersiveFillPadding, + immersiveFillPadding, + immersiveFillPadding + ) + private val hoverTempRect = Rect() private var menuAnimatorSet: AnimatorSet? = null private lateinit var taskInfo: RunningTaskInfo @@ -289,6 +342,8 @@ class MaximizeMenu( /** Invoked when the maximize or restore option is clicked. */ var onMaximizeClickListener: (() -> Unit)? = null + /** Invoked when the immersive or restore option is clicked. */ + var onImmersiveOrRestoreClickListener: (() -> Unit)? = null /** Invoked when the left snap option is clicked. */ var onLeftSnapClickListener: (() -> Unit)? = null /** Invoked when the right snap option is clicked. */ @@ -338,6 +393,11 @@ class MaximizeMenu( return@setOnHoverListener false } + immersiveToggleContainer.isGone = immersiveConfig is ImmersiveConfig.Hidden + sizeToggleContainer.isVisible = true + snapContainer.isGone = !showSnapOptions + + immersiveToggleButton.setOnClickListener { onImmersiveOrRestoreClickListener?.invoke() } sizeToggleButton.setOnClickListener { onMaximizeClickListener?.invoke() } snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } @@ -349,17 +409,36 @@ class MaximizeMenu( true } - val btnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) + // Maximize/restore button. + val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) R.string.desktop_mode_maximize_menu_restore_button_text else R.string.desktop_mode_maximize_menu_maximize_button_text - val btnText = context.resources.getText(btnTextId) - sizeToggleButton.contentDescription = btnText - sizeToggleButtonText.text = btnText + val sizeToggleBtnText = context.resources.getText(sizeToggleBtnTextId) + sizeToggleButton.contentDescription = sizeToggleBtnText + sizeToggleButtonText.text = sizeToggleBtnText + + // Immersive enter/exit button. + if (immersiveConfig is ImmersiveConfig.Visible) { + val immersiveToggleBtnTextId = when (immersiveConfig.direction) { + ImmersiveToggleDirection.ENTER -> { + R.string.desktop_mode_maximize_menu_immersive_button_text + } + + ImmersiveToggleDirection.EXIT -> { + R.string.desktop_mode_maximize_menu_immersive_restore_button_text + } + } + val immersiveToggleBtnText = context.resources.getText(immersiveToggleBtnTextId) + immersiveToggleButton.contentDescription = immersiveToggleBtnText + immersiveToggleButtonText.text = immersiveToggleBtnText + } // To prevent aliasing. sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } /** Bind the menu views to the new [RunningTaskInfo] data. */ @@ -373,6 +452,10 @@ class MaximizeMenu( sizeToggleButton.background = style.maximizeOption.drawable sizeToggleButtonText.setTextColor(style.textColor) + // Immersive option. + immersiveToggleButton.background = style.immersiveOption.drawable + immersiveToggleButtonText.setTextColor(style.textColor) + // Snap options. snapWindowText.setTextColor(style.textColor) updateSplitSnapSelection(SnapToHalfSelection.NONE) @@ -382,6 +465,8 @@ class MaximizeMenu( fun animateOpenMenu(onEnd: () -> Unit) { sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + immersiveToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) @@ -411,8 +496,10 @@ class MaximizeMenu( // scale is cancelled out and only the background is scaled. val value = animatedValue as Float sizeToggleButton.scaleY = value + immersiveToggleButton.scaleY = value snapButtonsLayout.scaleY = value sizeToggleButtonText.scaleY = value + immersiveToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -432,8 +519,10 @@ class MaximizeMenu( addUpdateListener { val value = animatedValue as Float sizeToggleButton.alpha = value + immersiveToggleButton.alpha = value snapButtonsLayout.alpha = value sizeToggleButtonText.alpha = value + immersiveToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -447,6 +536,8 @@ class MaximizeMenu( onEnd = { sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd.invoke() } ) @@ -457,6 +548,8 @@ class MaximizeMenu( fun animateCloseMenu(onEnd: (() -> Unit)) { sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + immersiveToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) cancelAnimation() menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( @@ -487,8 +580,10 @@ class MaximizeMenu( // scale is cancelled out and only the background is scaled. val value = animatedValue as Float sizeToggleButton.scaleY = value + immersiveToggleButton.scaleY = value snapButtonsLayout.scaleY = value sizeToggleButtonText.scaleY = value + immersiveToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -508,8 +603,10 @@ class MaximizeMenu( addUpdateListener { val value = animatedValue as Float sizeToggleButton.alpha = value + immersiveToggleButton.alpha = value snapButtonsLayout.alpha = value sizeToggleButtonText.alpha = value + immersiveToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -522,6 +619,8 @@ class MaximizeMenu( onEnd = { sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + immersiveToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd?.invoke() } ) @@ -531,6 +630,14 @@ class MaximizeMenu( /** Request that the accessibility service focus on the menu. */ fun requestAccessibilityFocus() { // Focus the first button in the menu by default. + if (immersiveToggleButton.isVisible) { + immersiveToggleButton.post { + immersiveToggleButton.sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED + ) + } + return + } sizeToggleButton.post { sizeToggleButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } @@ -557,7 +664,21 @@ class MaximizeMenu( backgroundColor = menuBackgroundColor, textColor = colorScheme.onSurface.toArgb(), maximizeOption = MenuStyle.MaximizeOption( - drawable = createMaximizeDrawable(menuBackgroundColor, colorScheme) + drawable = createMaximizeOrImmersiveDrawable( + menuBackgroundColor, + colorScheme, + fillPadding = when (sizeToggleDirection) { + SizeToggleDirection.MAXIMIZE -> maximizeFillPaddingRect + SizeToggleDirection.RESTORE -> maximizeRestoreFillPaddingRect + } + ) + ), + immersiveOption = MenuStyle.ImmersiveOption( + drawable = createMaximizeOrImmersiveDrawable( + menuBackgroundColor, + colorScheme, + fillPadding = immersiveFillPaddingRect, + ), ), snapOptions = MenuStyle.SnapOptions( inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(), @@ -624,19 +745,21 @@ class MaximizeMenu( } } - private fun createMaximizeDrawable( + private fun createMaximizeOrImmersiveDrawable( @ColorInt menuBackgroundColor: Int, - colorScheme: ColorScheme + colorScheme: ColorScheme, + fillPadding: Rect, ): StateListDrawable { val activeStrokeAndFill = colorScheme.primary.toArgb() val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12) - val activeDrawable = createMaximizeButtonDrawable( + val activeDrawable = createMaximizeOrImmersiveButtonDrawable( strokeAndFillColor = activeStrokeAndFill, backgroundColor = activeBackground, // Add a mask with the menu background's color because the active background color is // semi transparent, otherwise the transparency will reveal the stroke/fill color // behind it. - backgroundMask = menuBackgroundColor + backgroundMask = menuBackgroundColor, + fillPadding = fillPadding, ) return StateListDrawable().apply { addState(intArrayOf(android.R.attr.state_pressed), activeDrawable) @@ -646,19 +769,21 @@ class MaximizeMenu( // Inactive drawable. addState( StateSet.WILD_CARD, - createMaximizeButtonDrawable( + createMaximizeOrImmersiveButtonDrawable( strokeAndFillColor = colorScheme.outlineVariant.toArgb(), backgroundColor = colorScheme.surfaceContainerLow.toArgb(), - backgroundMask = null // not needed because the bg color is fully opaque + backgroundMask = null, // not needed because the bg color is fully opaque + fillPadding = fillPadding, ) ) } } - private fun createMaximizeButtonDrawable( + private fun createMaximizeOrImmersiveButtonDrawable( @ColorInt strokeAndFillColor: Int, @ColorInt backgroundColor: Int, - @ColorInt backgroundMask: Int? + @ColorInt backgroundMask: Int?, + fillPadding: Rect, ): LayerDrawable { val layers = mutableListOf<Drawable>() // First (bottom) layer, effectively the button's border ring once its inner shape is @@ -708,30 +833,17 @@ class MaximizeMenu( paint.style = Paint.Style.FILL }) - val (horizontalFillPadding, verticalFillPadding) = - if (sizeToggleDirection == SizeToggleDirection.MAXIMIZE) { - context.resources.getDimensionPixelSize(R.dimen - .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) to - context.resources.getDimensionPixelSize(R.dimen - .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) - } else { - context.resources.getDimensionPixelSize(R.dimen - .desktop_mode_maximize_menu_restore_button_fill_horizontal_padding) to - context.resources.getDimensionPixelSize(R.dimen - .desktop_mode_maximize_menu_restore_button_fill_vertical_padding) - } - return LayerDrawable(layers.toTypedArray()).apply { when (numberOfLayers) { 3 -> { setLayerInset(1, outlineStroke) - setLayerInset(2, horizontalFillPadding, verticalFillPadding, - horizontalFillPadding, verticalFillPadding) + setLayerInset(2, fillPadding.left, fillPadding.top, + fillPadding.right, fillPadding.bottom) } 4 -> { setLayerInset(intArrayOf(1, 2), outlineStroke) - setLayerInset(3, horizontalFillPadding, verticalFillPadding, - horizontalFillPadding, verticalFillPadding) + setLayerInset(3, fillPadding.left, fillPadding.top, + fillPadding.right, fillPadding.bottom) } else -> error("Unexpected number of layers: $numberOfLayers") } @@ -755,11 +867,15 @@ class MaximizeMenu( @ColorInt val backgroundColor: Int, @ColorInt val textColor: Int, val maximizeOption: MaximizeOption, + val immersiveOption: ImmersiveOption, val snapOptions: SnapOptions, ) { data class MaximizeOption( val drawable: StateListDrawable, ) + data class ImmersiveOption( + val drawable: StateListDrawable, + ) data class SnapOptions( @ColorInt val inactiveSnapSideColor: Int, @ColorInt val semiActiveSnapSideColor: Int, @@ -776,10 +892,23 @@ class MaximizeMenu( NONE, LEFT, RIGHT } + /** The possible immersive configs for this menu instance. */ + sealed class ImmersiveConfig { + data class Visible( + val direction: ImmersiveToggleDirection, + ) : ImmersiveConfig() + data object Hidden : ImmersiveConfig() + } + /** The possible selection states of the size toggle button in the maximize menu. */ enum class SizeToggleDirection { MAXIMIZE, RESTORE } + + /** The possible selection states of the immersive toggle button in the maximize menu. */ + enum class ImmersiveToggleDirection { + ENTER, EXIT + } } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 4c86529d8eed..c5526fc07280 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -127,6 +127,7 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times +import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.verify import org.mockito.kotlin.any import org.mockito.kotlin.argThat @@ -1278,12 +1279,45 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { .toggleDesktopTaskSize(decor.mTaskInfo, ResizeTrigger.MAXIMIZE_BUTTON, null) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun testImmersiveClick_togglesImmersiveState() { + val onImmersiveClickCaptor = argumentCaptor<() -> Unit>() + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onImmersiveOrRestoreListenerCaptor = onImmersiveClickCaptor, + requestingImmersive = true, + ) + + onImmersiveClickCaptor.firstValue() + + verify(mockDesktopTasksController) + .toggleDesktopTaskFullImmersiveState(decor.mTaskInfo) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun testImmersiveClick_closesMaximizeMenu() { + val onImmersiveClickCaptor = argumentCaptor<() -> Unit>() + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onImmersiveOrRestoreListenerCaptor = onImmersiveClickCaptor, + requestingImmersive = true, + ) + + onImmersiveClickCaptor.firstValue() + + verify(decor).closeMaximizeMenu() + } + private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), requestingImmersive: Boolean = false, onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onImmersiveOrRestoreListenerCaptor: KArgumentCaptor<() -> Unit> = + argumentCaptor<() -> Unit>(), onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, onRightSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = @@ -1307,6 +1341,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { )) onTaskOpening(decor.mTaskInfo, taskSurface) verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture()) + verify(decor) + .setOnImmersiveOrRestoreClickListener(onImmersiveOrRestoreListenerCaptor.capture()) verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture()) verify(decor).setOnRightSnapClickListener(onRightSnapClickListenerCaptor.capture()) verify(decor).setOnToDesktopClickListener(onToDesktopClickListenerCaptor.capture()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 0446ed7c1f41..8a2c7782906d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -905,8 +905,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new FakeMaximizeMenuFactory(menu)); assertFalse(decoration.isMaximizeMenuActive()); - createMaximizeMenu(decoration, menu); + createMaximizeMenu(decoration); + verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), + any(), mOnMaxMenuHoverChangeListener.capture(), any()); assertTrue(decoration.isMaximizeMenuActive()); } @@ -917,7 +919,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); decoration.setAppHeaderMaximizeButtonHovered(false); - createMaximizeMenu(decoration, menu); + createMaximizeMenu(decoration); + verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), + any(), mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(false); @@ -936,7 +940,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); decoration.setAppHeaderMaximizeButtonHovered(true); - createMaximizeMenu(decoration, menu); + createMaximizeMenu(decoration); decoration.setAppHeaderMaximizeButtonHovered(false); @@ -954,7 +958,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final MaximizeMenu menu = mock(MaximizeMenu.class); final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); - createMaximizeMenu(decoration, menu); + createMaximizeMenu(decoration); + verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), + any(), mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(true); @@ -967,13 +973,114 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final MaximizeMenu menu = mock(MaximizeMenu.class); final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); - createMaximizeMenu(decoration, menu); + createMaximizeMenu(decoration); + verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), + any(), mOnMaxMenuHoverChangeListener.capture(), any()); decoration.setAppHeaderMaximizeButtonHovered(true); verify(mMockHandler).removeCallbacks(any()); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void createMaximizeMenu_taskRequestsImmersive_showsImmersiveOption() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + taskInfo.requestedVisibleTypes = ~statusBars(); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + + createMaximizeMenu(decoration); + + verify(menu).show( + anyBoolean(), + anyInt(), + /* showImmersiveOption= */ eq(true), + anyBoolean(), + any(), + any(), + any(), + any(), + any(), + any() + ); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void createMaximizeMenu_taskDoesNotRequestImmersive_hiddenImmersiveOption() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + taskInfo.requestedVisibleTypes = statusBars(); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + + createMaximizeMenu(decoration); + + verify(menu).show( + anyBoolean(), + anyInt(), + /* showImmersiveOption= */ eq(false), + anyBoolean(), + any(), + any(), + any(), + any(), + any(), + any() + ); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void createMaximizeMenu_taskResizable_showsSnapOptions() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + taskInfo.isResizeable = true; + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + + createMaximizeMenu(decoration); + + verify(menu).show( + anyBoolean(), + anyInt(), + anyBoolean(), + /* showSnapOptions= */ eq(true), + any(), + any(), + any(), + any(), + any(), + any() + ); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void createMaximizeMenu_taskUnresizable_hiddenSnapOptions() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + taskInfo.isResizeable = false; + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + + createMaximizeMenu(decoration); + + verify(menu).show( + anyBoolean(), + anyInt(), + anyBoolean(), + /* showSnapOptions= */ eq(false), + any(), + any(), + any(), + any(), + any(), + any() + ); + } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) @@ -1331,13 +1438,13 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { anyInt(), anyInt(), anyInt(), anyInt()); } - private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { + private void createMaximizeMenu(DesktopModeWindowDecoration decoration) { final Function0<Unit> l = () -> Unit.INSTANCE; decoration.setOnMaximizeOrRestoreClickListener(l); + decoration.setOnImmersiveOrRestoreClickListener(l); decoration.setOnLeftSnapClickListener(l); decoration.setOnRightSnapClickListener(l); decoration.createMaximizeMenu(); - verify(menu).show(any(), any(), any(), mOnMaxMenuHoverChangeListener.capture(), any()); } private void fillRoundedCornersResources(int fillValue) { |