| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.wm.shell.startingsurface; |
| |
| import static android.content.Context.CONTEXT_RESTRICTED; |
| import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; |
| import static android.view.Choreographer.CALLBACK_INSETS_ANIMATION; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN; |
| import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT; |
| |
| import android.annotation.Nullable; |
| import android.app.ActivityManager.RunningTaskInfo; |
| import android.app.ActivityTaskManager; |
| import android.app.ActivityThread; |
| import android.app.TaskInfo; |
| import android.content.Context; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageManager; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Color; |
| import android.graphics.PixelFormat; |
| import android.hardware.display.DisplayManager; |
| import android.os.IBinder; |
| import android.os.RemoteCallback; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.Trace; |
| import android.os.UserHandle; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.view.Choreographer; |
| import android.view.Display; |
| import android.view.SurfaceControlViewHost; |
| import android.view.View; |
| import android.view.WindowInsetsController; |
| import android.view.WindowManager; |
| import android.view.WindowManagerGlobal; |
| import android.widget.FrameLayout; |
| import android.window.SplashScreenView; |
| import android.window.SplashScreenView.SplashScreenViewParcelable; |
| import android.window.StartingWindowInfo; |
| import android.window.StartingWindowInfo.StartingWindowType; |
| import android.window.StartingWindowRemovalInfo; |
| import android.window.TaskSnapshot; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.protolog.common.ProtoLog; |
| import com.android.internal.util.ContrastColorUtil; |
| import com.android.launcher3.icons.IconProvider; |
| import com.android.wm.shell.common.ShellExecutor; |
| import com.android.wm.shell.common.TransactionPool; |
| import com.android.wm.shell.common.annotations.ShellSplashscreenThread; |
| import com.android.wm.shell.protolog.ShellProtoLogGroup; |
| |
| import java.util.function.Supplier; |
| |
| /** |
| * A class which able to draw splash screen or snapshot as the starting window for a task. |
| * |
| * In order to speed up, there will use two threads to creating a splash screen in parallel. |
| * Right now we are still using PhoneWindow to create splash screen window, so the view is added to |
| * the ViewRootImpl, and those view won't be draw immediately because the ViewRootImpl will call |
| * scheduleTraversal to register a callback from Choreographer, so the drawing result of the view |
| * can synchronize on each frame. |
| * |
| * The bad thing is that we cannot decide when would Choreographer#doFrame happen, and drawing |
| * the AdaptiveIconDrawable object can be time consuming, so we use the splash-screen background |
| * thread to draw the AdaptiveIconDrawable object to a Bitmap and cache it to a BitmapShader after |
| * the SplashScreenView just created, once we get the BitmapShader then the #draw call can be very |
| * quickly. |
| * |
| * So basically we are using the spare time to prepare the SplashScreenView while splash screen |
| * thread is waiting for |
| * 1. WindowManager#addView(binder call to WM), |
| * 2. Choreographer#doFrame happen(uncertain time for next frame, depends on device), |
| * 3. Session#relayout(another binder call to WM which under Choreographer#doFrame, but will |
| * always happen before #draw). |
| * Because above steps are running on splash-screen thread, so pre-draw the BitmapShader on |
| * splash-screen background tread can make they execute in parallel, which ensure it is faster then |
| * to draw the AdaptiveIconDrawable when receive callback from Choreographer#doFrame. |
| * |
| * Here is the sequence to compare the difference between using single and two thread. |
| * |
| * Single thread: |
| * => makeSplashScreenContentView -> WM#addView .. waiting for Choreographer#doFrame -> relayout |
| * -> draw -> AdaptiveIconDrawable#draw |
| * |
| * Two threads: |
| * => makeSplashScreenContentView -> cachePaint(=AdaptiveIconDrawable#draw) |
| * => WM#addView -> .. waiting for Choreographer#doFrame -> relayout -> draw -> (draw the Paint |
| * directly). |
| */ |
| @ShellSplashscreenThread |
| public class StartingSurfaceDrawer { |
| private static final String TAG = StartingWindowController.TAG; |
| |
| private final Context mContext; |
| private final DisplayManager mDisplayManager; |
| private final ShellExecutor mSplashScreenExecutor; |
| @VisibleForTesting |
| final SplashscreenContentDrawer mSplashscreenContentDrawer; |
| private Choreographer mChoreographer; |
| private final WindowManagerGlobal mWindowManagerGlobal; |
| private StartingSurface.SysuiProxy mSysuiProxy; |
| private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); |
| |
| private static final int LIGHT_BARS_MASK = |
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS |
| | WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; |
| /** |
| * The minimum duration during which the splash screen is shown when the splash screen icon is |
| * animated. |
| */ |
| static final long MINIMAL_ANIMATION_DURATION = 400L; |
| |
| /** |
| * Allow the icon style splash screen to be displayed for longer to give time for the animation |
| * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly |
| * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration. |
| */ |
| static final long TIME_WINDOW_DURATION = 100L; |
| |
| /** |
| * The maximum duration during which the splash screen will be shown if the application is ready |
| * to show before the icon animation finishes. |
| */ |
| static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION; |
| |
| /** |
| * @param splashScreenExecutor The thread used to control add and remove starting window. |
| */ |
| public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, |
| IconProvider iconProvider, TransactionPool pool) { |
| mContext = context; |
| mDisplayManager = mContext.getSystemService(DisplayManager.class); |
| mSplashScreenExecutor = splashScreenExecutor; |
| mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); |
| mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); |
| mWindowManagerGlobal = WindowManagerGlobal.getInstance(); |
| mDisplayManager.getDisplay(DEFAULT_DISPLAY); |
| } |
| |
| @VisibleForTesting |
| final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); |
| |
| /** |
| * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is |
| * rendered and that have not yet been removed by their client. |
| */ |
| private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = |
| new SparseArray<>(1); |
| |
| private Display getDisplay(int displayId) { |
| return mDisplayManager.getDisplay(displayId); |
| } |
| |
| int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { |
| return splashScreenThemeResId != 0 |
| ? splashScreenThemeResId |
| : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() |
| : com.android.internal.R.style.Theme_DeviceDefault_DayNight; |
| } |
| |
| void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { |
| mSysuiProxy = sysuiProxy; |
| } |
| |
| /** |
| * Called when a task need a splash screen starting window. |
| * |
| * @param suggestType The suggestion type to draw the splash screen. |
| */ |
| void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken, |
| @StartingWindowType int suggestType) { |
| final RunningTaskInfo taskInfo = windowInfo.taskInfo; |
| final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null |
| ? windowInfo.targetActivityInfo |
| : taskInfo.topActivityInfo; |
| if (activityInfo == null || activityInfo.packageName == null) { |
| return; |
| } |
| |
| final int displayId = taskInfo.displayId; |
| final int taskId = taskInfo.taskId; |
| |
| // replace with the default theme if the application didn't set |
| final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d", |
| activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType); |
| final Display display = getDisplay(displayId); |
| if (display == null) { |
| // Can't show splash screen on requested display, so skip showing at all. |
| return; |
| } |
| Context context = displayId == DEFAULT_DISPLAY |
| ? mContext : mContext.createDisplayContext(display); |
| if (context == null) { |
| return; |
| } |
| if (theme != context.getThemeResId()) { |
| try { |
| context = context.createPackageContextAsUser(activityInfo.packageName, |
| CONTEXT_RESTRICTED, UserHandle.of(taskInfo.userId)); |
| context.setTheme(theme); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.w(TAG, "Failed creating package context with package name " |
| + activityInfo.packageName + " for user " + taskInfo.userId, e); |
| return; |
| } |
| } |
| |
| final Configuration taskConfig = taskInfo.getConfiguration(); |
| if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen: creating context based on task Configuration %s", |
| taskConfig); |
| final Context overrideContext = context.createConfigurationContext(taskConfig); |
| overrideContext.setTheme(theme); |
| final TypedArray typedArray = overrideContext.obtainStyledAttributes( |
| com.android.internal.R.styleable.Window); |
| final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); |
| try { |
| if (resId != 0 && overrideContext.getDrawable(resId) != null) { |
| // We want to use the windowBackground for the override context if it is |
| // available, otherwise we use the default one to make sure a themed starting |
| // window is displayed for the app. |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "addSplashScreen: apply overrideConfig %s", |
| taskConfig); |
| context = overrideContext; |
| } |
| } catch (Resources.NotFoundException e) { |
| Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: " |
| + taskId, e); |
| return; |
| } |
| typedArray.recycle(); |
| } |
| |
| final WindowManager.LayoutParams params = new WindowManager.LayoutParams( |
| WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); |
| params.setFitInsetsSides(0); |
| params.setFitInsetsTypes(0); |
| params.format = PixelFormat.TRANSLUCENT; |
| int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
| | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
| | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; |
| final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); |
| if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; |
| } |
| if (suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { |
| if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } |
| } else { |
| windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } |
| params.layoutInDisplayCutoutMode = a.getInt( |
| R.styleable.Window_windowLayoutInDisplayCutoutMode, |
| params.layoutInDisplayCutoutMode); |
| params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); |
| a.recycle(); |
| |
| // Assumes it's safe to show starting windows of launched apps while |
| // the keyguard is being hidden. This is okay because starting windows never show |
| // secret information. |
| // TODO(b/113840485): Occluded may not only happen on default display |
| if (displayId == DEFAULT_DISPLAY && windowInfo.isKeyguardOccluded) { |
| windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; |
| } |
| |
| // Force the window flags: this is a fake window, so it is not really |
| // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM |
| // flag because we do know that the next window will take input |
| // focus, so we want to get the IME window up on top of us right away. |
| // Touches will only pass through to the host activity window and will be blocked from |
| // passing to any other windows. |
| windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; |
| params.flags = windowFlags; |
| params.token = appToken; |
| params.packageName = activityInfo.packageName; |
| params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| |
| if (!context.getResources().getCompatibilityInfo().supportsScreen()) { |
| params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; |
| } |
| |
| params.setTitle("Splash Screen " + activityInfo.packageName); |
| |
| // TODO(b/173975965) tracking performance |
| // Prepare the splash screen content view on splash screen worker thread in parallel, so the |
| // content view won't be blocked by binder call like addWindow and relayout. |
| // 1. Trigger splash screen worker thread to create SplashScreenView before/while |
| // Session#addWindow. |
| // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start |
| // traversal, which will call Session#relayout on splash screen thread. |
| // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at |
| // the same time the splash screen thread should be executing Session#relayout. Blocking the |
| // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready. |
| |
| // Record whether create splash screen view success, notify to current thread after |
| // create splash screen view finished. |
| final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); |
| final FrameLayout rootLayout = new FrameLayout( |
| mSplashscreenContentDrawer.createViewContextWrapper(context)); |
| rootLayout.setPadding(0, 0, 0, 0); |
| rootLayout.setFitsSystemWindows(false); |
| final Runnable setViewSynchronized = () -> { |
| Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView"); |
| // waiting for setContentView before relayoutWindow |
| SplashScreenView contentView = viewSupplier.get(); |
| final StartingWindowRecord record = mStartingWindowRecords.get(taskId); |
| // If record == null, either the starting window added fail or removed already. |
| // Do not add this view if the token is mismatch. |
| if (record != null && appToken == record.mAppToken) { |
| // if view == null then creation of content view was failed. |
| if (contentView != null) { |
| try { |
| rootLayout.addView(contentView); |
| } catch (RuntimeException e) { |
| Slog.w(TAG, "failed set content view to starting window " |
| + "at taskId: " + taskId, e); |
| contentView = null; |
| } |
| } |
| record.setSplashScreenView(contentView); |
| } |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| }; |
| if (mSysuiProxy != null) { |
| mSysuiProxy.requestTopUi(true, TAG); |
| } |
| mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, |
| viewSupplier::setView, viewSupplier::setUiThreadInitTask); |
| try { |
| if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { |
| // We use the splash screen worker thread to create SplashScreenView while adding |
| // the window, as otherwise Choreographer#doFrame might be delayed on this thread. |
| // And since Choreographer#doFrame won't happen immediately after adding the window, |
| // if the view is not added to the PhoneWindow on the first #doFrame, the view will |
| // not be rendered on the first frame. So here we need to synchronize the view on |
| // the window before first round relayoutWindow, which will happen after insets |
| // animation. |
| mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null); |
| final StartingWindowRecord record = mStartingWindowRecords.get(taskId); |
| record.parseAppSystemBarColor(context); |
| // Block until we get the background color. |
| final SplashScreenView contentView = viewSupplier.get(); |
| if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { |
| contentView.addOnAttachStateChangeListener( |
| new View.OnAttachStateChangeListener() { |
| @Override |
| public void onViewAttachedToWindow(View v) { |
| final int lightBarAppearance = ContrastColorUtil.isColorLight( |
| contentView.getInitBackgroundColor()) |
| ? LIGHT_BARS_MASK : 0; |
| contentView.getWindowInsetsController().setSystemBarsAppearance( |
| lightBarAppearance, LIGHT_BARS_MASK); |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(View v) { |
| } |
| }); |
| } |
| record.mBGColor = contentView.getInitBackgroundColor(); |
| } else { |
| // release the icon view host |
| final SplashScreenView contentView = viewSupplier.get(); |
| if (contentView.getSurfaceHost() != null) { |
| SplashScreenView.releaseIconHost(contentView.getSurfaceHost()); |
| } |
| } |
| } catch (RuntimeException e) { |
| // don't crash if something else bad happens, for example a |
| // failure loading resources because we are loading from an app |
| // on external storage that has been unmounted. |
| Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e); |
| } |
| } |
| |
| int getStartingWindowBackgroundColorForTask(int taskId) { |
| final StartingWindowRecord startingWindowRecord = mStartingWindowRecords.get(taskId); |
| if (startingWindowRecord == null) { |
| return Color.TRANSPARENT; |
| } |
| return startingWindowRecord.mBGColor; |
| } |
| |
| private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { |
| private SplashScreenView mView; |
| private boolean mIsViewSet; |
| private Runnable mUiThreadInitTask; |
| void setView(SplashScreenView view) { |
| synchronized (this) { |
| mView = view; |
| mIsViewSet = true; |
| notify(); |
| } |
| } |
| |
| void setUiThreadInitTask(Runnable initTask) { |
| synchronized (this) { |
| mUiThreadInitTask = initTask; |
| } |
| } |
| |
| @Override |
| public @Nullable SplashScreenView get() { |
| synchronized (this) { |
| while (!mIsViewSet) { |
| try { |
| wait(); |
| } catch (InterruptedException ignored) { |
| } |
| } |
| if (mUiThreadInitTask != null) { |
| mUiThreadInitTask.run(); |
| mUiThreadInitTask = null; |
| } |
| return mView; |
| } |
| } |
| } |
| |
| int estimateTaskBackgroundColor(TaskInfo taskInfo) { |
| if (taskInfo.topActivityInfo == null) { |
| return Color.TRANSPARENT; |
| } |
| final ActivityInfo activityInfo = taskInfo.topActivityInfo; |
| final String packageName = activityInfo.packageName; |
| final int userId = taskInfo.userId; |
| final Context windowContext; |
| try { |
| windowContext = mContext.createPackageContextAsUser( |
| packageName, Context.CONTEXT_RESTRICTED, UserHandle.of(userId)); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.w(TAG, "Failed creating package context with package name " |
| + packageName + " for user " + taskInfo.userId, e); |
| return Color.TRANSPARENT; |
| } |
| try { |
| final IPackageManager packageManager = ActivityThread.getPackageManager(); |
| final String splashScreenThemeName = packageManager.getSplashScreenTheme(packageName, |
| userId); |
| final int splashScreenThemeId = splashScreenThemeName != null |
| ? windowContext.getResources().getIdentifier(splashScreenThemeName, null, null) |
| : 0; |
| |
| final int theme = getSplashScreenTheme(splashScreenThemeId, activityInfo); |
| |
| if (theme != windowContext.getThemeResId()) { |
| windowContext.setTheme(theme); |
| } |
| return mSplashscreenContentDrawer.estimateTaskBackgroundColor(windowContext); |
| } catch (RuntimeException | RemoteException e) { |
| Slog.w(TAG, "failed get starting window background color at taskId: " |
| + taskInfo.taskId, e); |
| } |
| return Color.TRANSPARENT; |
| } |
| |
| /** |
| * Called when a task need a snapshot starting window. |
| */ |
| void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken, |
| TaskSnapshot snapshot) { |
| final int taskId = startingWindowInfo.taskInfo.taskId; |
| // Remove any existing starting window for this task before adding. |
| removeWindowNoAnimate(taskId); |
| final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken, |
| snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId)); |
| if (surface == null) { |
| return; |
| } |
| final StartingWindowRecord tView = new StartingWindowRecord(appToken, |
| null/* decorView */, surface, STARTING_WINDOW_TYPE_SNAPSHOT); |
| mStartingWindowRecords.put(taskId, tView); |
| } |
| |
| /** |
| * Called when the content of a task is ready to show, starting window can be removed. |
| */ |
| public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "Task start finish, remove starting surface for task: %d", |
| removalInfo.taskId); |
| removeWindowSynced(removalInfo, false /* immediately */); |
| } |
| |
| /** |
| * Clear all starting windows immediately. |
| */ |
| public void clearAllWindows() { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "Clear all starting windows immediately"); |
| final int taskSize = mStartingWindowRecords.size(); |
| final int[] taskIds = new int[taskSize]; |
| for (int i = taskSize - 1; i >= 0; --i) { |
| taskIds[i] = mStartingWindowRecords.keyAt(i); |
| } |
| for (int i = taskSize - 1; i >= 0; --i) { |
| removeWindowNoAnimate(taskIds[i]); |
| } |
| } |
| |
| /** |
| * Called when the Task wants to copy the splash screen. |
| */ |
| public void copySplashScreenView(int taskId) { |
| final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); |
| SplashScreenViewParcelable parcelable; |
| SplashScreenView splashScreenView = preView != null ? preView.mContentView : null; |
| if (splashScreenView != null && splashScreenView.isCopyable()) { |
| parcelable = new SplashScreenViewParcelable(splashScreenView); |
| parcelable.setClientCallback( |
| new RemoteCallback((bundle) -> mSplashScreenExecutor.execute( |
| () -> onAppSplashScreenViewRemoved(taskId, false)))); |
| splashScreenView.onCopied(); |
| mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost()); |
| } else { |
| parcelable = null; |
| } |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "Copying splash screen window view for task: %d with parcelable %b", |
| taskId, parcelable != null); |
| ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); |
| } |
| |
| /** |
| * Called when the {@link SplashScreenView} is removed from the client Activity view's hierarchy |
| * or when the Activity is clean up. |
| * |
| * @param taskId The Task id on which the splash screen was attached |
| */ |
| public void onAppSplashScreenViewRemoved(int taskId) { |
| onAppSplashScreenViewRemoved(taskId, true /* fromServer */); |
| } |
| |
| /** |
| * @param fromServer If true, this means the removal was notified by the server. This is only |
| * used for debugging purposes. |
| * @see #onAppSplashScreenViewRemoved(int) |
| */ |
| private void onAppSplashScreenViewRemoved(int taskId, boolean fromServer) { |
| SurfaceControlViewHost viewHost = |
| mAnimatedSplashScreenSurfaceHosts.get(taskId); |
| if (viewHost == null) { |
| return; |
| } |
| mAnimatedSplashScreenSurfaceHosts.remove(taskId); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "%s the splash screen. Releasing SurfaceControlViewHost for task: %d", |
| fromServer ? "Server cleaned up" : "App removed", taskId); |
| SplashScreenView.releaseIconHost(viewHost); |
| } |
| |
| protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, |
| WindowManager.LayoutParams params, @StartingWindowType int suggestType) { |
| boolean shouldSaveView = true; |
| final Context context = view.getContext(); |
| try { |
| Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); |
| mWindowManagerGlobal.addView(view, params, display, |
| null /* parentWindow */, context.getUserId()); |
| } catch (WindowManager.BadTokenException e) { |
| // ignore |
| Slog.w(TAG, appToken + " already running, starting window not displayed. " |
| + e.getMessage()); |
| shouldSaveView = false; |
| } finally { |
| Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); |
| if (view.getParent() == null) { |
| Slog.w(TAG, "view not successfully added to wm, removing view"); |
| mWindowManagerGlobal.removeView(view, true /* immediate */); |
| shouldSaveView = false; |
| } |
| } |
| if (shouldSaveView) { |
| removeWindowNoAnimate(taskId); |
| saveSplashScreenRecord(appToken, taskId, view, suggestType); |
| } |
| return shouldSaveView; |
| } |
| |
| @VisibleForTesting |
| void saveSplashScreenRecord(IBinder appToken, int taskId, View view, |
| @StartingWindowType int suggestType) { |
| final StartingWindowRecord tView = new StartingWindowRecord(appToken, view, |
| null/* TaskSnapshotWindow */, suggestType); |
| mStartingWindowRecords.put(taskId, tView); |
| } |
| |
| private void removeWindowNoAnimate(int taskId) { |
| mTmpRemovalInfo.taskId = taskId; |
| removeWindowSynced(mTmpRemovalInfo, true /* immediately */); |
| } |
| |
| void onImeDrawnOnTask(int taskId) { |
| final StartingWindowRecord record = mStartingWindowRecords.get(taskId); |
| if (record != null && record.mTaskSnapshotWindow != null |
| && record.mTaskSnapshotWindow.hasImeSurface()) { |
| removeWindowNoAnimate(taskId); |
| } |
| } |
| |
| protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) { |
| final int taskId = removalInfo.taskId; |
| final StartingWindowRecord record = mStartingWindowRecords.get(taskId); |
| if (record != null) { |
| if (record.mDecorView != null) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "Removing splash screen window for task: %d", taskId); |
| if (record.mContentView != null) { |
| record.clearSystemBarColor(); |
| if (immediately |
| || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { |
| removeWindowInner(record.mDecorView, false); |
| } else { |
| if (removalInfo.playRevealAnimation) { |
| mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, |
| removalInfo.windowAnimationLeash, removalInfo.mainFrame, |
| () -> removeWindowInner(record.mDecorView, true), |
| record.mCreateTime); |
| } else { |
| // the SplashScreenView has been copied to client, hide the view to skip |
| // default exit animation |
| removeWindowInner(record.mDecorView, true); |
| } |
| } |
| } else { |
| // shouldn't happen |
| Slog.e(TAG, "Found empty splash screen, remove!"); |
| removeWindowInner(record.mDecorView, false); |
| } |
| |
| } |
| if (record.mTaskSnapshotWindow != null) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, |
| "Removing task snapshot window for %d", taskId); |
| if (immediately) { |
| record.mTaskSnapshotWindow.removeImmediately(); |
| } else { |
| record.mTaskSnapshotWindow.scheduleRemove(removalInfo.deferRemoveForIme); |
| } |
| } |
| mStartingWindowRecords.remove(taskId); |
| } |
| } |
| |
| private void removeWindowInner(View decorView, boolean hideView) { |
| if (mSysuiProxy != null) { |
| mSysuiProxy.requestTopUi(false, TAG); |
| } |
| if (hideView) { |
| decorView.setVisibility(View.GONE); |
| } |
| mWindowManagerGlobal.removeView(decorView, false /* immediate */); |
| } |
| |
| /** |
| * Record the view or surface for a starting window. |
| */ |
| private static class StartingWindowRecord { |
| private final IBinder mAppToken; |
| private final View mDecorView; |
| private final TaskSnapshotWindow mTaskSnapshotWindow; |
| private SplashScreenView mContentView; |
| private boolean mSetSplashScreen; |
| private @StartingWindowType int mSuggestType; |
| private int mBGColor; |
| private final long mCreateTime; |
| private int mSystemBarAppearance; |
| private boolean mDrawsSystemBarBackgrounds; |
| |
| StartingWindowRecord(IBinder appToken, View decorView, |
| TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) { |
| mAppToken = appToken; |
| mDecorView = decorView; |
| mTaskSnapshotWindow = taskSnapshotWindow; |
| if (mTaskSnapshotWindow != null) { |
| mBGColor = mTaskSnapshotWindow.getBackgroundColor(); |
| } |
| mSuggestType = suggestType; |
| mCreateTime = SystemClock.uptimeMillis(); |
| } |
| |
| private void setSplashScreenView(SplashScreenView splashScreenView) { |
| if (mSetSplashScreen) { |
| return; |
| } |
| mContentView = splashScreenView; |
| mSetSplashScreen = true; |
| } |
| |
| private void parseAppSystemBarColor(Context context) { |
| final TypedArray a = context.obtainStyledAttributes(R.styleable.Window); |
| mDrawsSystemBarBackgrounds = a.getBoolean( |
| R.styleable.Window_windowDrawsSystemBarBackgrounds, false); |
| if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { |
| mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; |
| } |
| if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) { |
| mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; |
| } |
| a.recycle(); |
| } |
| |
| // Reset the system bar color which set by splash screen, make it align to the app. |
| private void clearSystemBarColor() { |
| if (mDecorView == null) { |
| return; |
| } |
| if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) { |
| final WindowManager.LayoutParams lp = |
| (WindowManager.LayoutParams) mDecorView.getLayoutParams(); |
| if (mDrawsSystemBarBackgrounds) { |
| lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } else { |
| lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; |
| } |
| mDecorView.setLayoutParams(lp); |
| } |
| mDecorView.getWindowInsetsController().setSystemBarsAppearance( |
| mSystemBarAppearance, LIGHT_BARS_MASK); |
| } |
| } |
| } |