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.window.DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING; 20 21 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; 22 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; 23 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; 24 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; 25 26 import android.annotation.NonNull; 27 import android.annotation.SuppressLint; 28 import android.app.ActivityManager; 29 import android.app.ActivityManager.RunningTaskInfo; 30 import android.app.WindowConfiguration; 31 import android.app.WindowConfiguration.WindowingMode; 32 import android.content.Context; 33 import android.content.res.ColorStateList; 34 import android.content.res.Resources; 35 import android.graphics.Color; 36 import android.graphics.Insets; 37 import android.graphics.Point; 38 import android.graphics.Rect; 39 import android.graphics.Region; 40 import android.graphics.drawable.GradientDrawable; 41 import android.os.Handler; 42 import android.util.Size; 43 import android.view.Choreographer; 44 import android.view.InsetsState; 45 import android.view.MotionEvent; 46 import android.view.SurfaceControl; 47 import android.view.View; 48 import android.view.ViewConfiguration; 49 import android.view.WindowInsets; 50 import android.view.WindowManager; 51 import android.view.WindowManagerGlobal; 52 import android.window.DesktopExperienceFlags; 53 import android.window.DesktopModeFlags; 54 import android.window.WindowContainerTransaction; 55 56 import com.android.internal.annotations.VisibleForTesting; 57 import com.android.wm.shell.R; 58 import com.android.wm.shell.ShellTaskOrganizer; 59 import com.android.wm.shell.common.DisplayController; 60 import com.android.wm.shell.common.DisplayLayout; 61 import com.android.wm.shell.common.ShellExecutor; 62 import com.android.wm.shell.common.SyncTransactionQueue; 63 import com.android.wm.shell.shared.annotations.ShellBackgroundThread; 64 import com.android.wm.shell.shared.annotations.ShellMainThread; 65 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; 66 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; 67 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; 68 import com.android.wm.shell.windowdecor.extension.TaskInfoKt; 69 70 /** 71 * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with 72 * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button, 73 * maximize button and close button. 74 */ 75 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { 76 private final Handler mHandler; 77 private final @ShellMainThread ShellExecutor mMainExecutor; 78 private final @ShellBackgroundThread ShellExecutor mBgExecutor; 79 private final Choreographer mChoreographer; 80 private final SyncTransactionQueue mSyncQueue; 81 82 private View.OnClickListener mOnCaptionButtonClickListener; 83 private View.OnTouchListener mOnCaptionTouchListener; 84 private DragPositioningCallback mDragPositioningCallback; 85 private DragResizeInputListener mDragResizeListener; 86 87 private RelayoutParams mRelayoutParams = new RelayoutParams(); 88 private final RelayoutResult<WindowDecorLinearLayout> mResult = 89 new RelayoutResult<>(); 90 CaptionWindowDecoration( Context context, @NonNull Context userContext, DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, @NonNull WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier)91 CaptionWindowDecoration( 92 Context context, 93 @NonNull Context userContext, 94 DisplayController displayController, 95 ShellTaskOrganizer taskOrganizer, 96 RunningTaskInfo taskInfo, 97 SurfaceControl taskSurface, 98 Handler handler, 99 @ShellMainThread ShellExecutor mainExecutor, 100 @ShellBackgroundThread ShellExecutor bgExecutor, 101 Choreographer choreographer, 102 SyncTransactionQueue syncQueue, 103 @NonNull WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier) { 104 super(context, userContext, displayController, taskOrganizer, taskInfo, 105 taskSurface, windowDecorViewHostSupplier); 106 mHandler = handler; 107 mMainExecutor = mainExecutor; 108 mBgExecutor = bgExecutor; 109 mChoreographer = choreographer; 110 mSyncQueue = syncQueue; 111 } 112 setCaptionListeners( View.OnClickListener onCaptionButtonClickListener, View.OnTouchListener onCaptionTouchListener)113 void setCaptionListeners( 114 View.OnClickListener onCaptionButtonClickListener, 115 View.OnTouchListener onCaptionTouchListener) { 116 mOnCaptionButtonClickListener = onCaptionButtonClickListener; 117 mOnCaptionTouchListener = onCaptionTouchListener; 118 } 119 setDragPositioningCallback(DragPositioningCallback dragPositioningCallback)120 void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) { 121 mDragPositioningCallback = dragPositioningCallback; 122 } 123 124 @Override 125 @NonNull calculateValidDragArea()126 Rect calculateValidDragArea() { 127 final Context displayContext = mDisplayController.getDisplayContext(mTaskInfo.displayId); 128 if (displayContext == null) return new Rect(); 129 final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(), 130 R.dimen.caption_left_buttons_width); 131 132 // On a smaller screen, don't require as much empty space on screen, as offscreen 133 // drags will be restricted too much. 134 final int requiredEmptySpaceId = displayContext 135 .getResources().getConfiguration().smallestScreenWidthDp >= 600 136 ? R.dimen.freeform_required_visible_empty_space_in_header : 137 R.dimen.small_screen_required_visible_empty_space_in_header; 138 final int requiredEmptySpace = loadDimensionPixelSize(mContext.getResources(), 139 requiredEmptySpaceId); 140 141 final int rightButtonsWidth = loadDimensionPixelSize(mContext.getResources(), 142 R.dimen.caption_right_buttons_width); 143 final int taskWidth = mTaskInfo.configuration.windowConfiguration.getBounds().width(); 144 final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); 145 final int displayWidth = layout.width(); 146 final Rect stableBounds = new Rect(); 147 layout.getStableBounds(stableBounds); 148 return new Rect( 149 determineMinX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace, 150 taskWidth), 151 stableBounds.top, 152 determineMaxX(leftButtonsWidth, rightButtonsWidth, requiredEmptySpace, taskWidth, 153 displayWidth), 154 determineMaxY(requiredEmptySpace, stableBounds)); 155 } 156 157 158 /** 159 * Determine the lowest x coordinate of a freeform task. Used for restricting drag inputs. 160 */ determineMinX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace, int taskWidth)161 private int determineMinX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace, 162 int taskWidth) { 163 // Do not let apps with < 48dp empty header space go off the left edge at all. 164 if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) { 165 return 0; 166 } 167 return -taskWidth + requiredEmptySpace + rightButtonsWidth; 168 } 169 170 /** 171 * Determine the highest x coordinate of a freeform task. Used for restricting drag inputs. 172 */ determineMaxX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace, int taskWidth, int displayWidth)173 private int determineMaxX(int leftButtonsWidth, int rightButtonsWidth, int requiredEmptySpace, 174 int taskWidth, int displayWidth) { 175 // Do not let apps with < 48dp empty header space go off the right edge at all. 176 if (leftButtonsWidth + rightButtonsWidth + requiredEmptySpace > taskWidth) { 177 return displayWidth - taskWidth; 178 } 179 return displayWidth - requiredEmptySpace - leftButtonsWidth; 180 } 181 182 /** 183 * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs. 184 */ determineMaxY(int requiredEmptySpace, Rect stableBounds)185 private int determineMaxY(int requiredEmptySpace, Rect stableBounds) { 186 return stableBounds.bottom - requiredEmptySpace; 187 } 188 189 @Override relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, @NonNull Region displayExclusionRegion)190 void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus, 191 @NonNull Region displayExclusionRegion) { 192 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 193 // The crop and position of the task should only be set when a task is fluid resizing. In 194 // all other cases, it is expected that the transition handler positions and crops the task 195 // in order to allow the handler time to animate before the task before the final 196 // position and crop are set. 197 final boolean shouldSetTaskVisibilityPositionAndCrop = 198 mTaskDragResizer.isResizingOrAnimating(); 199 // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is 200 // synced with the buffer transaction (that draws the View). Both will be shown on screen 201 // at the same, whereas applying them independently causes flickering. See b/270202228. 202 relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, 203 shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion); 204 } 205 206 @VisibleForTesting updateRelayoutParams( RelayoutParams relayoutParams, @NonNull Context context, ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, InsetsState displayInsetsState, boolean hasGlobalFocus, @NonNull Region globalExclusionRegion)207 static void updateRelayoutParams( 208 RelayoutParams relayoutParams, 209 @NonNull Context context, 210 ActivityManager.RunningTaskInfo taskInfo, 211 boolean applyStartTransactionOnDraw, 212 boolean shouldSetTaskVisibilityPositionAndCrop, 213 boolean isStatusBarVisible, 214 boolean isKeyguardVisibleAndOccluded, 215 InsetsState displayInsetsState, 216 boolean hasGlobalFocus, 217 @NonNull Region globalExclusionRegion) { 218 relayoutParams.reset(); 219 relayoutParams.mRunningTaskInfo = taskInfo; 220 relayoutParams.mLayoutResId = R.layout.caption_window_decor; 221 relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); 222 if (DesktopExperienceFlags.ENABLE_DYNAMIC_RADIUS_COMPUTATION_BUGFIX.isTrue()) { 223 relayoutParams.mShadowRadiusId = hasGlobalFocus 224 ? R.dimen.freeform_decor_shadow_focused_thickness 225 : R.dimen.freeform_decor_shadow_unfocused_thickness; 226 } else { 227 relayoutParams.mShadowRadius = hasGlobalFocus 228 ? context.getResources().getDimensionPixelSize( 229 R.dimen.freeform_decor_shadow_focused_thickness) 230 : context.getResources().getDimensionPixelSize( 231 R.dimen.freeform_decor_shadow_unfocused_thickness); 232 } 233 relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; 234 relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; 235 relayoutParams.mIsCaptionVisible = taskInfo.isFreeform() 236 || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); 237 relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion); 238 239 if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { 240 // If the app is requesting to customize the caption bar, allow input to fall 241 // through to the windows below so that the app can respond to input events on 242 // their custom content. 243 relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; 244 } 245 final RelayoutParams.OccludingCaptionElement backButtonElement = 246 new RelayoutParams.OccludingCaptionElement(); 247 backButtonElement.mWidthResId = R.dimen.caption_left_buttons_width; 248 backButtonElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START; 249 relayoutParams.mOccludingCaptionElements.add(backButtonElement); 250 // Then, the right-aligned section (minimize, maximize and close buttons). 251 final RelayoutParams.OccludingCaptionElement controlsElement = 252 new RelayoutParams.OccludingCaptionElement(); 253 controlsElement.mWidthResId = R.dimen.caption_right_buttons_width; 254 controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; 255 relayoutParams.mOccludingCaptionElements.add(controlsElement); 256 relayoutParams.mCaptionTopPadding = getTopPadding(relayoutParams, 257 taskInfo.getConfiguration().windowConfiguration.getBounds(), displayInsetsState); 258 // Set opaque background for all freeform tasks to prevent freeform tasks below 259 // from being visible if freeform task window above is translucent. 260 // Otherwise if fluid resize is enabled, add a background to freeform tasks. 261 relayoutParams.mShouldSetBackground = DesktopModeStatus.shouldSetBackground(taskInfo); 262 } 263 264 @SuppressLint("MissingPermission") relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus, @NonNull Region globalExclusionRegion)265 void relayout(RunningTaskInfo taskInfo, 266 SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, 267 boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, 268 boolean hasGlobalFocus, 269 @NonNull Region globalExclusionRegion) { 270 final boolean isFreeform = 271 taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; 272 final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue() 273 ? isFreeform : isFreeform && taskInfo.isResizeable; 274 275 final WindowDecorLinearLayout oldRootView = mResult.mRootView; 276 final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; 277 final WindowContainerTransaction wct = new WindowContainerTransaction(); 278 279 updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, 280 shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, 281 mIsKeyguardVisibleAndOccluded, 282 mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, 283 globalExclusionRegion); 284 285 relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); 286 // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo 287 288 mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); 289 290 if (mResult.mRootView == null) { 291 // This means something blocks the window decor from showing, e.g. the task is hidden. 292 // Nothing is set up in this case including the decoration surface. 293 return; 294 } 295 if (oldRootView != mResult.mRootView) { 296 setupRootView(); 297 } 298 299 bindData(mResult.mRootView, taskInfo); 300 301 if (!isDragResizeable) { 302 closeDragResizeListener(); 303 return; 304 } 305 306 if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { 307 closeDragResizeListener(); 308 final ShellExecutor bgExecutor = 309 DesktopModeFlags.ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD.isTrue() 310 ? mBgExecutor : mMainExecutor; 311 mDragResizeListener = new DragResizeInputListener( 312 mContext, 313 WindowManagerGlobal.getWindowSession(), 314 mMainExecutor, 315 bgExecutor, 316 mTaskInfo, 317 mHandler, 318 mChoreographer, 319 mDisplay.getDisplayId(), 320 mDecorationContainerSurface, 321 mDragPositioningCallback, 322 mSurfaceControlBuilderSupplier, 323 mSurfaceControlTransactionSupplier, 324 mDisplayController); 325 } 326 final DragResizeInputListener newListener = mDragResizeListener; 327 final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) 328 .getScaledTouchSlop(); 329 final Resources res = mResult.mRootView.getResources(); 330 final DragResizeWindowGeometry newGeometry = new DragResizeWindowGeometry( 331 0 /* taskCornerRadius */, 332 new Size(mResult.mWidth, mResult.mHeight), 333 getResizeEdgeHandleSize(res), 334 getResizeHandleEdgeInset(res), getFineResizeCornerSize(res), 335 getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE); 336 newListener.addInitializedCallback(() -> { 337 mDragResizeListener.setGeometry(newGeometry, touchSlop); 338 }); 339 } 340 341 /** 342 * Sets up listeners when a new root view is created. 343 */ setupRootView()344 private void setupRootView() { 345 final View caption = mResult.mRootView.findViewById(R.id.caption); 346 caption.setOnTouchListener(mOnCaptionTouchListener); 347 final View close = caption.findViewById(R.id.close_window); 348 close.setOnClickListener(mOnCaptionButtonClickListener); 349 final View back = caption.findViewById(R.id.back_button); 350 back.setOnClickListener(mOnCaptionButtonClickListener); 351 final View minimize = caption.findViewById(R.id.minimize_window); 352 minimize.setOnClickListener(mOnCaptionButtonClickListener); 353 final View maximize = caption.findViewById(R.id.maximize_window); 354 maximize.setOnClickListener(mOnCaptionButtonClickListener); 355 } 356 bindData(View rootView, RunningTaskInfo taskInfo)357 private void bindData(View rootView, RunningTaskInfo taskInfo) { 358 // Set up the tint first so that the drawable can be stylized when loaded. 359 setupCaptionColor(taskInfo); 360 361 final boolean isFullscreen = 362 taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 363 rootView.findViewById(R.id.maximize_window) 364 .setBackgroundResource(isFullscreen ? R.drawable.decor_restore_button_dark 365 : R.drawable.decor_maximize_button_dark); 366 } 367 setupCaptionColor(RunningTaskInfo taskInfo)368 private void setupCaptionColor(RunningTaskInfo taskInfo) { 369 if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { 370 setCaptionColor(Color.TRANSPARENT); 371 } else { 372 final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); 373 setCaptionColor(statusBarColor); 374 } 375 } 376 setCaptionColor(int captionColor)377 private void setCaptionColor(int captionColor) { 378 if (mResult.mRootView == null) { 379 return; 380 } 381 382 final View caption = mResult.mRootView.findViewById(R.id.caption); 383 final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); 384 captionDrawable.setColor(captionColor); 385 386 final int buttonTintColorRes = 387 Color.valueOf(captionColor).luminance() < 0.5 388 ? R.color.decor_button_light_color 389 : R.color.decor_button_dark_color; 390 final ColorStateList buttonTintColor = 391 caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); 392 393 final View back = caption.findViewById(R.id.back_button); 394 back.setBackgroundTintList(buttonTintColor); 395 396 final View minimize = caption.findViewById(R.id.minimize_window); 397 minimize.setBackgroundTintList(buttonTintColor); 398 399 final View maximize = caption.findViewById(R.id.maximize_window); 400 maximize.setBackgroundTintList(buttonTintColor); 401 402 final View close = caption.findViewById(R.id.close_window); 403 close.setBackgroundTintList(buttonTintColor); 404 } 405 406 boolean isHandlingDragResize() { 407 return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize(); 408 } 409 410 private void closeDragResizeListener() { 411 if (mDragResizeListener == null) { 412 return; 413 } 414 mDragResizeListener.close(); 415 mDragResizeListener = null; 416 } 417 418 private static int getTopPadding(RelayoutParams params, Rect taskBounds, 419 InsetsState insetsState) { 420 if (!params.mRunningTaskInfo.isFreeform()) { 421 Insets systemDecor = insetsState.calculateInsets(taskBounds, 422 WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(), 423 false /* ignoreVisibility */); 424 return systemDecor.top; 425 } else { 426 return 0; 427 } 428 } 429 430 /** 431 * Checks whether the touch event falls inside the customizable caption region. 432 */ 433 boolean checkTouchEventInCustomizableRegion(MotionEvent ev) { 434 return mResult.mCustomizableCaptionRegion.contains((int) ev.getRawX(), (int) ev.getRawY()); 435 } 436 437 boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { 438 return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); 439 } 440 441 @Override 442 public void close() { 443 closeDragResizeListener(); 444 super.close(); 445 } 446 447 @Override 448 int getCaptionHeightId(@WindowingMode int windowingMode) { 449 return getCaptionHeightIdStatic(windowingMode); 450 } 451 452 private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { 453 return R.dimen.freeform_decor_caption_height; 454 } 455 456 @Override 457 int getCaptionViewId() { 458 return R.id.caption; 459 } 460 } 461