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