1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.windowdecor; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 22 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.ActivityManager.RunningTaskInfo; 27 import android.content.Context; 28 import android.content.res.ColorStateList; 29 import android.content.res.Resources; 30 import android.graphics.PointF; 31 import android.graphics.drawable.Drawable; 32 import android.view.MotionEvent; 33 import android.view.SurfaceControl; 34 import android.view.View; 35 import android.widget.Button; 36 import android.widget.ImageButton; 37 import android.widget.ImageView; 38 import android.widget.TextView; 39 import android.window.SurfaceSyncGroup; 40 41 import com.android.wm.shell.R; 42 import com.android.wm.shell.desktopmode.DesktopModeStatus; 43 44 /** 45 * Handle menu opened when the appropriate button is clicked on. 46 * 47 * Displays up to 3 pills that show the following: 48 * App Info: App name, app icon, and collapse button to close the menu. 49 * Windowing Options(Proto 2 only): Buttons to change windowing modes. 50 * Additional Options: Miscellaneous functions including screenshot and closing task. 51 */ 52 class HandleMenu { 53 private static final String TAG = "HandleMenu"; 54 private final Context mContext; 55 private final WindowDecoration mParentDecor; 56 private WindowDecoration.AdditionalWindow mAppInfoPill; 57 private WindowDecoration.AdditionalWindow mWindowingPill; 58 private WindowDecoration.AdditionalWindow mMoreActionsPill; 59 private final PointF mAppInfoPillPosition = new PointF(); 60 private final PointF mWindowingPillPosition = new PointF(); 61 private final PointF mMoreActionsPillPosition = new PointF(); 62 private final boolean mShouldShowWindowingPill; 63 private final Drawable mAppIcon; 64 private final CharSequence mAppName; 65 private final View.OnClickListener mOnClickListener; 66 private final View.OnTouchListener mOnTouchListener; 67 private final RunningTaskInfo mTaskInfo; 68 private final int mLayoutResId; 69 private final int mCaptionX; 70 private final int mCaptionY; 71 private int mMarginMenuTop; 72 private int mMarginMenuStart; 73 private int mMarginMenuSpacing; 74 private int mMenuWidth; 75 private int mAppInfoPillHeight; 76 private int mWindowingPillHeight; 77 private int mMoreActionsPillHeight; 78 private int mShadowRadius; 79 private int mCornerRadius; 80 81 HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY, View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill)82 HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY, 83 View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, 84 Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill) { 85 mParentDecor = parentDecor; 86 mContext = mParentDecor.mDecorWindowContext; 87 mTaskInfo = mParentDecor.mTaskInfo; 88 mLayoutResId = layoutResId; 89 mCaptionX = captionX; 90 mCaptionY = captionY; 91 mOnClickListener = onClickListener; 92 mOnTouchListener = onTouchListener; 93 mAppIcon = appIcon; 94 mAppName = appName; 95 mShouldShowWindowingPill = shouldShowWindowingPill; 96 loadHandleMenuDimensions(); 97 updateHandleMenuPillPositions(); 98 } 99 show()100 void show() { 101 final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); 102 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 103 104 createAppInfoPill(t, ssg); 105 if (mShouldShowWindowingPill) { 106 createWindowingPill(t, ssg); 107 } 108 createMoreActionsPill(t, ssg); 109 ssg.addTransaction(t); 110 ssg.markSyncReady(); 111 setupHandleMenu(); 112 } 113 createAppInfoPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)114 private void createAppInfoPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { 115 final int x = (int) mAppInfoPillPosition.x; 116 final int y = (int) mAppInfoPillPosition.y; 117 mAppInfoPill = mParentDecor.addWindow( 118 R.layout.desktop_mode_window_decor_handle_menu_app_info_pill, 119 "Menu's app info pill", 120 t, ssg, x, y, mMenuWidth, mAppInfoPillHeight, mShadowRadius, mCornerRadius); 121 } 122 createWindowingPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)123 private void createWindowingPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { 124 final int x = (int) mWindowingPillPosition.x; 125 final int y = (int) mWindowingPillPosition.y; 126 mWindowingPill = mParentDecor.addWindow( 127 R.layout.desktop_mode_window_decor_handle_menu_windowing_pill, 128 "Menu's windowing pill", 129 t, ssg, x, y, mMenuWidth, mWindowingPillHeight, mShadowRadius, mCornerRadius); 130 } 131 createMoreActionsPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg)132 private void createMoreActionsPill(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) { 133 final int x = (int) mMoreActionsPillPosition.x; 134 final int y = (int) mMoreActionsPillPosition.y; 135 mMoreActionsPill = mParentDecor.addWindow( 136 R.layout.desktop_mode_window_decor_handle_menu_more_actions_pill, 137 "Menu's more actions pill", 138 t, ssg, x, y, mMenuWidth, mMoreActionsPillHeight, mShadowRadius, mCornerRadius); 139 } 140 141 /** 142 * Set up interactive elements and color of this handle menu 143 */ setupHandleMenu()144 private void setupHandleMenu() { 145 // App Info pill setup. 146 final View appInfoPillView = mAppInfoPill.mWindowViewHost.getView(); 147 final ImageButton collapseBtn = appInfoPillView.findViewById(R.id.collapse_menu_button); 148 final ImageView appIcon = appInfoPillView.findViewById(R.id.application_icon); 149 final TextView appName = appInfoPillView.findViewById(R.id.application_name); 150 collapseBtn.setOnClickListener(mOnClickListener); 151 appInfoPillView.setOnTouchListener(mOnTouchListener); 152 appIcon.setImageDrawable(mAppIcon); 153 appName.setText(mAppName); 154 155 // Windowing pill setup. 156 if (mShouldShowWindowingPill) { 157 final View windowingPillView = mWindowingPill.mWindowViewHost.getView(); 158 final ImageButton fullscreenBtn = windowingPillView.findViewById( 159 R.id.fullscreen_button); 160 final ImageButton splitscreenBtn = windowingPillView.findViewById( 161 R.id.split_screen_button); 162 final ImageButton floatingBtn = windowingPillView.findViewById(R.id.floating_button); 163 final ImageButton desktopBtn = windowingPillView.findViewById(R.id.desktop_button); 164 fullscreenBtn.setOnClickListener(mOnClickListener); 165 splitscreenBtn.setOnClickListener(mOnClickListener); 166 floatingBtn.setOnClickListener(mOnClickListener); 167 desktopBtn.setOnClickListener(mOnClickListener); 168 // The button corresponding to the windowing mode that the task is currently in uses a 169 // different color than the others. 170 final ColorStateList activeColorStateList = ColorStateList.valueOf( 171 mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_active)); 172 final ColorStateList inActiveColorStateList = ColorStateList.valueOf( 173 mContext.getColor(R.color.desktop_mode_caption_menu_buttons_color_inactive)); 174 fullscreenBtn.setImageTintList( 175 mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN 176 ? activeColorStateList : inActiveColorStateList); 177 splitscreenBtn.setImageTintList( 178 mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW 179 ? activeColorStateList : inActiveColorStateList); 180 floatingBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_PINNED 181 ? activeColorStateList : inActiveColorStateList); 182 desktopBtn.setImageTintList(mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM 183 ? activeColorStateList : inActiveColorStateList); 184 } 185 186 // More Actions pill setup. 187 final View moreActionsPillView = mMoreActionsPill.mWindowViewHost.getView(); 188 final Button closeBtn = moreActionsPillView.findViewById(R.id.close_button); 189 closeBtn.setOnClickListener(mOnClickListener); 190 final Button selectBtn = moreActionsPillView.findViewById(R.id.select_button); 191 selectBtn.setOnClickListener(mOnClickListener); 192 } 193 194 /** 195 * Updates the handle menu pills' position variables to reflect their next positions 196 */ updateHandleMenuPillPositions()197 private void updateHandleMenuPillPositions() { 198 final int menuX, menuY; 199 final int captionWidth = mTaskInfo.getConfiguration() 200 .windowConfiguration.getBounds().width(); 201 if (mLayoutResId 202 == R.layout.desktop_mode_app_controls_window_decor) { 203 // Align the handle menu to the left of the caption. 204 menuX = mCaptionX + mMarginMenuStart; 205 menuY = mCaptionY + mMarginMenuTop; 206 } else { 207 // Position the handle menu at the center of the caption. 208 menuX = mCaptionX + (captionWidth / 2) - (mMenuWidth / 2); 209 menuY = mCaptionY + mMarginMenuStart; 210 } 211 212 // App Info pill setup. 213 final int appInfoPillY = menuY; 214 mAppInfoPillPosition.set(menuX, appInfoPillY); 215 216 final int windowingPillY, moreActionsPillY; 217 if (mShouldShowWindowingPill) { 218 windowingPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing; 219 mWindowingPillPosition.set(menuX, windowingPillY); 220 moreActionsPillY = windowingPillY + mWindowingPillHeight + mMarginMenuSpacing; 221 mMoreActionsPillPosition.set(menuX, moreActionsPillY); 222 } else { 223 // Just start after the end of the app info pill + margins. 224 moreActionsPillY = appInfoPillY + mAppInfoPillHeight + mMarginMenuSpacing; 225 mMoreActionsPillPosition.set(menuX, moreActionsPillY); 226 } 227 } 228 229 /** 230 * Update pill layout, in case task changes have caused positioning to change. 231 * @param t 232 */ relayout(SurfaceControl.Transaction t)233 void relayout(SurfaceControl.Transaction t) { 234 if (mAppInfoPill != null) { 235 updateHandleMenuPillPositions(); 236 t.setPosition(mAppInfoPill.mWindowSurface, 237 mAppInfoPillPosition.x, mAppInfoPillPosition.y); 238 // Only show windowing buttons in proto2. Proto1 uses a system-level mode only. 239 final boolean shouldShowWindowingPill = DesktopModeStatus.isProto2Enabled(); 240 if (shouldShowWindowingPill) { 241 t.setPosition(mWindowingPill.mWindowSurface, 242 mWindowingPillPosition.x, mWindowingPillPosition.y); 243 } 244 t.setPosition(mMoreActionsPill.mWindowSurface, 245 mMoreActionsPillPosition.x, mMoreActionsPillPosition.y); 246 } 247 } 248 /** 249 * Check a passed MotionEvent if a click has occurred on any button on this caption 250 * Note this should only be called when a regular onClick is not possible 251 * (i.e. the button was clicked through status bar layer) 252 * @param ev the MotionEvent to compare against. 253 */ checkClickEvent(MotionEvent ev)254 void checkClickEvent(MotionEvent ev) { 255 final View appInfoPill = mAppInfoPill.mWindowViewHost.getView(); 256 final ImageButton collapse = appInfoPill.findViewById(R.id.collapse_menu_button); 257 // Translate the input point from display coordinates to the same space as the collapse 258 // button, meaning its parent (app info pill view). 259 final PointF inputPoint = new PointF(ev.getX() - mAppInfoPillPosition.x, 260 ev.getY() - mAppInfoPillPosition.y); 261 if (pointInView(collapse, inputPoint.x, inputPoint.y)) { 262 mOnClickListener.onClick(collapse); 263 } 264 } 265 266 /** 267 * A valid menu input is one of the following: 268 * An input that happens in the menu views. 269 * Any input before the views have been laid out. 270 * @param inputPoint the input to compare against. 271 */ isValidMenuInput(PointF inputPoint)272 boolean isValidMenuInput(PointF inputPoint) { 273 if (!viewsLaidOut()) return true; 274 final boolean pointInAppInfoPill = pointInView( 275 mAppInfoPill.mWindowViewHost.getView(), 276 inputPoint.x - mAppInfoPillPosition.x, 277 inputPoint.y - mAppInfoPillPosition.y); 278 boolean pointInWindowingPill = false; 279 if (mWindowingPill != null) { 280 pointInWindowingPill = pointInView( 281 mWindowingPill.mWindowViewHost.getView(), 282 inputPoint.x - mWindowingPillPosition.x, 283 inputPoint.y - mWindowingPillPosition.y); 284 } 285 final boolean pointInMoreActionsPill = pointInView( 286 mMoreActionsPill.mWindowViewHost.getView(), 287 inputPoint.x - mMoreActionsPillPosition.x, 288 inputPoint.y - mMoreActionsPillPosition.y); 289 290 return pointInAppInfoPill || pointInWindowingPill || pointInMoreActionsPill; 291 } 292 pointInView(View v, float x, float y)293 private boolean pointInView(View v, float x, float y) { 294 return v != null && v.getLeft() <= x && v.getRight() >= x 295 && v.getTop() <= y && v.getBottom() >= y; 296 } 297 298 /** 299 * Check if the views for handle menu can be seen. 300 * @return 301 */ viewsLaidOut()302 private boolean viewsLaidOut() { 303 return mAppInfoPill.mWindowViewHost.getView().isLaidOut(); 304 } 305 306 loadHandleMenuDimensions()307 private void loadHandleMenuDimensions() { 308 final Resources resources = mContext.getResources(); 309 mMenuWidth = loadDimensionPixelSize(resources, 310 R.dimen.desktop_mode_handle_menu_width); 311 mMarginMenuTop = loadDimensionPixelSize(resources, 312 R.dimen.desktop_mode_handle_menu_margin_top); 313 mMarginMenuStart = loadDimensionPixelSize(resources, 314 R.dimen.desktop_mode_handle_menu_margin_start); 315 mMarginMenuSpacing = loadDimensionPixelSize(resources, 316 R.dimen.desktop_mode_handle_menu_pill_spacing_margin); 317 mAppInfoPillHeight = loadDimensionPixelSize(resources, 318 R.dimen.desktop_mode_handle_menu_app_info_pill_height); 319 mWindowingPillHeight = loadDimensionPixelSize(resources, 320 R.dimen.desktop_mode_handle_menu_windowing_pill_height); 321 mMoreActionsPillHeight = loadDimensionPixelSize(resources, 322 R.dimen.desktop_mode_handle_menu_more_actions_pill_height); 323 mShadowRadius = loadDimensionPixelSize(resources, 324 R.dimen.desktop_mode_handle_menu_shadow_radius); 325 mCornerRadius = loadDimensionPixelSize(resources, 326 R.dimen.desktop_mode_handle_menu_corner_radius); 327 } 328 loadDimensionPixelSize(Resources resources, int resourceId)329 private int loadDimensionPixelSize(Resources resources, int resourceId) { 330 if (resourceId == Resources.ID_NULL) { 331 return 0; 332 } 333 return resources.getDimensionPixelSize(resourceId); 334 } 335 close()336 void close() { 337 mAppInfoPill.releaseView(); 338 mAppInfoPill = null; 339 if (mWindowingPill != null) { 340 mWindowingPill.releaseView(); 341 mWindowingPill = null; 342 } 343 mMoreActionsPill.releaseView(); 344 mMoreActionsPill = null; 345 } 346 347 static final class Builder { 348 private final WindowDecoration mParent; 349 private CharSequence mName; 350 private Drawable mAppIcon; 351 private View.OnClickListener mOnClickListener; 352 private View.OnTouchListener mOnTouchListener; 353 private int mLayoutId; 354 private int mCaptionX; 355 private int mCaptionY; 356 private boolean mShowWindowingPill; 357 358 Builder(@onNull WindowDecoration parent)359 Builder(@NonNull WindowDecoration parent) { 360 mParent = parent; 361 } 362 setAppName(@ullable CharSequence name)363 Builder setAppName(@Nullable CharSequence name) { 364 mName = name; 365 return this; 366 } 367 setAppIcon(@ullable Drawable appIcon)368 Builder setAppIcon(@Nullable Drawable appIcon) { 369 mAppIcon = appIcon; 370 return this; 371 } 372 setOnClickListener(@ullable View.OnClickListener onClickListener)373 Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) { 374 mOnClickListener = onClickListener; 375 return this; 376 } 377 setOnTouchListener(@ullable View.OnTouchListener onTouchListener)378 Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) { 379 mOnTouchListener = onTouchListener; 380 return this; 381 } 382 setLayoutId(int layoutId)383 Builder setLayoutId(int layoutId) { 384 mLayoutId = layoutId; 385 return this; 386 } 387 setCaptionPosition(int captionX, int captionY)388 Builder setCaptionPosition(int captionX, int captionY) { 389 mCaptionX = captionX; 390 mCaptionY = captionY; 391 return this; 392 } 393 setWindowingButtonsVisible(boolean windowingButtonsVisible)394 Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) { 395 mShowWindowingPill = windowingButtonsVisible; 396 return this; 397 } 398 build()399 HandleMenu build() { 400 return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener, 401 mOnTouchListener, mAppIcon, mName, mShowWindowingPill); 402 } 403 } 404 } 405