• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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