diff options
6 files changed, 519 insertions, 563 deletions
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 5ffd883a7ceb..5d662b20ebb9 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 @@ -852,18 +852,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void createHandleMenu(SplitScreenController splitScreenController) { loadAppInfoIfNeeded(); - mHandleMenu = new HandleMenu.Builder(this) - .setAppIcon(mAppIconBitmap) - .setAppName(mAppName) - .setOnClickListener(mOnCaptionButtonClickListener) - .setOnTouchListener(mOnCaptionTouchListener) - .setLayoutId(mRelayoutParams.mLayoutResId) - .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) - .setCaptionHeight(mResult.mCaptionHeight) - .setDisplayController(mDisplayController) - .setSplitScreenController(splitScreenController) - .setBrowserLinkAvailable(browserLinkAvailable()) - .build(); + mHandleMenu = new HandleMenu( + this, + mRelayoutParams.mLayoutResId, + mOnCaptionButtonClickListener, + mOnCaptionTouchListener, + mAppIconBitmap, + mAppName, + mDisplayController, + splitScreenController, + DesktopModeStatus.canEnterDesktopMode(mContext), + browserLinkAvailable(), + mResult.mCaptionHeight + ); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java deleted file mode 100644 index bf311499f8d2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ /dev/null @@ -1,536 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.windowdecor; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.view.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_UP; - -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.View; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; -import android.window.SurfaceSyncGroup; - -import androidx.annotation.VisibleForTesting; - -import com.android.window.flags.Flags; -import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; - -/** - * Handle menu opened when the appropriate button is clicked on. - * - * Displays up to 3 pills that show the following: - * App Info: App name, app icon, and collapse button to close the menu. - * Windowing Options(Proto 2 only): Buttons to change windowing modes. - * Additional Options: Miscellaneous functions including screenshot and closing task. - */ -class HandleMenu { - private static final String TAG = "HandleMenu"; - private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false; - private final Context mContext; - private final DesktopModeWindowDecoration mParentDecor; - @VisibleForTesting - AdditionalViewContainer mHandleMenuViewContainer; - // Position of the handle menu used for laying out the handle view. - @VisibleForTesting - final PointF mHandleMenuPosition = new PointF(); - // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} - // may be in a different coordinate space than the input coordinates. Therefore, we still care - // about the menu's coordinates relative to the display as a whole, so we need to maintain - // those as well. - final Point mGlobalMenuPosition = new Point(); - private final boolean mShouldShowWindowingPill; - private final boolean mShouldShowBrowserPill; - private final Bitmap mAppIconBitmap; - private final CharSequence mAppName; - private final View.OnClickListener mOnClickListener; - private final View.OnTouchListener mOnTouchListener; - private final RunningTaskInfo mTaskInfo; - private final DisplayController mDisplayController; - private final SplitScreenController mSplitScreenController; - private final int mLayoutResId; - private int mMarginMenuTop; - private int mMarginMenuStart; - private int mMenuHeight; - private int mMenuWidth; - private final int mCaptionHeight; - private HandleMenuAnimator mHandleMenuAnimator; - - - HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId, - View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, - Bitmap appIcon, CharSequence appName, DisplayController displayController, - SplitScreenController splitScreenController, boolean shouldShowWindowingPill, - boolean shouldShowBrowserPill, int captionHeight) { - mParentDecor = parentDecor; - mContext = mParentDecor.mDecorWindowContext; - mTaskInfo = mParentDecor.mTaskInfo; - mDisplayController = displayController; - mSplitScreenController = splitScreenController; - mLayoutResId = layoutResId; - mOnClickListener = onClickListener; - mOnTouchListener = onTouchListener; - mAppIconBitmap = appIcon; - mAppName = appName; - mShouldShowWindowingPill = shouldShowWindowingPill; - mShouldShowBrowserPill = shouldShowBrowserPill; - mCaptionHeight = captionHeight; - loadHandleMenuDimensions(); - updateHandleMenuPillPositions(); - } - - void show() { - final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - - createHandleMenuViewContainer(t, ssg); - ssg.addTransaction(t); - ssg.markSyncReady(); - setupHandleMenu(); - animateHandleMenu(); - } - - private void createHandleMenuViewContainer(SurfaceControl.Transaction t, - SurfaceSyncGroup ssg) { - final int x = (int) mHandleMenuPosition.x; - final int y = (int) mHandleMenuPosition.y; - if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) { - mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext, - R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId, - x, y, mMenuWidth, mMenuHeight); - } else { - mHandleMenuViewContainer = mParentDecor.addWindow( - R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", - t, ssg, x, y, mMenuWidth, mMenuHeight); - } - final View handleMenuView = mHandleMenuViewContainer.getView(); - mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight); - } - - /** - * Animates the appearance of the handle menu and its three pills. - */ - private void animateHandleMenu() { - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCaptionHandleExpandToOpen(); - } else { - mHandleMenuAnimator.animateOpen(); - } - } - - /** - * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions - * pill. - */ - private void setupHandleMenu() { - final View handleMenu = mHandleMenuViewContainer.getView(); - handleMenu.setOnTouchListener(mOnTouchListener); - setupAppInfoPill(handleMenu); - if (mShouldShowWindowingPill) { - setupWindowingPill(handleMenu); - } - setupMoreActionsPill(handleMenu); - setupOpenInBrowserPill(handleMenu); - } - - /** - * Set up interactive elements of handle menu's app info pill. - */ - private void setupAppInfoPill(View handleMenu) { - final HandleMenuImageButton collapseBtn = - handleMenu.findViewById(R.id.collapse_menu_button); - final ImageView appIcon = handleMenu.findViewById(R.id.application_icon); - final TextView appName = handleMenu.findViewById(R.id.application_name); - collapseBtn.setOnClickListener(mOnClickListener); - collapseBtn.setTaskInfo(mTaskInfo); - appIcon.setImageBitmap(mAppIconBitmap); - appName.setText(mAppName); - } - - /** - * Set up interactive elements and color of handle menu's windowing pill. - */ - private void setupWindowingPill(View handleMenu) { - final ImageButton fullscreenBtn = handleMenu.findViewById( - R.id.fullscreen_button); - final ImageButton splitscreenBtn = handleMenu.findViewById( - R.id.split_screen_button); - final ImageButton floatingBtn = handleMenu.findViewById(R.id.floating_button); - // TODO: Remove once implemented. - floatingBtn.setVisibility(View.GONE); - - final ImageButton desktopBtn = handleMenu.findViewById(R.id.desktop_button); - fullscreenBtn.setOnClickListener(mOnClickListener); - splitscreenBtn.setOnClickListener(mOnClickListener); - floatingBtn.setOnClickListener(mOnClickListener); - desktopBtn.setOnClickListener(mOnClickListener); - // The button corresponding to the windowing mode that the task is currently in uses a - // different color than the others. - final ColorStateList[] iconColors = getWindowingIconColor(); - final ColorStateList inActiveColorStateList = iconColors[0]; - final ColorStateList activeColorStateList = iconColors[1]; - final int windowingMode = mTaskInfo.getWindowingMode(); - fullscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_FULLSCREEN - ? activeColorStateList : inActiveColorStateList); - splitscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_MULTI_WINDOW - ? activeColorStateList : inActiveColorStateList); - floatingBtn.setImageTintList(windowingMode == WINDOWING_MODE_PINNED - ? activeColorStateList : inActiveColorStateList); - desktopBtn.setImageTintList(windowingMode == WINDOWING_MODE_FREEFORM - ? activeColorStateList : inActiveColorStateList); - } - - /** - * Set up interactive elements & height of handle menu's more actions pill - */ - private void setupMoreActionsPill(View handleMenu) { - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE); - } - } - - private void setupOpenInBrowserPill(View handleMenu) { - if (!mShouldShowBrowserPill) { - handleMenu.findViewById(R.id.open_in_browser_pill).setVisibility(View.GONE); - return; - } - final Button browserButton = handleMenu.findViewById(R.id.open_in_browser_button); - browserButton.setOnClickListener(mOnClickListener); - } - - /** - * Returns array of windowing icon color based on current UI theme. First element of the - * array is for inactive icons and the second is for active icons. - */ - private ColorStateList[] getWindowingIconColor() { - final int mode = mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK; - final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); - final TypedArray typedArray = mContext.obtainStyledAttributes(new int[]{ - com.android.internal.R.attr.materialColorOnSurface, - com.android.internal.R.attr.materialColorPrimary}); - final int inActiveColor = typedArray.getColor(0, isNightMode ? Color.WHITE : Color.BLACK); - final int activeColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK); - typedArray.recycle(); - return new ColorStateList[]{ColorStateList.valueOf(inActiveColor), - ColorStateList.valueOf(activeColor)}; - } - - /** - * Updates handle menu's position variables to reflect its next position. - */ - private void updateHandleMenuPillPositions() { - int menuX; - final int menuY; - final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); - updateGlobalMenuPosition(taskBounds); - if (mLayoutResId == R.layout.desktop_mode_app_header) { - // Align the handle menu to the left side of the caption. - menuX = mMarginMenuStart; - menuY = mMarginMenuTop; - } else { - if (Flags.enableAdditionalWindowsAboveStatusBar()) { - // In a focused decor, we use global coordinates for handle menu. Therefore we - // need to account for other factors like split stage and menu/handle width to - // center the menu. - menuX = mGlobalMenuPosition.x; - menuY = mGlobalMenuPosition.y; - } else { - menuX = (taskBounds.width() / 2) - (mMenuWidth / 2); - menuY = mMarginMenuTop; - } - } - // Handle Menu position setup. - mHandleMenuPosition.set(menuX, menuY); - } - - private void updateGlobalMenuPosition(Rect taskBounds) { - if (mTaskInfo.isFreeform()) { - mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart, - taskBounds.top + mMarginMenuTop); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - mGlobalMenuPosition.set( - (taskBounds.width() / 2) - (mMenuWidth / 2), - mMarginMenuTop - ); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId); - final Rect leftOrTopStageBounds = new Rect(); - final Rect rightOrBottomStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftOrTopStageBounds, - rightOrBottomStageBounds); - // TODO(b/343561161): This needs to be calculated differently if the task is in - // top/bottom split. - if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - mGlobalMenuPosition.set(leftOrTopStageBounds.width() - + (rightOrBottomStageBounds.width() / 2) - - (mMenuWidth / 2), - mMarginMenuTop); - } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { - mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2) - - (mMenuWidth / 2), - mMarginMenuTop); - } - } - } - - /** - * Update pill layout, in case task changes have caused positioning to change. - */ - void relayout(SurfaceControl.Transaction t) { - if (mHandleMenuViewContainer != null) { - updateHandleMenuPillPositions(); - mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y); - } - } - - /** - * Check a passed MotionEvent if a click or hover has occurred on any button on this caption - * Note this should only be called when a regular onClick/onHover is not possible - * (i.e. the button was clicked through status bar layer) - * - * @param ev the MotionEvent to compare against. - */ - void checkMotionEvent(MotionEvent ev) { - // If the menu view is above status bar, we can let the views handle input directly. - if (isViewAboveStatusBar()) return; - final View handleMenu = mHandleMenuViewContainer.getView(); - final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); - final PointF inputPoint = translateInputToLocalSpace(ev); - final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y); - final int action = ev.getActionMasked(); - collapse.setHovered(inputInCollapseButton && action != ACTION_UP); - collapse.setPressed(inputInCollapseButton && action == ACTION_DOWN); - if (action == ACTION_UP && inputInCollapseButton) { - collapse.performClick(); - } - } - - private boolean isViewAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() - && !mTaskInfo.isFreeform(); - } - - // Translate the input point from display coordinates to the same space as the handle menu. - private PointF translateInputToLocalSpace(MotionEvent ev) { - return new PointF(ev.getX() - mHandleMenuPosition.x, - ev.getY() - mHandleMenuPosition.y); - } - - /** - * 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. - */ - boolean isValidMenuInput(PointF inputPoint) { - if (!viewsLaidOut()) return true; - if (!isViewAboveStatusBar()) { - return pointInView( - mHandleMenuViewContainer.getView(), - inputPoint.x - mHandleMenuPosition.x, - inputPoint.y - mHandleMenuPosition.y); - } else { - // Handle menu exists in a different coordinate space when added to WindowManager. - // Therefore we must compare the provided input coordinates to global menu coordinates. - // This includes factoring for split stage as input coordinates are relative to split - // stage position, not relative to the display as a whole. - PointF inputRelativeToMenu = new PointF( - inputPoint.x - mGlobalMenuPosition.x, - inputPoint.y - mGlobalMenuPosition.y - ); - if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) - == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - // TODO(b/343561161): This also needs to be calculated differently if - // the task is in top/bottom split. - Rect leftStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); - inputRelativeToMenu.x += leftStageBounds.width(); - } - return pointInView( - mHandleMenuViewContainer.getView(), - inputRelativeToMenu.x, - inputRelativeToMenu.y); - } - } - - private boolean pointInView(View v, float x, float y) { - return v != null && v.getLeft() <= x && v.getRight() >= x - && v.getTop() <= y && v.getBottom() >= y; - } - - /** - * Check if the views for handle menu can be seen. - */ - private boolean viewsLaidOut() { - return mHandleMenuViewContainer.getView().isLaidOut(); - } - - private void loadHandleMenuDimensions() { - final Resources resources = mContext.getResources(); - mMenuWidth = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_width); - mMenuHeight = getHandleMenuHeight(resources); - mMarginMenuTop = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_top); - mMarginMenuStart = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_start); - } - - /** - * Determines handle menu height based on if windowing pill should be shown. - */ - private int getHandleMenuHeight(Resources resources) { - int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height); - if (!mShouldShowWindowingPill) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_windowing_pill_height); - } - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_more_actions_pill_height); - } - if (!mShouldShowBrowserPill) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height); - } - return menuHeight; - } - - private int loadDimensionPixelSize(Resources resources, int resourceId) { - if (resourceId == Resources.ID_NULL) { - return 0; - } - return resources.getDimensionPixelSize(resourceId); - } - - void close() { - final Runnable after = () -> { - mHandleMenuViewContainer.releaseView(); - mHandleMenuViewContainer = null; - }; - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCollapseIntoHandleClose(after); - } else { - mHandleMenuAnimator.animateClose(after); - } - } - - static final class Builder { - private final DesktopModeWindowDecoration mParent; - private CharSequence mName; - private Bitmap mAppIcon; - private View.OnClickListener mOnClickListener; - private View.OnTouchListener mOnTouchListener; - private int mLayoutId; - private boolean mShowWindowingPill; - private int mCaptionHeight; - private DisplayController mDisplayController; - private SplitScreenController mSplitScreenController; - private boolean mShowBrowserPill; - - Builder(@NonNull DesktopModeWindowDecoration parent) { - mParent = parent; - } - - Builder setAppName(@Nullable CharSequence name) { - mName = name; - return this; - } - - Builder setAppIcon(@Nullable Bitmap appIcon) { - mAppIcon = appIcon; - return this; - } - - Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) { - mOnClickListener = onClickListener; - return this; - } - - Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) { - mOnTouchListener = onTouchListener; - return this; - } - - Builder setLayoutId(int layoutId) { - mLayoutId = layoutId; - return this; - } - - Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) { - mShowWindowingPill = windowingButtonsVisible; - return this; - } - - Builder setCaptionHeight(int captionHeight) { - mCaptionHeight = captionHeight; - return this; - } - - Builder setDisplayController(DisplayController displayController) { - mDisplayController = displayController; - return this; - } - - Builder setSplitScreenController(SplitScreenController splitScreenController) { - mSplitScreenController = splitScreenController; - return this; - } - - Builder setBrowserLinkAvailable(Boolean showBrowserPill) { - mShowBrowserPill = showBrowserPill; - return this; - } - - HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mOnClickListener, - mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, - mShowWindowingPill, mShowBrowserPill, mCaptionHeight); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt new file mode 100644 index 000000000000..39595cf77951 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.view.MotionEvent +import android.view.SurfaceControl +import android.view.View +import android.widget.Button +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import android.window.SurfaceSyncGroup +import androidx.annotation.VisibleForTesting +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.split.SplitScreenConstants +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.extension.isFullscreen + +/** + * Handle menu opened when the appropriate button is clicked on. + * + * Displays up to 3 pills that show the following: + * App Info: App name, app icon, and collapse button to close the menu. + * Windowing Options(Proto 2 only): Buttons to change windowing modes. + * Additional Options: Miscellaneous functions including screenshot and closing task. + */ +class HandleMenu( + private val parentDecor: DesktopModeWindowDecoration, + private val layoutResId: Int, + private val onClickListener: View.OnClickListener?, + private val onTouchListener: View.OnTouchListener?, + private val appIconBitmap: Bitmap?, + private val appName: CharSequence?, + private val displayController: DisplayController, + private val splitScreenController: SplitScreenController, + private val shouldShowWindowingPill: Boolean, + private val shouldShowBrowserPill: Boolean, + private val captionHeight: Int +) { + private val context: Context = parentDecor.mDecorWindowContext + private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo + + private val isViewAboveStatusBar: Boolean + get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform) + + private var marginMenuTop = 0 + private var marginMenuStart = 0 + private var menuHeight = 0 + private var menuWidth = 0 + private var handleMenuAnimator: HandleMenuAnimator? = null + + @VisibleForTesting + var handleMenuViewContainer: AdditionalViewContainer? = null + + // Position of the handle menu used for laying out the handle view. + @VisibleForTesting + val handleMenuPosition: PointF = PointF() + + // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} + // may be in a different coordinate space than the input coordinates. Therefore, we still care + // about the menu's coordinates relative to the display as a whole, so we need to maintain + // those as well. + private val globalMenuPosition: Point = Point() + + /** + * An a array of windowing icon color based on current UI theme. First element of the + * array is for inactive icons and the second is for active icons. + */ + private val windowingIconColor: Array<ColorStateList> + get() { + val mode = (context.resources.configuration.uiMode + and Configuration.UI_MODE_NIGHT_MASK) + val isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES) + val typedArray = context.obtainStyledAttributes( + intArrayOf( + com.android.internal.R.attr.materialColorOnSurface, + com.android.internal.R.attr.materialColorPrimary + ) + ) + val inActiveColor = + typedArray.getColor(0, if (isNightMode) Color.WHITE else Color.BLACK) + val activeColor = typedArray.getColor(1, if (isNightMode) Color.WHITE else Color.BLACK) + typedArray.recycle() + return arrayOf( + ColorStateList.valueOf(inActiveColor), + ColorStateList.valueOf(activeColor) + ) + } + + init { + loadHandleMenuDimensions() + updateHandleMenuPillPositions() + } + + fun show() { + val ssg = SurfaceSyncGroup(TAG) + val t = SurfaceControl.Transaction() + + createHandleMenuViewContainer(t, ssg) + ssg.addTransaction(t) + ssg.markSyncReady() + setupHandleMenu() + animateHandleMenu() + } + + private fun createHandleMenuViewContainer( + t: SurfaceControl.Transaction, + ssg: SurfaceSyncGroup + ) { + val x = handleMenuPosition.x.toInt() + val y = handleMenuPosition.y.toInt() + handleMenuViewContainer = + if (!taskInfo.isFreeform && Flags.enableAdditionalWindowsAboveStatusBar()) { + AdditionalSystemViewContainer( + context = context, + layoutId = R.layout.desktop_mode_window_decor_handle_menu, + taskId = taskInfo.taskId, + x = x, + y = y, + width = menuWidth, + height = menuHeight + ) + } else { + parentDecor.addWindow( + R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", + t, ssg, x, y, menuWidth, menuHeight + ) + } + handleMenuViewContainer?.view?.let { view -> + handleMenuAnimator = + HandleMenuAnimator(view, menuWidth, captionHeight.toFloat()) + } + } + + /** + * Animates the appearance of the handle menu and its three pills. + */ + private fun animateHandleMenu() { + when (taskInfo.windowingMode) { + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, + WINDOWING_MODE_MULTI_WINDOW -> { + handleMenuAnimator?.animateCaptionHandleExpandToOpen() + } + else -> { + handleMenuAnimator?.animateOpen() + } + } + } + + /** + * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions + * pill. + */ + private fun setupHandleMenu() { + val handleMenu = handleMenuViewContainer?.view ?: return + handleMenu.setOnTouchListener(onTouchListener) + setupAppInfoPill(handleMenu) + if (shouldShowWindowingPill) { + setupWindowingPill(handleMenu) + } + setupMoreActionsPill(handleMenu) + setupOpenInBrowserPill(handleMenu) + } + + /** + * Set up interactive elements of handle menu's app info pill. + */ + private fun setupAppInfoPill(handleMenu: View) { + val collapseBtn = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + val appIcon = handleMenu.findViewById<ImageView>(R.id.application_icon) + val appName = handleMenu.findViewById<TextView>(R.id.application_name) + collapseBtn.setOnClickListener(onClickListener) + collapseBtn.taskInfo = taskInfo + appIcon.setImageBitmap(appIconBitmap) + appName.text = this.appName + } + + /** + * Set up interactive elements and color of handle menu's windowing pill. + */ + private fun setupWindowingPill(handleMenu: View) { + val fullscreenBtn = handleMenu.findViewById<ImageButton>(R.id.fullscreen_button) + val splitscreenBtn = handleMenu.findViewById<ImageButton>(R.id.split_screen_button) + val floatingBtn = handleMenu.findViewById<ImageButton>(R.id.floating_button) + // TODO: Remove once implemented. + floatingBtn.visibility = View.GONE + + val desktopBtn = handleMenu.findViewById<ImageButton>(R.id.desktop_button) + fullscreenBtn.setOnClickListener(onClickListener) + splitscreenBtn.setOnClickListener(onClickListener) + floatingBtn.setOnClickListener(onClickListener) + desktopBtn.setOnClickListener(onClickListener) + // The button corresponding to the windowing mode that the task is currently in uses a + // different color than the others. + val iconColors = windowingIconColor + val inActiveColorStateList = iconColors[0] + val activeColorStateList = iconColors[1] + fullscreenBtn.imageTintList = if (taskInfo.isFullscreen) { + activeColorStateList + } else { + inActiveColorStateList + } + splitscreenBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + activeColorStateList + } else { + inActiveColorStateList + } + floatingBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_PINNED) { + activeColorStateList + } else { + inActiveColorStateList + } + desktopBtn.imageTintList = if (taskInfo.isFreeform) { + activeColorStateList + } else { + inActiveColorStateList + } + } + + /** + * Set up interactive elements & height of handle menu's more actions pill + */ + private fun setupMoreActionsPill(handleMenu: View) { + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + handleMenu.findViewById<View>(R.id.more_actions_pill).visibility = View.GONE + } + } + + private fun setupOpenInBrowserPill(handleMenu: View) { + if (!shouldShowBrowserPill) { + handleMenu.findViewById<View>(R.id.open_in_browser_pill).visibility = View.GONE + return + } + val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button) + browserButton.setOnClickListener(onClickListener) + } + + /** + * Updates handle menu's position variables to reflect its next position. + */ + private fun updateHandleMenuPillPositions() { + val menuX: Int + val menuY: Int + val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds + updateGlobalMenuPosition(taskBounds) + if (layoutResId == R.layout.desktop_mode_app_header) { + // Align the handle menu to the left side of the caption. + menuX = marginMenuStart + menuY = marginMenuTop + } else { + if (Flags.enableAdditionalWindowsAboveStatusBar()) { + // In a focused decor, we use global coordinates for handle menu. Therefore we + // need to account for other factors like split stage and menu/handle width to + // center the menu. + menuX = globalMenuPosition.x + menuY = globalMenuPosition.y + } else { + menuX = (taskBounds.width() / 2) - (menuWidth / 2) + menuY = marginMenuTop + } + } + // Handle Menu position setup. + handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) + } + + private fun updateGlobalMenuPosition(taskBounds: Rect) { + when (taskInfo.windowingMode) { + WINDOWING_MODE_FREEFORM -> { + globalMenuPosition.set( + /* x = */ taskBounds.left + marginMenuStart, + /* y = */ taskBounds.top + marginMenuTop + ) + } + WINDOWING_MODE_FULLSCREEN -> { + globalMenuPosition.set( + /* x = */ taskBounds.width() / 2 - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + WINDOWING_MODE_MULTI_WINDOW -> { + val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + when (splitPosition) { + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { + globalMenuPosition.set( + /* x = */ leftOrTopStageBounds.width() + + (rightOrBottomStageBounds.width() / 2) + - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { + globalMenuPosition.set( + /* x = */ (leftOrTopStageBounds.width() / 2) + - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + } + } + } + } + + /** + * Update pill layout, in case task changes have caused positioning to change. + */ + fun relayout(t: SurfaceControl.Transaction) { + handleMenuViewContainer?.let { container -> + updateHandleMenuPillPositions() + container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y) + } + } + + /** + * Check a passed MotionEvent if a click or hover has occurred on any button on this caption + * Note this should only be called when a regular onClick/onHover is not possible + * (i.e. the button was clicked through status bar layer) + * + * @param ev the MotionEvent to compare against. + */ + fun checkMotionEvent(ev: MotionEvent) { + // If the menu view is above status bar, we can let the views handle input directly. + if (isViewAboveStatusBar) return + val handleMenu = handleMenuViewContainer?.view ?: return + val collapse = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + val inputPoint = translateInputToLocalSpace(ev) + val inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y) + val action = ev.actionMasked + collapse.isHovered = inputInCollapseButton && action != MotionEvent.ACTION_UP + collapse.isPressed = inputInCollapseButton && action == MotionEvent.ACTION_DOWN + if (action == MotionEvent.ACTION_UP && inputInCollapseButton) { + collapse.performClick() + } + } + + // Translate the input point from display coordinates to the same space as the handle menu. + private fun translateInputToLocalSpace(ev: MotionEvent): PointF { + return PointF( + ev.x - handleMenuPosition.x, + ev.y - handleMenuPosition.y + ) + } + + /** + * 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(inputPoint: PointF): Boolean { + if (!viewsLaidOut()) return true + if (!isViewAboveStatusBar) { + return pointInView( + handleMenuViewContainer?.view, + inputPoint.x - handleMenuPosition.x, + inputPoint.y - handleMenuPosition.y + ) + } else { + // Handle menu exists in a different coordinate space when added to WindowManager. + // Therefore we must compare the provided input coordinates to global menu coordinates. + // This includes factoring for split stage as input coordinates are relative to split + // stage position, not relative to the display as a whole. + val inputRelativeToMenu = PointF( + inputPoint.x - globalMenuPosition.x, + inputPoint.y - globalMenuPosition.y + ) + if (splitScreenController.getSplitPosition(taskInfo.taskId) + == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT) { + // TODO(b/343561161): This also needs to be calculated differently if + // the task is in top/bottom split. + val leftStageBounds = Rect() + splitScreenController.getStageBounds(leftStageBounds, Rect()) + inputRelativeToMenu.x += leftStageBounds.width().toFloat() + } + return pointInView( + handleMenuViewContainer?.view, + inputRelativeToMenu.x, + inputRelativeToMenu.y + ) + } + } + + private fun pointInView(v: View?, x: Float, y: Float): Boolean { + return v != null && v.left <= x && v.right >= x && v.top <= y && v.bottom >= y + } + + /** + * Check if the views for handle menu can be seen. + */ + private fun viewsLaidOut(): Boolean = handleMenuViewContainer?.view?.isLaidOut ?: false + + private fun loadHandleMenuDimensions() { + val resources = context.resources + menuWidth = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_width) + menuHeight = getHandleMenuHeight(resources) + marginMenuTop = loadDimensionPixelSize( + resources, + R.dimen.desktop_mode_handle_menu_margin_top + ) + marginMenuStart = loadDimensionPixelSize( + resources, + R.dimen.desktop_mode_handle_menu_margin_start + ) + } + + /** + * Determines handle menu height based on if windowing pill should be shown. + */ + private fun getHandleMenuHeight(resources: Resources): Int { + var menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height) + if (!shouldShowWindowingPill) { + menuHeight -= loadDimensionPixelSize( + resources, + R.dimen.desktop_mode_handle_menu_windowing_pill_height + ) + } + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + menuHeight -= loadDimensionPixelSize( + resources, + R.dimen.desktop_mode_handle_menu_more_actions_pill_height + ) + } + if (!shouldShowBrowserPill) { + menuHeight -= loadDimensionPixelSize(resources, + R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height) + } + return menuHeight + } + + private fun loadDimensionPixelSize(resources: Resources, resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) { + return 0 + } + return resources.getDimensionPixelSize(resourceId) + } + + fun close() { + val after = { + handleMenuViewContainer?.releaseView() + handleMenuViewContainer = null + } + if (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || + taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + handleMenuAnimator?.animateCollapseIntoHandleClose(after) + } else { + handleMenuAnimator?.animateClose(after) + } + } + + companion object { + private const val TAG = "HandleMenu" + private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 25a829b44448..e3d22342cc9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -108,7 +108,7 @@ class HandleMenuAnimator( * * @param after runs after the animation finishes. */ - fun animateCollapseIntoHandleClose(after: Runnable) { + fun animateCollapseIntoHandleClose(after: () -> Unit) { appInfoCollapseToHandle() animateAppInfoPillFadeOut() windowingPillClose() @@ -125,7 +125,7 @@ class HandleMenuAnimator( * @param after runs after animation finishes. * */ - fun animateClose(after: Runnable) { + fun animateClose(after: () -> Unit) { appInfoPillCollapse() animateAppInfoPillFadeOut() windowingPillClose() @@ -463,9 +463,9 @@ class HandleMenuAnimator( * * @param after runs after animation finishes. */ - private fun runAnimations(after: Runnable? = null) { + private fun runAnimations(after: (() -> Unit)? = null) { runningAnimation?.apply { - // Remove all listeners, so that after runnable isn't triggered upon cancel. + // Remove all listeners, so that the after function isn't triggered upon cancel. removeAllListeners() // If an animation runs while running animation is triggered, gracefully cancel. cancel() @@ -475,7 +475,7 @@ class HandleMenuAnimator( playTogether(animators) animators.clear() doOnEnd { - after?.run() + after?.invoke() runningAnimation = null } start() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 03dbbb3bbb82..923c4dde5df7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -105,7 +105,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * System-wide context. Only used to create context with overridden configurations. */ final Context mContext; - final DisplayController mDisplayController; + final @NonNull DisplayController mDisplayController; final ShellTaskOrganizer mTaskOrganizer; final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; @@ -158,7 +158,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> WindowDecoration( Context context, - DisplayController displayController, + @NonNull DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, @NonNull SurfaceControl taskSurface, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index cb73d1508281..e0e603ffb6f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -126,11 +126,11 @@ class HandleMenuTest : ShellTestCase() { fun testFullscreenMenuUsesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) val handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of display. val expected = Point(DISPLAY_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN) - assertEquals(expected.toPointF(), handleMenu.mHandleMenuPosition) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test @@ -138,10 +138,10 @@ class HandleMenuTest : ShellTestCase() { fun testFreeformMenu_usesViewHostViewContainer() { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) // Verify menu is created near top-left of task. val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN) - assertEquals(expected.toPointF(), handleMenu.mHandleMenuPosition) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test @@ -149,11 +149,11 @@ class HandleMenuTest : ShellTestCase() { fun testSplitLeftMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split left task. val expected = Point(SPLIT_LEFT_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN) - assertEquals(expected.toPointF(), handleMenu.mHandleMenuPosition) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test @@ -161,11 +161,11 @@ class HandleMenuTest : ShellTestCase() { fun testSplitRightMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split right task. val expected = Point(SPLIT_RIGHT_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN) - assertEquals(expected.toPointF(), handleMenu.mHandleMenuPosition) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } private fun createTaskInfo(windowingMode: Int, splitPosition: Int) { |