• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.common.split;
18 
19 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
20 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
21 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
22 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
23 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ValueAnimator;
28 import android.app.ActivityManager;
29 import android.content.Context;
30 import android.content.res.Configuration;
31 import android.graphics.Color;
32 import android.graphics.PixelFormat;
33 import android.graphics.Rect;
34 import android.graphics.drawable.Drawable;
35 import android.os.Binder;
36 import android.view.IWindow;
37 import android.view.LayoutInflater;
38 import android.view.SurfaceControl;
39 import android.view.SurfaceControlViewHost;
40 import android.view.SurfaceSession;
41 import android.view.View;
42 import android.view.WindowManager;
43 import android.view.WindowlessWindowManager;
44 import android.widget.FrameLayout;
45 import android.widget.ImageView;
46 
47 import androidx.annotation.NonNull;
48 
49 import com.android.launcher3.icons.IconProvider;
50 import com.android.wm.shell.R;
51 import com.android.wm.shell.common.ScreenshotUtils;
52 import com.android.wm.shell.common.SurfaceUtils;
53 
54 /**
55  * Handles split decor like showing resizing hint for a specific split.
56  */
57 public class SplitDecorManager extends WindowlessWindowManager {
58     private static final String TAG = SplitDecorManager.class.getSimpleName();
59     private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground";
60     private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
61     private static final long FADE_DURATION = 133;
62 
63     private final IconProvider mIconProvider;
64     private final SurfaceSession mSurfaceSession;
65 
66     private Drawable mIcon;
67     private ImageView mResizingIconView;
68     private SurfaceControlViewHost mViewHost;
69     private SurfaceControl mHostLeash;
70     private SurfaceControl mIconLeash;
71     private SurfaceControl mBackgroundLeash;
72     private SurfaceControl mGapBackgroundLeash;
73     private SurfaceControl mScreenshot;
74 
75     private boolean mShown;
76     private boolean mIsResizing;
77     private final Rect mOldBounds = new Rect();
78     private final Rect mResizingBounds = new Rect();
79     private final Rect mTempRect = new Rect();
80     private ValueAnimator mFadeAnimator;
81     private ValueAnimator mScreenshotAnimator;
82 
83     private int mIconSize;
84     private int mOffsetX;
85     private int mOffsetY;
86     private int mRunningAnimationCount = 0;
87 
SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession)88     public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
89             SurfaceSession surfaceSession) {
90         super(configuration, null /* rootSurface */, null /* hostInputToken */);
91         mIconProvider = iconProvider;
92         mSurfaceSession = surfaceSession;
93     }
94 
95     @Override
attachToParentSurface(IWindow window, SurfaceControl.Builder b)96     protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
97         // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
98         final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
99                 .setContainerLayer()
100                 .setName(TAG)
101                 .setHidden(true)
102                 .setParent(mHostLeash)
103                 .setCallsite("SplitDecorManager#attachToParentSurface");
104         mIconLeash = builder.build();
105         b.setParent(mIconLeash);
106     }
107 
108     /** Inflates split decor surface on the root surface. */
inflate(Context context, SurfaceControl rootLeash, Rect rootBounds)109     public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) {
110         if (mIconLeash != null && mViewHost != null) {
111             return;
112         }
113 
114         context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
115                 null /* options */);
116         mHostLeash = rootLeash;
117         mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this);
118 
119         mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size);
120         final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context)
121                 .inflate(R.layout.split_decor, null);
122         mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon);
123 
124         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
125                 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
126                 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
127         lp.width = rootBounds.width();
128         lp.height = rootBounds.height();
129         lp.token = new Binder();
130         lp.setTitle(TAG);
131         lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
132         // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
133         //  TRUSTED_OVERLAY for windowless window without input channel.
134         mViewHost.setView(rootLayout, lp);
135     }
136 
137     /** Releases the surfaces for split decor. */
release(SurfaceControl.Transaction t)138     public void release(SurfaceControl.Transaction t) {
139         if (mFadeAnimator != null) {
140             if (mFadeAnimator.isRunning()) {
141                 mFadeAnimator.cancel();
142             }
143             mFadeAnimator = null;
144         }
145         if (mScreenshotAnimator != null) {
146             if (mScreenshotAnimator.isRunning()) {
147                 mScreenshotAnimator.cancel();
148             }
149             mScreenshotAnimator = null;
150         }
151         if (mViewHost != null) {
152             mViewHost.release();
153             mViewHost = null;
154         }
155         if (mIconLeash != null) {
156             t.remove(mIconLeash);
157             mIconLeash = null;
158         }
159         if (mBackgroundLeash != null) {
160             t.remove(mBackgroundLeash);
161             mBackgroundLeash = null;
162         }
163         if (mGapBackgroundLeash != null) {
164             t.remove(mGapBackgroundLeash);
165             mGapBackgroundLeash = null;
166         }
167         if (mScreenshot != null) {
168             t.remove(mScreenshot);
169             mScreenshot = null;
170         }
171         mHostLeash = null;
172         mIcon = null;
173         mResizingIconView = null;
174         mIsResizing = false;
175         mShown = false;
176         mOldBounds.setEmpty();
177         mResizingBounds.setEmpty();
178     }
179 
180     /** Showing resizing hint. */
onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, boolean immediately)181     public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
182             Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
183             boolean immediately) {
184         if (mResizingIconView == null) {
185             return;
186         }
187 
188         if (!mIsResizing) {
189             mIsResizing = true;
190             mOldBounds.set(newBounds);
191         }
192         mResizingBounds.set(newBounds);
193         mOffsetX = offsetX;
194         mOffsetY = offsetY;
195 
196         final boolean show =
197                 newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height();
198         final boolean update = show != mShown;
199         if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) {
200             // If we need to animate and animator still running, cancel it before we ensure both
201             // background and icon surfaces are non null for next animation.
202             mFadeAnimator.cancel();
203         }
204 
205         if (mBackgroundLeash == null) {
206             mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
207                     RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
208             t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
209                     .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
210         }
211 
212         if (mGapBackgroundLeash == null && !immediately) {
213             final boolean isLandscape = newBounds.height() == sideBounds.height();
214             final int left = isLandscape ? mOldBounds.width() : 0;
215             final int top = isLandscape ? 0 : mOldBounds.height();
216             mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
217                     GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
218             // Fill up another side bounds area.
219             t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
220                     .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
221                     .setPosition(mGapBackgroundLeash, left, top)
222                     .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height());
223         }
224 
225         if (mIcon == null && resizingTask.topActivityInfo != null) {
226             mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo);
227             mResizingIconView.setImageDrawable(mIcon);
228             mResizingIconView.setVisibility(View.VISIBLE);
229 
230             WindowManager.LayoutParams lp =
231                     (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
232             lp.width = mIconSize;
233             lp.height = mIconSize;
234             mViewHost.relayout(lp);
235             t.setLayer(mIconLeash, Integer.MAX_VALUE);
236         }
237         t.setPosition(mIconLeash,
238                 newBounds.width() / 2 - mIconSize / 2,
239                 newBounds.height() / 2 - mIconSize / 2);
240 
241         if (update) {
242             if (immediately) {
243                 t.setVisibility(mBackgroundLeash, show);
244                 t.setVisibility(mIconLeash, show);
245             } else {
246                 startFadeAnimation(show, false, null);
247             }
248             mShown = show;
249         }
250     }
251 
252     /** Stops showing resizing hint. */
onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback)253     public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
254         if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
255             mScreenshotAnimator.cancel();
256         }
257 
258         if (mScreenshot != null) {
259             t.setPosition(mScreenshot, mOffsetX, mOffsetY);
260 
261             final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
262             mScreenshotAnimator = ValueAnimator.ofFloat(1, 0);
263             mScreenshotAnimator.addUpdateListener(valueAnimator -> {
264                 final float progress = (float) valueAnimator.getAnimatedValue();
265                 animT.setAlpha(mScreenshot, progress);
266                 animT.apply();
267             });
268             mScreenshotAnimator.addListener(new AnimatorListenerAdapter() {
269                 @Override
270                 public void onAnimationStart(Animator animation) {
271                     mRunningAnimationCount++;
272                 }
273 
274                 @Override
275                 public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
276                     mRunningAnimationCount--;
277                     animT.remove(mScreenshot);
278                     animT.apply();
279                     animT.close();
280                     mScreenshot = null;
281 
282                     if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
283                         animFinishedCallback.run();
284                     }
285                 }
286             });
287             mScreenshotAnimator.start();
288         }
289 
290         if (mResizingIconView == null) {
291             return;
292         }
293 
294         mIsResizing = false;
295         mOffsetX = 0;
296         mOffsetY = 0;
297         mOldBounds.setEmpty();
298         mResizingBounds.setEmpty();
299         if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
300             if (!mShown) {
301                 // If fade-out animation is running, just add release callback to it.
302                 SurfaceControl.Transaction finishT = new SurfaceControl.Transaction();
303                 mFadeAnimator.addListener(new AnimatorListenerAdapter() {
304                     @Override
305                     public void onAnimationEnd(Animator animation) {
306                         releaseDecor(finishT);
307                         finishT.apply();
308                         finishT.close();
309                     }
310                 });
311                 return;
312             }
313         }
314         if (mShown) {
315             fadeOutDecor(animFinishedCallback);
316         } else {
317             // Decor surface is hidden so release it directly.
318             releaseDecor(t);
319             if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
320                 animFinishedCallback.run();
321             }
322         }
323     }
324 
325     /** Screenshot host leash and attach on it if meet some conditions */
screenshotIfNeeded(SurfaceControl.Transaction t)326     public void screenshotIfNeeded(SurfaceControl.Transaction t) {
327         if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
328             if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
329                 mScreenshotAnimator.cancel();
330             } else if (mScreenshot != null) {
331                 t.remove(mScreenshot);
332             }
333 
334             mTempRect.set(mOldBounds);
335             mTempRect.offsetTo(0, 0);
336             mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect,
337                     Integer.MAX_VALUE - 1);
338         }
339     }
340 
341     /** Set screenshot and attach on host leash it if meet some conditions */
setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t)342     public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
343         if (screenshot == null || !screenshot.isValid()) return;
344 
345         if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
346             if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
347                 mScreenshotAnimator.cancel();
348             } else if (mScreenshot != null) {
349                 t.remove(mScreenshot);
350             }
351 
352             mScreenshot = screenshot;
353             t.reparent(screenshot, mHostLeash);
354             t.setLayer(screenshot, Integer.MAX_VALUE - 1);
355         }
356     }
357 
358     /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback
359      * directly. */
fadeOutDecor(Runnable finishedCallback)360     public void fadeOutDecor(Runnable finishedCallback) {
361         if (mShown) {
362             // If previous animation is running, just cancel it.
363             if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
364                 mFadeAnimator.cancel();
365             }
366 
367             startFadeAnimation(false /* show */, true, finishedCallback);
368             mShown = false;
369         } else {
370             if (finishedCallback != null) finishedCallback.run();
371         }
372     }
373 
startFadeAnimation(boolean show, boolean releaseSurface, Runnable finishedCallback)374     private void startFadeAnimation(boolean show, boolean releaseSurface,
375             Runnable finishedCallback) {
376         final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
377         mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
378         mFadeAnimator.setDuration(FADE_DURATION);
379         mFadeAnimator.addUpdateListener(valueAnimator-> {
380             final float progress = (float) valueAnimator.getAnimatedValue();
381             if (mBackgroundLeash != null) {
382                 animT.setAlpha(mBackgroundLeash, show ? progress : 1 - progress);
383             }
384             if (mIconLeash != null) {
385                 animT.setAlpha(mIconLeash, show ? progress : 1 - progress);
386             }
387             animT.apply();
388         });
389         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
390             @Override
391             public void onAnimationStart(@NonNull Animator animation) {
392                 mRunningAnimationCount++;
393                 if (show) {
394                     animT.show(mBackgroundLeash).show(mIconLeash);
395                 }
396                 if (mGapBackgroundLeash != null) {
397                     animT.setVisibility(mGapBackgroundLeash, show);
398                 }
399                 animT.apply();
400             }
401 
402             @Override
403             public void onAnimationEnd(@NonNull Animator animation) {
404                 mRunningAnimationCount--;
405                 if (!show) {
406                     if (mBackgroundLeash != null) {
407                         animT.hide(mBackgroundLeash);
408                     }
409                     if (mIconLeash != null) {
410                         animT.hide(mIconLeash);
411                     }
412                 }
413                 if (releaseSurface) {
414                     releaseDecor(animT);
415                 }
416                 animT.apply();
417                 animT.close();
418 
419                 if (mRunningAnimationCount == 0 && finishedCallback != null) {
420                     finishedCallback.run();
421                 }
422             }
423         });
424         mFadeAnimator.start();
425     }
426 
427     /** Release or hide decor hint. */
releaseDecor(SurfaceControl.Transaction t)428     private void releaseDecor(SurfaceControl.Transaction t) {
429         if (mBackgroundLeash != null) {
430             t.remove(mBackgroundLeash);
431             mBackgroundLeash = null;
432         }
433 
434         if (mGapBackgroundLeash != null) {
435             t.remove(mGapBackgroundLeash);
436             mGapBackgroundLeash = null;
437         }
438 
439         if (mIcon != null) {
440             mResizingIconView.setVisibility(View.GONE);
441             mResizingIconView.setImageDrawable(null);
442             t.hide(mIconLeash);
443             mIcon = null;
444         }
445     }
446 
getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo)447     private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
448         final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
449         return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents();
450     }
451 }
452