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.desktopmode;
18 
19 import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.RectEvaluator;
24 import android.animation.ValueAnimator;
25 import android.annotation.NonNull;
26 import android.app.ActivityManager;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.PixelFormat;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.util.DisplayMetrics;
33 import android.view.SurfaceControl;
34 import android.view.SurfaceControlViewHost;
35 import android.view.View;
36 import android.view.WindowManager;
37 import android.view.WindowlessWindowManager;
38 import android.view.animation.DecelerateInterpolator;
39 
40 import com.android.wm.shell.R;
41 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
42 import com.android.wm.shell.ShellTaskOrganizer;
43 import com.android.wm.shell.common.DisplayController;
44 import com.android.wm.shell.common.DisplayLayout;
45 import com.android.wm.shell.common.SyncTransactionQueue;
46 
47 /**
48  * Animated visual indicator for Desktop Mode windowing transitions.
49  */
50 public class DesktopModeVisualIndicator {
51     public static final int INVALID_INDICATOR = -1;
52     /** Indicates impending transition into desktop mode */
53     public static final int TO_DESKTOP_INDICATOR = 1;
54     /** Indicates impending transition into fullscreen */
55     public static final int TO_FULLSCREEN_INDICATOR = 2;
56     /** Indicates impending transition into split select on the left side */
57     public static final int TO_SPLIT_LEFT_INDICATOR = 3;
58     /** Indicates impending transition into split select on the right side */
59     public static final int TO_SPLIT_RIGHT_INDICATOR = 4;
60 
61     private final Context mContext;
62     private final DisplayController mDisplayController;
63     private final ShellTaskOrganizer mTaskOrganizer;
64     private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer;
65     private final ActivityManager.RunningTaskInfo mTaskInfo;
66     private final SurfaceControl mTaskSurface;
67     private final Rect mIndicatorRange = new Rect();
68     private SurfaceControl mLeash;
69 
70     private final SyncTransactionQueue mSyncQueue;
71     private SurfaceControlViewHost mViewHost;
72 
73     private View mView;
74     private boolean mIsFullscreen;
75     private int mType;
76 
DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, int type)77     public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue,
78             ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
79             Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer,
80             RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, int type) {
81         mSyncQueue = syncQueue;
82         mTaskInfo = taskInfo;
83         mDisplayController = displayController;
84         mContext = context;
85         mTaskSurface = taskSurface;
86         mTaskOrganizer = taskOrganizer;
87         mRootTdaOrganizer = taskDisplayAreaOrganizer;
88         mType = type;
89         defineIndicatorRange();
90         createView();
91     }
92 
93     /**
94      * If an indicator is warranted based on the input and task bounds, return the type of
95      * indicator that should be created.
96      */
determineIndicatorType(PointF inputCoordinates, Rect taskBounds, DisplayLayout layout, Context context)97     public static int determineIndicatorType(PointF inputCoordinates, Rect taskBounds,
98             DisplayLayout layout, Context context) {
99         int transitionAreaHeight = context.getResources().getDimensionPixelSize(
100                 com.android.wm.shell.R.dimen.desktop_mode_transition_area_height);
101         int transitionAreaWidth = context.getResources().getDimensionPixelSize(
102                 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width);
103         if (taskBounds.top <= transitionAreaHeight) return TO_FULLSCREEN_INDICATOR;
104         if (inputCoordinates.x <= transitionAreaWidth) return TO_SPLIT_LEFT_INDICATOR;
105         if (inputCoordinates.x >= layout.width() - transitionAreaWidth) {
106             return TO_SPLIT_RIGHT_INDICATOR;
107         }
108         return INVALID_INDICATOR;
109     }
110 
111     /**
112      * Determine range of inputs that will keep this indicator displaying.
113      */
defineIndicatorRange()114     private void defineIndicatorRange() {
115         DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
116         int captionHeight = mContext.getResources().getDimensionPixelSize(
117                 com.android.wm.shell.R.dimen.freeform_decor_caption_height);
118         int transitionAreaHeight = mContext.getResources().getDimensionPixelSize(
119                 com.android.wm.shell.R.dimen.desktop_mode_transition_area_height);
120         int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
121                 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width);
122         switch (mType) {
123             case TO_DESKTOP_INDICATOR:
124                 // TO_DESKTOP indicator is only dismissed on release; entire display is valid.
125                 mIndicatorRange.set(0, 0, layout.width(), layout.height());
126                 break;
127             case TO_FULLSCREEN_INDICATOR:
128                 // If drag results in caption going above the top edge of the display, we still
129                 // want to transition to fullscreen.
130                 mIndicatorRange.set(0, -captionHeight, layout.width(), transitionAreaHeight);
131                 break;
132             case TO_SPLIT_LEFT_INDICATOR:
133                 mIndicatorRange.set(0, transitionAreaHeight, transitionAreaWidth, layout.height());
134                 break;
135             case TO_SPLIT_RIGHT_INDICATOR:
136                 mIndicatorRange.set(layout.width() - transitionAreaWidth, transitionAreaHeight,
137                         layout.width(), layout.height());
138                 break;
139             default:
140                 break;
141         }
142     }
143 
144 
145     /**
146      * Create a fullscreen indicator with no animation
147      */
createView()148     private void createView() {
149         final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
150         final Resources resources = mContext.getResources();
151         final DisplayMetrics metrics = resources.getDisplayMetrics();
152         final int screenWidth = metrics.widthPixels;
153         final int screenHeight = metrics.heightPixels;
154 
155         mView = new View(mContext);
156         final SurfaceControl.Builder builder = new SurfaceControl.Builder();
157         mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder);
158         String description;
159         switch (mType) {
160             case TO_DESKTOP_INDICATOR:
161                 description = "Desktop indicator";
162                 break;
163             case TO_FULLSCREEN_INDICATOR:
164                 description = "Fullscreen indicator";
165                 break;
166             case TO_SPLIT_LEFT_INDICATOR:
167                 description = "Split Left indicator";
168                 break;
169             case TO_SPLIT_RIGHT_INDICATOR:
170                 description = "Split Right indicator";
171                 break;
172             default:
173                 description = "Invalid indicator";
174                 break;
175         }
176         mLeash = builder
177                 .setName(description)
178                 .setContainerLayer()
179                 .build();
180         t.show(mLeash);
181         final WindowManager.LayoutParams lp =
182                 new WindowManager.LayoutParams(screenWidth, screenHeight,
183                         WindowManager.LayoutParams.TYPE_APPLICATION,
184                         WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
185         lp.setTitle(description + " for Task=" + mTaskInfo.taskId);
186         lp.setTrustedOverlay();
187         final WindowlessWindowManager windowManager = new WindowlessWindowManager(
188                 mTaskInfo.configuration, mLeash,
189                 null /* hostInputToken */);
190         mViewHost = new SurfaceControlViewHost(mContext,
191                 mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
192                 "DesktopModeVisualIndicator");
193         mViewHost.setView(mView, lp);
194         // We want this indicator to be behind the dragged task, but in front of all others.
195         t.setRelativeLayer(mLeash, mTaskSurface, -1);
196 
197         mSyncQueue.runInSync(transaction -> {
198             transaction.merge(t);
199             t.close();
200         });
201     }
202 
203     /**
204      * Create an indicator. Animator fades it in while expanding the bounds outwards.
205      */
createIndicatorWithAnimatedBounds()206     public void createIndicatorWithAnimatedBounds() {
207         mIsFullscreen = mType == TO_FULLSCREEN_INDICATOR;
208         mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background);
209         final VisualIndicatorAnimator animator = VisualIndicatorAnimator
210                 .animateBounds(mView, mType,
211                         mDisplayController.getDisplayLayout(mTaskInfo.displayId));
212         animator.start();
213     }
214 
215     /**
216      * Takes existing fullscreen indicator and animates it to freeform bounds
217      */
transitionFullscreenIndicatorToFreeform()218     public void transitionFullscreenIndicatorToFreeform() {
219         mIsFullscreen = false;
220         mType = TO_DESKTOP_INDICATOR;
221         final VisualIndicatorAnimator animator = VisualIndicatorAnimator.toFreeformAnimator(
222                 mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId));
223         animator.start();
224     }
225 
226     /**
227      * Takes the existing freeform indicator and animates it to fullscreen
228      */
transitionFreeformIndicatorToFullscreen()229     public void transitionFreeformIndicatorToFullscreen() {
230         mIsFullscreen = true;
231         mType = TO_FULLSCREEN_INDICATOR;
232         final VisualIndicatorAnimator animator =
233                 VisualIndicatorAnimator.toFullscreenAnimatorWithAnimatedBounds(
234                 mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId));
235         animator.start();
236     }
237 
238     /**
239      * Determine if a MotionEvent is in the same range that enabled the indicator.
240      * Used to dismiss the indicator when a transition will no longer result from releasing.
241      */
eventOutsideRange(float x, float y)242     public boolean eventOutsideRange(float x, float y) {
243         return !mIndicatorRange.contains((int) x, (int) y);
244     }
245 
246     /**
247      * Release the indicator and its components when it is no longer needed.
248      */
releaseVisualIndicator(SurfaceControl.Transaction t)249     public void releaseVisualIndicator(SurfaceControl.Transaction t) {
250         if (mViewHost == null) return;
251         if (mViewHost != null) {
252             mViewHost.release();
253             mViewHost = null;
254         }
255 
256         if (mLeash != null) {
257             t.remove(mLeash);
258             mLeash = null;
259         }
260     }
261 
262     /**
263      * Returns true if visual indicator is fullscreen
264      */
isFullscreen()265     public boolean isFullscreen() {
266         return mIsFullscreen;
267     }
268 
269     /**
270      * Animator for Desktop Mode transitions which supports bounds and alpha animation.
271      */
272     private static class VisualIndicatorAnimator extends ValueAnimator {
273         private static final int FULLSCREEN_INDICATOR_DURATION = 200;
274         private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f;
275         private static final float INDICATOR_FINAL_OPACITY = 0.7f;
276 
277         private final View mView;
278         private final Rect mStartBounds;
279         private final Rect mEndBounds;
280         private final RectEvaluator mRectEvaluator;
281 
VisualIndicatorAnimator(View view, Rect startBounds, Rect endBounds)282         private VisualIndicatorAnimator(View view, Rect startBounds,
283                 Rect endBounds) {
284             mView = view;
285             mStartBounds = new Rect(startBounds);
286             mEndBounds = endBounds;
287             setFloatValues(0, 1);
288             mRectEvaluator = new RectEvaluator(new Rect());
289         }
290 
291         /**
292          * Create animator for visual indicator of fullscreen transition
293          *
294          * @param view the view for this indicator
295          * @param displayLayout information about the display the transitioning task is currently on
296          */
toFullscreenAnimatorWithAnimatedBounds( @onNull View view, @NonNull DisplayLayout displayLayout)297         public static VisualIndicatorAnimator toFullscreenAnimatorWithAnimatedBounds(
298                 @NonNull View view, @NonNull DisplayLayout displayLayout) {
299             final int padding = displayLayout.stableInsets().top;
300             Rect startBounds = new Rect(padding, padding,
301                     displayLayout.width() - padding, displayLayout.height() - padding);
302             view.getBackground().setBounds(startBounds);
303 
304             final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
305                     view, startBounds, getMaxBounds(startBounds));
306             animator.setInterpolator(new DecelerateInterpolator());
307             setupIndicatorAnimation(animator);
308             return animator;
309         }
310 
animateBounds( @onNull View view, int type, @NonNull DisplayLayout displayLayout)311         public static VisualIndicatorAnimator animateBounds(
312                 @NonNull View view, int type, @NonNull DisplayLayout displayLayout) {
313             final int padding = displayLayout.stableInsets().top;
314             Rect startBounds = new Rect();
315             switch (type) {
316                 case TO_FULLSCREEN_INDICATOR:
317                     startBounds.set(padding, padding,
318                             displayLayout.width() - padding,
319                             displayLayout.height() - padding);
320                     break;
321                 case TO_SPLIT_LEFT_INDICATOR:
322                     startBounds.set(padding, padding,
323                             displayLayout.width() / 2 - padding,
324                             displayLayout.height() - padding);
325                     break;
326                 case TO_SPLIT_RIGHT_INDICATOR:
327                     startBounds.set(displayLayout.width() / 2 + padding, padding,
328                             displayLayout.width() - padding,
329                             displayLayout.height() - padding);
330                     break;
331             }
332             view.getBackground().setBounds(startBounds);
333 
334             final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
335                     view, startBounds, getMaxBounds(startBounds));
336             animator.setInterpolator(new DecelerateInterpolator());
337             setupIndicatorAnimation(animator);
338             return animator;
339         }
340 
341         /**
342          * Create animator for visual indicator of freeform transition
343          *
344          * @param view the view for this indicator
345          * @param displayLayout information about the display the transitioning task is currently on
346          */
toFreeformAnimator(@onNull View view, @NonNull DisplayLayout displayLayout)347         public static VisualIndicatorAnimator toFreeformAnimator(@NonNull View view,
348                 @NonNull DisplayLayout displayLayout) {
349             final float adjustmentPercentage = 1f - FINAL_FREEFORM_SCALE;
350             final int width = displayLayout.width();
351             final int height = displayLayout.height();
352             Rect startBounds = new Rect(0, 0, width, height);
353             Rect endBounds = new Rect((int) (adjustmentPercentage * width / 2),
354                     (int) (adjustmentPercentage * height / 2),
355                     (int) (displayLayout.width() - (adjustmentPercentage * width / 2)),
356                     (int) (displayLayout.height() - (adjustmentPercentage * height / 2)));
357             final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
358                     view, startBounds, endBounds);
359             animator.setInterpolator(new DecelerateInterpolator());
360             setupIndicatorAnimation(animator);
361             return animator;
362         }
363 
364         /**
365          * Add necessary listener for animation of indicator
366          */
setupIndicatorAnimation(@onNull VisualIndicatorAnimator animator)367         private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator) {
368             animator.addUpdateListener(a -> {
369                 if (animator.mView != null) {
370                     animator.updateBounds(a.getAnimatedFraction(), animator.mView);
371                     animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView);
372                 } else {
373                     animator.cancel();
374                 }
375             });
376             animator.addListener(new AnimatorListenerAdapter() {
377                 @Override
378                 public void onAnimationEnd(Animator animation) {
379                     animator.mView.getBackground().setBounds(animator.mEndBounds);
380                 }
381             });
382             animator.setDuration(FULLSCREEN_INDICATOR_DURATION);
383         }
384 
385         /**
386          * Update bounds of view based on current animation fraction.
387          * Use of delta is to animate bounds independently, in case we need to
388          * run multiple animations simultaneously.
389          *
390          * @param fraction fraction to use, compared against previous fraction
391          * @param view     the view to update
392          */
updateBounds(float fraction, View view)393         private void updateBounds(float fraction, View view) {
394             if (mStartBounds.equals(mEndBounds)) {
395                 return;
396             }
397             Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds);
398             view.getBackground().setBounds(currentBounds);
399         }
400 
401         /**
402          * Fade in the fullscreen indicator
403          *
404          * @param fraction current animation fraction
405          */
updateIndicatorAlpha(float fraction, View view)406         private void updateIndicatorAlpha(float fraction, View view) {
407             view.setAlpha(fraction * INDICATOR_FINAL_OPACITY);
408         }
409 
410         /**
411          * Return the max bounds of a visual indicator
412          */
getMaxBounds(Rect startBounds)413         private static Rect getMaxBounds(Rect startBounds) {
414             return new Rect((int) (startBounds.left
415                             - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
416                     (int) (startBounds.top
417                             - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())),
418                     (int) (startBounds.right
419                             + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
420                     (int) (startBounds.bottom
421                             + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())));
422         }
423     }
424 }
425