1 /* 2 * Copyright (C) 2022 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 21 import android.app.ActivityManager; 22 import android.content.Context; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.content.res.Configuration; 26 import android.graphics.Point; 27 import android.graphics.PointF; 28 import android.graphics.Rect; 29 import android.graphics.Region; 30 import android.graphics.drawable.Drawable; 31 import android.os.Handler; 32 import android.os.IBinder; 33 import android.util.Log; 34 import android.view.Choreographer; 35 import android.view.MotionEvent; 36 import android.view.SurfaceControl; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.window.WindowContainerTransaction; 40 41 import com.android.internal.policy.ScreenDecorationsUtils; 42 import com.android.launcher3.icons.IconProvider; 43 import com.android.wm.shell.R; 44 import com.android.wm.shell.ShellTaskOrganizer; 45 import com.android.wm.shell.common.DisplayController; 46 import com.android.wm.shell.common.SyncTransactionQueue; 47 import com.android.wm.shell.desktopmode.DesktopModeStatus; 48 import com.android.wm.shell.desktopmode.DesktopTasksController; 49 import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder; 50 import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; 51 import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; 52 53 import java.util.HashSet; 54 import java.util.Set; 55 56 /** 57 * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with 58 * {@link DesktopModeWindowDecorViewModel}. 59 * 60 * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. 61 */ 62 public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { 63 private static final String TAG = "DesktopModeWindowDecoration"; 64 65 private final Handler mHandler; 66 private final Choreographer mChoreographer; 67 private final SyncTransactionQueue mSyncQueue; 68 69 private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder; 70 private View.OnClickListener mOnCaptionButtonClickListener; 71 private View.OnTouchListener mOnCaptionTouchListener; 72 private DragPositioningCallback mDragPositioningCallback; 73 private DragResizeInputListener mDragResizeListener; 74 private DragDetector mDragDetector; 75 76 private RelayoutParams mRelayoutParams = new RelayoutParams(); 77 private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = 78 new WindowDecoration.RelayoutResult<>(); 79 80 private final Point mPositionInParent = new Point(); 81 private HandleMenu mHandleMenu; 82 83 private ResizeVeil mResizeVeil; 84 85 private Drawable mAppIcon; 86 private CharSequence mAppName; 87 88 private TaskCornersListener mCornersListener; 89 90 private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>(); 91 private int mRelayoutBlock; 92 DesktopModeWindowDecoration( Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue)93 DesktopModeWindowDecoration( 94 Context context, 95 DisplayController displayController, 96 ShellTaskOrganizer taskOrganizer, 97 ActivityManager.RunningTaskInfo taskInfo, 98 SurfaceControl taskSurface, 99 Handler handler, 100 Choreographer choreographer, 101 SyncTransactionQueue syncQueue) { 102 super(context, displayController, taskOrganizer, taskInfo, taskSurface); 103 104 mHandler = handler; 105 mChoreographer = choreographer; 106 mSyncQueue = syncQueue; 107 108 loadAppInfo(); 109 } 110 111 @Override getConfigurationWithOverrides( ActivityManager.RunningTaskInfo taskInfo)112 protected Configuration getConfigurationWithOverrides( 113 ActivityManager.RunningTaskInfo taskInfo) { 114 Configuration configuration = taskInfo.getConfiguration(); 115 if (DesktopTasksController.isDesktopDensityOverrideSet()) { 116 // Density is overridden for desktop tasks. Keep system density for window decoration. 117 configuration.densityDpi = mContext.getResources().getConfiguration().densityDpi; 118 } 119 return configuration; 120 } 121 setCaptionListeners( View.OnClickListener onCaptionButtonClickListener, View.OnTouchListener onCaptionTouchListener)122 void setCaptionListeners( 123 View.OnClickListener onCaptionButtonClickListener, 124 View.OnTouchListener onCaptionTouchListener) { 125 mOnCaptionButtonClickListener = onCaptionButtonClickListener; 126 mOnCaptionTouchListener = onCaptionTouchListener; 127 } 128 setCornersListener(TaskCornersListener cornersListener)129 void setCornersListener(TaskCornersListener cornersListener) { 130 mCornersListener = cornersListener; 131 } 132 setDragPositioningCallback(DragPositioningCallback dragPositioningCallback)133 void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) { 134 mDragPositioningCallback = dragPositioningCallback; 135 } 136 setDragDetector(DragDetector dragDetector)137 void setDragDetector(DragDetector dragDetector) { 138 mDragDetector = dragDetector; 139 mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); 140 } 141 142 @Override relayout(ActivityManager.RunningTaskInfo taskInfo)143 void relayout(ActivityManager.RunningTaskInfo taskInfo) { 144 // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell 145 // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl 146 // and interferes with the transition animation that is playing at the same time. 147 if (mRelayoutBlock > 0) { 148 return; 149 } 150 151 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 152 // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is 153 // synced with the buffer transaction (that draws the View). Both will be shown on screen 154 // at the same, whereas applying them independently causes flickering. See b/270202228. 155 relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */); 156 } 157 relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw)158 void relayout(ActivityManager.RunningTaskInfo taskInfo, 159 SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, 160 boolean applyStartTransactionOnDraw) { 161 final int shadowRadiusID = taskInfo.isFocused 162 ? R.dimen.freeform_decor_shadow_focused_thickness 163 : R.dimen.freeform_decor_shadow_unfocused_thickness; 164 final boolean isFreeform = 165 taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; 166 final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; 167 168 if (isHandleMenuActive()) { 169 mHandleMenu.relayout(startT); 170 } 171 172 final WindowDecorLinearLayout oldRootView = mResult.mRootView; 173 final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; 174 final WindowContainerTransaction wct = new WindowContainerTransaction(); 175 176 final int windowDecorLayoutId = getDesktopModeWindowDecorLayoutId( 177 taskInfo.getWindowingMode()); 178 mRelayoutParams.reset(); 179 mRelayoutParams.mRunningTaskInfo = taskInfo; 180 mRelayoutParams.mLayoutResId = windowDecorLayoutId; 181 mRelayoutParams.mCaptionHeightId = getCaptionHeightId(); 182 mRelayoutParams.mShadowRadiusId = shadowRadiusID; 183 mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; 184 185 mRelayoutParams.mCornerRadius = 186 (int) ScreenDecorationsUtils.getWindowCornerRadius(mContext); 187 relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); 188 // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo 189 190 mTaskOrganizer.applyTransaction(wct); 191 192 if (mResult.mRootView == null) { 193 // This means something blocks the window decor from showing, e.g. the task is hidden. 194 // Nothing is set up in this case including the decoration surface. 195 return; 196 } 197 if (oldRootView != mResult.mRootView) { 198 if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) { 199 mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder( 200 mResult.mRootView, 201 mOnCaptionTouchListener, 202 mOnCaptionButtonClickListener 203 ); 204 } else if (mRelayoutParams.mLayoutResId 205 == R.layout.desktop_mode_app_controls_window_decor) { 206 mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( 207 mResult.mRootView, 208 mOnCaptionTouchListener, 209 mOnCaptionButtonClickListener, 210 mAppName, 211 mAppIcon 212 ); 213 } else { 214 throw new IllegalArgumentException("Unexpected layout resource id"); 215 } 216 } 217 mWindowDecorViewHolder.bindData(mTaskInfo); 218 219 if (!mTaskInfo.isFocused) { 220 closeHandleMenu(); 221 } 222 223 if (!isDragResizeable) { 224 closeDragResizeListener(); 225 return; 226 } 227 228 if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { 229 closeDragResizeListener(); 230 mDragResizeListener = new DragResizeInputListener( 231 mContext, 232 mHandler, 233 mChoreographer, 234 mDisplay.getDisplayId(), 235 mRelayoutParams.mCornerRadius, 236 mDecorationContainerSurface, 237 mDragPositioningCallback, 238 mSurfaceControlBuilderSupplier, 239 mSurfaceControlTransactionSupplier); 240 } 241 242 final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) 243 .getScaledTouchSlop(); 244 mDragDetector.setTouchSlop(touchSlop); 245 246 final int resize_handle = mResult.mRootView.getResources() 247 .getDimensionPixelSize(R.dimen.freeform_resize_handle); 248 final int resize_corner = mResult.mRootView.getResources() 249 .getDimensionPixelSize(R.dimen.freeform_resize_corner); 250 251 // If either task geometry or position have changed, update this task's cornersListener 252 if (mDragResizeListener.setGeometry( 253 mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop) 254 || !mTaskInfo.positionInParent.equals(mPositionInParent)) { 255 mCornersListener.onTaskCornersChanged(mTaskInfo.taskId, getGlobalCornersRegion()); 256 } 257 mPositionInParent.set(mTaskInfo.positionInParent); 258 } 259 isHandleMenuActive()260 boolean isHandleMenuActive() { 261 return mHandleMenu != null; 262 } 263 loadAppInfo()264 private void loadAppInfo() { 265 String packageName = mTaskInfo.realActivity.getPackageName(); 266 PackageManager pm = mContext.getApplicationContext().getPackageManager(); 267 try { 268 IconProvider provider = new IconProvider(mContext); 269 mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, 270 PackageManager.ComponentInfoFlags.of(0))); 271 ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 272 PackageManager.ApplicationInfoFlags.of(0)); 273 mAppName = pm.getApplicationLabel(applicationInfo); 274 } catch (PackageManager.NameNotFoundException e) { 275 Log.w(TAG, "Package not found: " + packageName, e); 276 } 277 } 278 closeDragResizeListener()279 private void closeDragResizeListener() { 280 if (mDragResizeListener == null) { 281 return; 282 } 283 mDragResizeListener.close(); 284 mDragResizeListener = null; 285 } 286 287 /** 288 * Create the resize veil for this task. Note the veil's visibility is View.GONE by default 289 * until a resize event calls showResizeVeil below. 290 */ createResizeVeil()291 void createResizeVeil() { 292 mResizeVeil = new ResizeVeil(mContext, mAppIcon, mTaskInfo, 293 mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier); 294 } 295 296 /** 297 * Show the resize veil. 298 */ showResizeVeil(Rect taskBounds)299 public void showResizeVeil(Rect taskBounds) { 300 mResizeVeil.showVeil(mTaskSurface, taskBounds); 301 } 302 303 /** 304 * Show the resize veil. 305 */ showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds)306 public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) { 307 mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */); 308 } 309 310 /** 311 * Set new bounds for the resize veil 312 */ updateResizeVeil(Rect newBounds)313 public void updateResizeVeil(Rect newBounds) { 314 mResizeVeil.updateResizeVeil(newBounds); 315 } 316 317 /** 318 * Set new bounds for the resize veil 319 */ updateResizeVeil(SurfaceControl.Transaction tx, Rect newBounds)320 public void updateResizeVeil(SurfaceControl.Transaction tx, Rect newBounds) { 321 mResizeVeil.updateResizeVeil(tx, newBounds); 322 } 323 324 /** 325 * Fade the resize veil out. 326 */ hideResizeVeil()327 public void hideResizeVeil() { 328 mResizeVeil.hideVeil(); 329 } 330 disposeResizeVeil()331 private void disposeResizeVeil() { 332 if (mResizeVeil == null) return; 333 mResizeVeil.dispose(); 334 mResizeVeil = null; 335 } 336 337 /** 338 * Create and display handle menu window 339 */ createHandleMenu()340 void createHandleMenu() { 341 mHandleMenu = new HandleMenu.Builder(this) 342 .setAppIcon(mAppIcon) 343 .setAppName(mAppName) 344 .setOnClickListener(mOnCaptionButtonClickListener) 345 .setOnTouchListener(mOnCaptionTouchListener) 346 .setLayoutId(mRelayoutParams.mLayoutResId) 347 .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY) 348 .setWindowingButtonsVisible(DesktopModeStatus.isProto2Enabled()) 349 .build(); 350 mHandleMenu.show(); 351 } 352 353 /** 354 * Close the handle menu window 355 */ closeHandleMenu()356 void closeHandleMenu() { 357 if (!isHandleMenuActive()) return; 358 mHandleMenu.close(); 359 mHandleMenu = null; 360 } 361 362 @Override releaseViews()363 void releaseViews() { 364 closeHandleMenu(); 365 super.releaseViews(); 366 } 367 368 /** 369 * Close an open handle menu if input is outside of menu coordinates 370 * 371 * @param ev the tapped point to compare against 372 */ closeHandleMenuIfNeeded(MotionEvent ev)373 void closeHandleMenuIfNeeded(MotionEvent ev) { 374 if (!isHandleMenuActive()) return; 375 376 PointF inputPoint = offsetCaptionLocation(ev); 377 378 // If this is called before open_menu_button's onClick, we don't want to close 379 // the menu since it will just reopen in onClick. 380 final boolean pointInOpenMenuButton = pointInView( 381 mResult.mRootView.findViewById(R.id.open_menu_button), 382 inputPoint.x, 383 inputPoint.y); 384 385 if (!mHandleMenu.isValidMenuInput(inputPoint) && !pointInOpenMenuButton) { 386 closeHandleMenu(); 387 } 388 } 389 isFocused()390 boolean isFocused() { 391 return mTaskInfo.isFocused; 392 } 393 394 /** 395 * Offset the coordinates of a {@link MotionEvent} to be in the same coordinate space as caption 396 * 397 * @param ev the {@link MotionEvent} to offset 398 * @return the point of the input in local space 399 */ offsetCaptionLocation(MotionEvent ev)400 private PointF offsetCaptionLocation(MotionEvent ev) { 401 final PointF result = new PointF(ev.getX(), ev.getY()); 402 final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId) 403 .positionInParent; 404 result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY); 405 result.offset(-positionInParent.x, -positionInParent.y); 406 return result; 407 } 408 409 /** 410 * Determine if a passed MotionEvent is in a view in caption 411 * 412 * @param ev the {@link MotionEvent} to check 413 * @param layoutId the id of the view 414 * @return {@code true} if event is inside the specified view, {@code false} if not 415 */ checkEventInCaptionView(MotionEvent ev, int layoutId)416 private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) { 417 if (mResult.mRootView == null) return false; 418 final PointF inputPoint = offsetCaptionLocation(ev); 419 final View view = mResult.mRootView.findViewById(layoutId); 420 return view != null && pointInView(view, inputPoint.x, inputPoint.y); 421 } 422 checkTouchEventInHandle(MotionEvent ev)423 boolean checkTouchEventInHandle(MotionEvent ev) { 424 if (isHandleMenuActive()) return false; 425 return checkEventInCaptionView(ev, R.id.caption_handle); 426 } 427 428 /** 429 * Check a passed MotionEvent if a click has occurred on any button on this caption 430 * Note this should only be called when a regular onClick is not possible 431 * (i.e. the button was clicked through status bar layer) 432 * 433 * @param ev the MotionEvent to compare 434 */ checkClickEvent(MotionEvent ev)435 void checkClickEvent(MotionEvent ev) { 436 if (mResult.mRootView == null) return; 437 if (!isHandleMenuActive()) { 438 final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); 439 final View handle = caption.findViewById(R.id.caption_handle); 440 clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle); 441 } else { 442 mHandleMenu.checkClickEvent(ev); 443 } 444 } 445 clickIfPointInView(PointF inputPoint, View v)446 private boolean clickIfPointInView(PointF inputPoint, View v) { 447 if (pointInView(v, inputPoint.x, inputPoint.y)) { 448 mOnCaptionButtonClickListener.onClick(v); 449 return true; 450 } 451 return false; 452 } 453 pointInView(View v, float x, float y)454 boolean pointInView(View v, float x, float y) { 455 return v != null && v.getLeft() <= x && v.getRight() >= x 456 && v.getTop() <= y && v.getBottom() >= y; 457 } 458 459 @Override close()460 public void close() { 461 closeDragResizeListener(); 462 closeHandleMenu(); 463 mCornersListener.onTaskCornersRemoved(mTaskInfo.taskId); 464 disposeResizeVeil(); 465 super.close(); 466 } 467 getDesktopModeWindowDecorLayoutId(int windowingMode)468 private int getDesktopModeWindowDecorLayoutId(int windowingMode) { 469 if (DesktopModeStatus.isProto1Enabled()) { 470 return R.layout.desktop_mode_app_controls_window_decor; 471 } 472 return windowingMode == WINDOWING_MODE_FREEFORM 473 ? R.layout.desktop_mode_app_controls_window_decor 474 : R.layout.desktop_mode_focused_window_decor; 475 } 476 477 /** 478 * Create a new region out of the corner rects of this task. 479 */ getGlobalCornersRegion()480 Region getGlobalCornersRegion() { 481 Region cornersRegion = mDragResizeListener.getCornersRegion(); 482 cornersRegion.translate(mPositionInParent.x, mPositionInParent.y); 483 return cornersRegion; 484 } 485 486 /** 487 * If transition exists in mTransitionsPausingRelayout, remove the transition and decrement 488 * mRelayoutBlock 489 */ removeTransitionPausingRelayout(IBinder transition)490 void removeTransitionPausingRelayout(IBinder transition) { 491 if (mTransitionsPausingRelayout.remove(transition)) { 492 mRelayoutBlock--; 493 } 494 } 495 496 @Override getCaptionHeightId()497 int getCaptionHeightId() { 498 return R.dimen.freeform_decor_caption_height; 499 } 500 501 /** 502 * Add transition to mTransitionsPausingRelayout 503 */ addTransitionPausingRelayout(IBinder transition)504 void addTransitionPausingRelayout(IBinder transition) { 505 mTransitionsPausingRelayout.add(transition); 506 } 507 508 /** 509 * If two transitions merge and the merged transition is in mTransitionsPausingRelayout, 510 * remove the merged transition from the set and add the transition it was merged into. 511 */ mergeTransitionPausingRelayout(IBinder merged, IBinder playing)512 public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) { 513 if (mTransitionsPausingRelayout.remove(merged)) { 514 mTransitionsPausingRelayout.add(playing); 515 } 516 } 517 518 /** 519 * Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0. 520 */ incrementRelayoutBlock()521 public void incrementRelayoutBlock() { 522 mRelayoutBlock++; 523 } 524 525 static class Factory { 526 create( Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue)527 DesktopModeWindowDecoration create( 528 Context context, 529 DisplayController displayController, 530 ShellTaskOrganizer taskOrganizer, 531 ActivityManager.RunningTaskInfo taskInfo, 532 SurfaceControl taskSurface, 533 Handler handler, 534 Choreographer choreographer, 535 SyncTransactionQueue syncQueue) { 536 return new DesktopModeWindowDecoration( 537 context, 538 displayController, 539 taskOrganizer, 540 taskInfo, 541 taskSurface, 542 handler, 543 choreographer, 544 syncQueue); 545 } 546 } 547 548 interface TaskCornersListener { 549 /** Inform the implementing class of this task's change in corner resize handles */ onTaskCornersChanged(int taskId, Region corner)550 void onTaskCornersChanged(int taskId, Region corner); 551 552 /** Inform the implementing class that this task no longer needs its corners tracked, 553 * likely due to it closing. */ onTaskCornersRemoved(int taskId)554 void onTaskCornersRemoved(int taskId); 555 } 556 } 557