1 /*
2  * Copyright (C) 2020 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.startingsurface;
18 
19 import static android.content.Context.CONTEXT_RESTRICTED;
20 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
21 import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION;
22 import static android.view.Display.DEFAULT_DISPLAY;
23 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
24 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT;
25 
26 import android.annotation.Nullable;
27 import android.app.ActivityManager.RunningTaskInfo;
28 import android.app.ActivityTaskManager;
29 import android.app.ActivityThread;
30 import android.app.TaskInfo;
31 import android.content.Context;
32 import android.content.pm.ActivityInfo;
33 import android.content.pm.IPackageManager;
34 import android.content.pm.PackageManager;
35 import android.content.res.Configuration;
36 import android.content.res.Resources;
37 import android.content.res.TypedArray;
38 import android.graphics.Color;
39 import android.graphics.PixelFormat;
40 import android.hardware.display.DisplayManager;
41 import android.os.IBinder;
42 import android.os.RemoteCallback;
43 import android.os.RemoteException;
44 import android.os.Trace;
45 import android.os.UserHandle;
46 import android.util.Slog;
47 import android.util.SparseArray;
48 import android.view.Choreographer;
49 import android.view.Display;
50 import android.view.SurfaceControlViewHost;
51 import android.view.View;
52 import android.view.WindowManager;
53 import android.view.WindowManagerGlobal;
54 import android.widget.FrameLayout;
55 import android.window.SplashScreenView;
56 import android.window.SplashScreenView.SplashScreenViewParcelable;
57 import android.window.StartingWindowInfo;
58 import android.window.StartingWindowInfo.StartingWindowType;
59 import android.window.StartingWindowRemovalInfo;
60 import android.window.TaskSnapshot;
61 
62 import com.android.internal.R;
63 import com.android.internal.annotations.VisibleForTesting;
64 import com.android.launcher3.icons.IconProvider;
65 import com.android.wm.shell.common.ShellExecutor;
66 import com.android.wm.shell.common.TransactionPool;
67 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
68 
69 import java.util.function.Supplier;
70 
71 /**
72  * A class which able to draw splash screen or snapshot as the starting window for a task.
73  *
74  * In order to speed up, there will use two threads to creating a splash screen in parallel.
75  * Right now we are still using PhoneWindow to create splash screen window, so the view is added to
76  * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call
77  * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view
78  * can synchronize on each frame.
79  *
80  * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing
81  * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background
82  * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after
83  * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very
84  * quickly.
85  *
86  * So basically we are using the spare time to prepare the SplashScreenView while splash screen
87  * thread is waiting for
88  * 1. WindowManager#addView(binder call to WM),
89  * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device),
90  * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will
91  * always happen before #draw).
92  * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on
93  * splash-screen background tread can make they execute in parallel, which ensure it is faster then
94  * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame.
95  *
96  * Here is the sequence to compare the difference between using single and two thread.
97  *
98  * Single thread:
99  * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout
100  * -> draw -> AdaptiveIconDrawable#draw
101  *
102  * Two threads:
103  * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw)
104  * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint
105  * directly).
106  */
107 @ShellSplashscreenThread
108 public class StartingSurfaceDrawer {
109     static final String TAG = StartingSurfaceDrawer.class.getSimpleName();
110     static final boolean DEBUG_SPLASH_SCREEN = StartingWindowController.DEBUG_SPLASH_SCREEN;
111     static final boolean DEBUG_TASK_SNAPSHOT = StartingWindowController.DEBUG_TASK_SNAPSHOT;
112 
113     private final Context mContext;
114     private final DisplayManager mDisplayManager;
115     private final ShellExecutor mSplashScreenExecutor;
116     @VisibleForTesting
117     final SplashscreenContentDrawer mSplashscreenContentDrawer;
118     private Choreographer mChoreographer;
119     private final WindowManagerGlobal mWindowManagerGlobal;
120     private StartingSurface.SysuiProxy mSysuiProxy;
121     private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
122 
123     /**
124      * @param splashScreenExecutor The thread used to control add and remove starting window.
125      */
StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, IconProvider iconProvider, TransactionPool pool)126     public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
127             IconProvider iconProvider, TransactionPool pool) {
128         mContext = context;
129         mDisplayManager = mContext.getSystemService(DisplayManager.class);
130         mSplashScreenExecutor = splashScreenExecutor;
131         mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool);
132         mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
133         mWindowManagerGlobal = WindowManagerGlobal.getInstance();
134         mDisplayManager.getDisplay(DEFAULT_DISPLAY);
135     }
136 
137     @VisibleForTesting
138     final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>();
139 
140     /**
141      * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is
142      * rendered and that have not yet been removed by their client.
143      */
144     private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts =
145             new SparseArray<>(1);
146 
getDisplay(int displayId)147     private Display getDisplay(int displayId) {
148         return mDisplayManager.getDisplay(displayId);
149     }
150 
getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo)151     int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) {
152         return splashScreenThemeResId != 0
153                 ? splashScreenThemeResId
154                 : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
155                         : com.android.internal.R.style.Theme_DeviceDefault_DayNight;
156     }
157 
setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy)158     void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) {
159         mSysuiProxy = sysuiProxy;
160     }
161 
162     /**
163      * Called when a task need a splash screen starting window.
164      *
165      * @param suggestType The suggestion type to draw the splash screen.
166      */
addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken, @StartingWindowType int suggestType)167     void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
168             @StartingWindowType int suggestType) {
169         final RunningTaskInfo taskInfo = windowInfo.taskInfo;
170         final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
171                 ? windowInfo.targetActivityInfo
172                 : taskInfo.topActivityInfo;
173         if (activityInfo == null || activityInfo.packageName == null) {
174             return;
175         }
176 
177         final int displayId = taskInfo.displayId;
178         final int taskId = taskInfo.taskId;
179 
180         // replace with the default theme if the application didn't set
181         final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
182         if (DEBUG_SPLASH_SCREEN) {
183             Slog.d(TAG, "addSplashScreen " + activityInfo.packageName
184                     + " theme=" + Integer.toHexString(theme) + " task=" + taskInfo.taskId
185                     + " suggestType=" + suggestType);
186         }
187         final Display display = getDisplay(displayId);
188         if (display == null) {
189             // Can't show splash screen on requested display, so skip showing at all.
190             return;
191         }
192         Context context = displayId == DEFAULT_DISPLAY
193                 ? mContext : mContext.createDisplayContext(display);
194         if (context == null) {
195             return;
196         }
197         if (theme != context.getThemeResId()) {
198             try {
199                 context = context.createPackageContextAsUser(activityInfo.packageName,
200                         CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId));
201                 context.setTheme(theme);
202             } catch (PackageManager.NameNotFoundException e) {
203                 Slog.w(TAG, "Failed creating package context with package name "
204                         + activityInfo.packageName + " for user " + taskInfo.userId, e);
205                 return;
206             }
207         }
208 
209         final Configuration taskConfig = taskInfo.getConfiguration();
210         if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) {
211             if (DEBUG_SPLASH_SCREEN) {
212                 Slog.d(TAG, "addSplashScreen: creating context based"
213                         + " on task Configuration " + taskConfig + " for splash screen");
214             }
215             final Context overrideContext = context.createConfigurationContext(taskConfig);
216             overrideContext.setTheme(theme);
217             final TypedArray typedArray = overrideContext.obtainStyledAttributes(
218                     com.android.internal.R.styleable.Window);
219             final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
220             try {
221                 if (resId != 0 && overrideContext.getDrawable(resId) != null) {
222                     // We want to use the windowBackground for the override context if it is
223                     // available, otherwise we use the default one to make sure a themed starting
224                     // window is displayed for the app.
225                     if (DEBUG_SPLASH_SCREEN) {
226                         Slog.d(TAG, "addSplashScreen: apply overrideConfig"
227                                 + taskConfig + " to starting window resId=" + resId);
228                     }
229                     context = overrideContext;
230                 }
231             } catch (Resources.NotFoundException e) {
232                 Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: "
233                         + taskId, e);
234                 return;
235             }
236             typedArray.recycle();
237         }
238 
239         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
240                 WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
241         params.setFitInsetsSides(0);
242         params.setFitInsetsTypes(0);
243         params.format = PixelFormat.TRANSLUCENT;
244         int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
245                 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
246                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
247         final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
248         if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
249             windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
250         }
251         if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
252             if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) {
253                 windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
254             }
255         } else {
256             windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
257         }
258         params.layoutInDisplayCutoutMode = a.getInt(
259                 R.styleable.Window_windowLayoutInDisplayCutoutMode,
260                 params.layoutInDisplayCutoutMode);
261         params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
262         a.recycle();
263 
264         // Assumes it's safe to show starting windows of launched apps while
265         // the keyguard is being hidden. This is okay because starting windows never show
266         // secret information.
267         // TODO(b/113840485): Occluded may not only happen on default display
268         if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) {
269             windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
270         }
271 
272         // Force the window flags: this is a fake window, so it is not really
273         // touchable or focusable by the user.  We also add in the ALT_FOCUSABLE_IM
274         // flag because we do know that the next window will take input
275         // focus, so we want to get the IME window up on top of us right away.
276         windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
277                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
278                 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
279         params.flags = windowFlags;
280         params.token = appToken;
281         params.packageName = activityInfo.packageName;
282         params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
283         // Setting as trusted overlay to let touches pass through. This is safe because this
284         // window is controlled by the system.
285         params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
286 
287         if (!context.getResources().getCompatibilityInfo().supportsScreen()) {
288             params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
289         }
290 
291         params.setTitle("Splash Screen " + activityInfo.packageName);
292 
293         // TODO(b/173975965) tracking performance
294         // Prepare the splash screen content view on splash screen worker thread in parallel, so the
295         // content view won't be blocked by binder call like addWindow and relayout.
296         // 1. Trigger splash screen worker thread to create SplashScreenView before/while
297         // Session#addWindow.
298         // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
299         // traversal, which will call Session#relayout on splash screen thread.
300         // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at
301         // the same time the splash screen thread should be executing Session#relayout. Blocking the
302         // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.
303 
304         // Record whether create splash screen view success, notify to current thread after
305         // create splash screen view finished.
306         final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
307         final FrameLayout rootLayout = new FrameLayout(
308                 mSplashscreenContentDrawer.createViewContextWrapper(context));
309         rootLayout.setPadding(0, 0, 0, 0);
310         rootLayout.setFitsSystemWindows(false);
311         final Runnable setViewSynchronized = () -> {
312             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
313             // waiting for setContentView before relayoutWindow
314             SplashScreenView contentView = viewSupplier.get();
315             final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
316             // If record == null, either the starting window added fail or removed already.
317             // Do not add this view if the token is mismatch.
318             if (record != null && appToken == record.mAppToken) {
319                 // if view == null then creation of content view was failed.
320                 if (contentView != null) {
321                     try {
322                         rootLayout.addView(contentView);
323                     } catch (RuntimeException e) {
324                         Slog.w(TAG, "failed set content view to starting window "
325                                 + "at taskId: " + taskId, e);
326                         contentView = null;
327                     }
328                 }
329                 record.setSplashScreenView(contentView);
330             }
331             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
332         };
333         if (mSysuiProxy != null) {
334             mSysuiProxy.requestTopUi(true, TAG);
335         }
336         mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
337                 viewSupplier::setView, viewSupplier::setUiThreadInitTask);
338         try {
339             if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
340                 // We use the splash screen worker thread to create SplashScreenView while adding
341                 // the window, as otherwise Choreographer#doFrame might be delayed on this thread.
342                 // And since Choreographer#doFrame won't happen immediately after adding the window,
343                 // if the view is not added to the PhoneWindow on the first #doFrame, the view will
344                 // not be rendered on the first frame. So here we need to synchronize the view on
345                 // the window before first round relayoutWindow, which will happen after insets
346                 // animation.
347                 mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);
348                 // Block until we get the background color.
349                 final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
350                 final SplashScreenView contentView = viewSupplier.get();
351                 record.mBGColor = contentView.getInitBackgroundColor();
352             }
353         } catch (RuntimeException e) {
354             // don't crash if something else bad happens, for example a
355             // failure loading resources because we are loading from an app
356             // on external storage that has been unmounted.
357             Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e);
358         }
359     }
360 
getStartingWindowBackgroundColorForTask(int taskId)361     int getStartingWindowBackgroundColorForTask(int taskId) {
362         final StartingWindowRecord startingWindowRecord = mStartingWindowRecords.get(taskId);
363         if (startingWindowRecord == null) {
364             return Color.TRANSPARENT;
365         }
366         return startingWindowRecord.mBGColor;
367     }
368 
369     private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> {
370         private SplashScreenView mView;
371         private boolean mIsViewSet;
372         private Runnable mUiThreadInitTask;
setView(SplashScreenView view)373         void setView(SplashScreenView view) {
374             synchronized (this) {
375                 mView = view;
376                 mIsViewSet = true;
377                 notify();
378             }
379         }
380 
setUiThreadInitTask(Runnable initTask)381         void setUiThreadInitTask(Runnable initTask) {
382             synchronized (this) {
383                 mUiThreadInitTask = initTask;
384             }
385         }
386 
387         @Override
get()388         public @Nullable SplashScreenView get() {
389             synchronized (this) {
390                 while (!mIsViewSet) {
391                     try {
392                         wait();
393                     } catch (InterruptedException ignored) {
394                     }
395                 }
396                 if (mUiThreadInitTask != null) {
397                     mUiThreadInitTask.run();
398                     mUiThreadInitTask = null;
399                 }
400                 return mView;
401             }
402         }
403     }
404 
estimateTaskBackgroundColor(TaskInfo taskInfo)405     int estimateTaskBackgroundColor(TaskInfo taskInfo) {
406         if (taskInfo.topActivityInfo == null) {
407             return Color.TRANSPARENT;
408         }
409         final ActivityInfo activityInfo = taskInfo.topActivityInfo;
410         final String packageName = activityInfo.packageName;
411         final int userId = taskInfo.userId;
412         final Context windowContext;
413         try {
414             windowContext = mContext.createPackageContextAsUser(
415                     packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId));
416         } catch (PackageManager.NameNotFoundException e) {
417             Slog.w(TAG, "Failed creating package context with package name "
418                     + packageName + " for user " + taskInfo.userId, e);
419             return Color.TRANSPARENT;
420         }
421         try {
422             final IPackageManager packageManager = ActivityThread.getPackageManager();
423             final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName,
424                     userId);
425             final int splashScreenThemeId = splashScreenThemeName != null
426                     ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null)
427                     : 0;
428 
429             final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo);
430 
431             if (theme != windowContext.getThemeResId()) {
432                 windowContext.setTheme(theme);
433             }
434             return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext);
435         } catch (RuntimeException | RemoteException e) {
436             Slog.w(TAG, "failed get starting window background color at taskId: "
437                     + taskInfo.taskId, e);
438         }
439         return Color.TRANSPARENT;
440     }
441 
442     /**
443      * Called when a task need a snapshot starting window.
444      */
makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken, TaskSnapshot snapshot)445     void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken,
446             TaskSnapshot snapshot) {
447         final int taskId = startingWindowInfo.taskInfo.taskId;
448         // Remove any existing starting window for this task before adding.
449         removeWindowNoAnimate(taskId);
450         final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken,
451                 snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId));
452         if (surface == null) {
453             return;
454         }
455         final StartingWindowRecord tView = new StartingWindowRecord(appToken,
456                 null/* decorView */, surface, STARTING_WINDOW_TYPE_SNAPSHOT);
457         mStartingWindowRecords.put(taskId, tView);
458     }
459 
460     /**
461      * Called when the content of a task is ready to show, starting window can be removed.
462      */
removeStartingWindow(StartingWindowRemovalInfo removalInfo)463     public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
464         if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
465             Slog.d(TAG, "Task start finish, remove starting surface for task "
466                     + removalInfo.taskId);
467         }
468         removeWindowSynced(removalInfo, false /* immediately */);
469     }
470 
471     /**
472      * Clear all starting windows immediately.
473      */
clearAllWindows()474     public void clearAllWindows() {
475         if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
476             Slog.d(TAG, "Clear all starting windows immediately");
477         }
478         final int taskSize = mStartingWindowRecords.size();
479         final int[] taskIds = new int[taskSize];
480         for (int i = taskSize - 1; i >= 0; --i) {
481             taskIds[i] = mStartingWindowRecords.keyAt(i);
482         }
483         for (int i = taskSize - 1; i >= 0; --i) {
484             removeWindowNoAnimate(taskIds[i]);
485         }
486     }
487 
488     /**
489      * Called when the Task wants to copy the splash screen.
490      */
copySplashScreenView(int taskId)491     public void copySplashScreenView(int taskId) {
492         final StartingWindowRecord preView = mStartingWindowRecords.get(taskId);
493         SplashScreenViewParcelable parcelable;
494         SplashScreenView splashScreenView = preView != null ? preView.mContentView : null;
495         if (splashScreenView != null && splashScreenView.isCopyable()) {
496             parcelable = new SplashScreenViewParcelable(splashScreenView);
497             parcelable.setClientCallback(
498                     new RemoteCallback((bundle) -> mSplashScreenExecutor.execute(
499                             () -> onAppSplashScreenViewRemoved(taskId, false))));
500             splashScreenView.onCopied();
501             mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost());
502         } else {
503             parcelable = null;
504         }
505         if (DEBUG_SPLASH_SCREEN) {
506             Slog.v(TAG, "Copying splash screen window view for task: " + taskId
507                     + " parcelable: " + parcelable);
508         }
509         ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable);
510     }
511 
512     /**
513      * Called when the {@link SplashScreenView} is removed from the client Activity view's hierarchy
514      * or when the Activity is clean up.
515      *
516      * @param taskId The Task id on which the splash screen was attached
517      */
onAppSplashScreenViewRemoved(int taskId)518     public void onAppSplashScreenViewRemoved(int taskId) {
519         onAppSplashScreenViewRemoved(taskId, true /* fromServer */);
520     }
521 
522     /**
523      * @param fromServer If true, this means the removal was notified by the server. This is only
524      *                   used for debugging purposes.
525      * @see #onAppSplashScreenViewRemoved(int)
526      */
onAppSplashScreenViewRemoved(int taskId, boolean fromServer)527     private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) {
528         SurfaceControlViewHost viewHost =
529                 mAnimatedSplashScreenSurfaceHosts.get(taskId);
530         if (viewHost == null) {
531             return;
532         }
533         mAnimatedSplashScreenSurfaceHosts.remove(taskId);
534         if (DEBUG_SPLASH_SCREEN) {
535             String reason = fromServer ? "Server cleaned up" : "App removed";
536             Slog.v(TAG, reason + "the splash screen. Releasing SurfaceControlViewHost for task:"
537                     + taskId);
538         }
539         SplashScreenView.releaseIconHost(viewHost);
540     }
541 
addWindow(int taskId, IBinder appToken, View view, Display display, WindowManager.LayoutParams params, @StartingWindowType int suggestType)542     protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
543             WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
544         boolean shouldSaveView = true;
545         final Context context = view.getContext();
546         try {
547             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
548             mWindowManagerGlobal.addView(view, params, display,
549                     null /* parentWindow */, context.getUserId());
550         } catch (WindowManager.BadTokenException e) {
551             // ignore
552             Slog.w(TAG, appToken + " already running, starting window not displayed. "
553                     + e.getMessage());
554             shouldSaveView = false;
555         } finally {
556             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
557             if (view.getParent() == null) {
558                 Slog.w(TAG, "view not successfully added to wm, removing view");
559                 mWindowManagerGlobal.removeView(view, true /* immediate */);
560                 shouldSaveView = false;
561             }
562         }
563         if (shouldSaveView) {
564             removeWindowNoAnimate(taskId);
565             saveSplashScreenRecord(appToken, taskId, view, suggestType);
566         }
567         return shouldSaveView;
568     }
569 
570     @VisibleForTesting
saveSplashScreenRecord(IBinder appToken, int taskId, View view, @StartingWindowType int suggestType)571     void saveSplashScreenRecord(IBinder appToken, int taskId, View view,
572             @StartingWindowType int suggestType) {
573         final StartingWindowRecord tView = new StartingWindowRecord(appToken, view,
574                 null/* TaskSnapshotWindow */, suggestType);
575         mStartingWindowRecords.put(taskId, tView);
576     }
577 
removeWindowNoAnimate(int taskId)578     private void removeWindowNoAnimate(int taskId) {
579         mTmpRemovalInfo.taskId = taskId;
580         removeWindowSynced(mTmpRemovalInfo, true /* immediately */);
581     }
582 
onImeDrawnOnTask(int taskId)583     void onImeDrawnOnTask(int taskId) {
584         final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
585         if (record != null && record.mTaskSnapshotWindow != null
586                 && record.mTaskSnapshotWindow.hasImeSurface()) {
587             removeWindowNoAnimate(taskId);
588         }
589     }
590 
removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately)591     protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) {
592         final int taskId = removalInfo.taskId;
593         final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
594         if (record != null) {
595             if (record.mDecorView != null) {
596                 if (DEBUG_SPLASH_SCREEN) {
597                     Slog.v(TAG, "Removing splash screen window for task: " + taskId);
598                 }
599                 if (record.mContentView != null) {
600                     if (immediately
601                             || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
602                         removeWindowInner(record.mDecorView, false);
603                     } else {
604                         if (removalInfo.playRevealAnimation) {
605                             mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
606                                     removalInfo.windowAnimationLeash, removalInfo.mainFrame,
607                                     () -> removeWindowInner(record.mDecorView, true));
608                         } else {
609                             // the SplashScreenView has been copied to client, hide the view to skip
610                             // default exit animation
611                             removeWindowInner(record.mDecorView, true);
612                         }
613                     }
614                 } else {
615                     // shouldn't happen
616                     Slog.e(TAG, "Found empty splash screen, remove!");
617                     removeWindowInner(record.mDecorView, false);
618                 }
619                 mStartingWindowRecords.remove(taskId);
620             }
621             if (record.mTaskSnapshotWindow != null) {
622                 if (DEBUG_TASK_SNAPSHOT) {
623                     Slog.v(TAG, "Removing task snapshot window for " + taskId);
624                 }
625                 if (immediately) {
626                     record.mTaskSnapshotWindow.removeImmediately();
627                 } else {
628                     record.mTaskSnapshotWindow.scheduleRemove(() ->
629                             mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme);
630                 }
631             }
632         }
633     }
634 
removeWindowInner(View decorView, boolean hideView)635     private void removeWindowInner(View decorView, boolean hideView) {
636         if (mSysuiProxy != null) {
637             mSysuiProxy.requestTopUi(false, TAG);
638         }
639         if (hideView) {
640             decorView.setVisibility(View.GONE);
641         }
642         mWindowManagerGlobal.removeView(decorView, false /* immediate */);
643     }
644 
645     /**
646      * Record the view or surface for a starting window.
647      */
648     private static class StartingWindowRecord {
649         private final IBinder mAppToken;
650         private final View mDecorView;
651         private final TaskSnapshotWindow mTaskSnapshotWindow;
652         private SplashScreenView mContentView;
653         private boolean mSetSplashScreen;
654         private @StartingWindowType int mSuggestType;
655         private int mBGColor;
656 
StartingWindowRecord(IBinder appToken, View decorView, TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType)657         StartingWindowRecord(IBinder appToken, View decorView,
658                 TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) {
659             mAppToken = appToken;
660             mDecorView = decorView;
661             mTaskSnapshotWindow = taskSnapshotWindow;
662             if (mTaskSnapshotWindow != null) {
663                 mBGColor = mTaskSnapshotWindow.getBackgroundColor();
664             }
665             mSuggestType = suggestType;
666         }
667 
setSplashScreenView(SplashScreenView splashScreenView)668         private void setSplashScreenView(SplashScreenView splashScreenView) {
669             if (mSetSplashScreen) {
670                 return;
671             }
672             mContentView = splashScreenView;
673             mSetSplashScreen = true;
674         }
675     }
676 }
677