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