Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c1c4c5a6 authored by wilsonshih's avatar wilsonshih
Browse files

Delegate splash screen starting window to SystemUI(1/N)

Mirror PhoneWindowManager#addSplashScreen to StartingSurfaceDrawer,
which make WMShell able to draw the splash screen starting window.

- Use StartingSurfaceController#DEBUG_ENABLE_SHELL_DRAWER to switch
drawer when developing this feature.

- Temporarily put StartingSurfaceDrawer in ShellTaskOrganizer, the
drawer should be controlled by a controller which should be create
while porting ActivityRecord#addStartingWindow to Shell.

Ref doc: go/delegate_starting_window

Bug: 131727939
Test: atest AppWindowTokenTests WindowOrganizerTests ActivityStackTests
Test: atest WMShellTest ShellTaskOrganizerTests
StartingSurfaceDrawerTests
Test: check from winscope that the splash screen window can
attach to/detach from the ActivityRecord.

Change-Id: I6dfc9ff75807e2f9479149d14f219c91a6527393
parent 6ec9842f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -2417,6 +2417,7 @@ package android.window {

  public class TaskOrganizer extends android.window.WindowOrganizer {
    ctor public TaskOrganizer();
    method @BinderThread public void addStartingWindow(@NonNull android.app.ActivityManager.RunningTaskInfo, @NonNull android.os.IBinder);
    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void createRootTask(int, int, @Nullable android.os.IBinder);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull android.window.WindowContainerToken);
    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public java.util.List<android.app.ActivityManager.RunningTaskInfo> getChildTasks(@NonNull android.window.WindowContainerToken, @NonNull int[]);
@@ -2427,6 +2428,7 @@ package android.window {
    method @BinderThread public void onTaskInfoChanged(@NonNull android.app.ActivityManager.RunningTaskInfo);
    method @BinderThread public void onTaskVanished(@NonNull android.app.ActivityManager.RunningTaskInfo);
    method @CallSuper @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public java.util.List<android.window.TaskAppearedInfo> registerOrganizer();
    method @BinderThread public void removeStartingWindow(@NonNull android.app.ActivityManager.RunningTaskInfo);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void setInterceptBackPressedOnTaskRoot(@NonNull android.window.WindowContainerToken, boolean);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void setLaunchRoot(int, @NonNull android.window.WindowContainerToken);
    method @CallSuper @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void unregisterOrganizer();
+15 −0
Original line number Diff line number Diff line
@@ -25,6 +25,21 @@ import android.window.WindowContainerToken;
 * {@hide}
 */
oneway interface ITaskOrganizer {
    /**
     * Called when a Task is starting and the system would like to show a UI to indicate that an
     * application is starting. The client is responsible to add/remove the starting window if it
     * has create a starting window for the Task.
     *
     * @param taskInfo The information about the Task that's available
     * @param appToken Token of the application being started.
     */
    void addStartingWindow(in ActivityManager.RunningTaskInfo taskInfo, IBinder appToken);

    /**
     * Called when the Task want to remove the starting window.
     */
    void removeStartingWindow(in ActivityManager.RunningTaskInfo taskInfo);

    /**
     * A callback when the Task is available for the registered organizer. The client is responsible
     * for releasing the SurfaceControl in the callback. For non-root tasks, the leash may initially
+28 −0
Original line number Diff line number Diff line
@@ -84,6 +84,25 @@ public class TaskOrganizer extends WindowOrganizer {
        }
    }

    /**
     * Called when a Task is starting and the system would like to show a UI to indicate that an
     * application is starting. The client is responsible to add/remove the starting window if it
     * has create a starting window for the Task.
     *
     * @param taskInfo The information about the Task that's available
     * @param appToken Token of the application being started.
     *        context to for resources
     */
    @BinderThread
    public void addStartingWindow(@NonNull ActivityManager.RunningTaskInfo taskInfo,
            @NonNull IBinder appToken) {}

    /**
     * Called when the Task want to remove the starting window.
     */
    @BinderThread
    public void removeStartingWindow(@NonNull ActivityManager.RunningTaskInfo taskInfo) {}

    /**
     * Called when a task with the registered windowing mode can be controlled by this task
     * organizer. For non-root tasks, the leash may initially be hidden so it is up to the organizer
@@ -192,6 +211,15 @@ public class TaskOrganizer extends WindowOrganizer {
    }

    private final ITaskOrganizer mInterface = new ITaskOrganizer.Stub() {
        @Override
        public void addStartingWindow(ActivityManager.RunningTaskInfo taskInfo, IBinder appToken) {
            TaskOrganizer.this.addStartingWindow(taskInfo, appToken);
        }

        @Override
        public void removeStartingWindow(ActivityManager.RunningTaskInfo taskInfo) {
            TaskOrganizer.this.removeStartingWindow(taskInfo);
        }

        @Override
        public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+20 −3
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG
import android.annotation.IntDef;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.util.ArrayMap;
@@ -44,6 +45,7 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.startingsurface.StartingSurfaceDrawer;

import java.io.PrintWriter;
import java.util.ArrayList;
@@ -104,21 +106,26 @@ public class ShellTaskOrganizer extends TaskOrganizer {
    private final Transitions mTransitions;

    private final Object mLock = new Object();
    private final StartingSurfaceDrawer mStartingSurfaceDrawer;

    public ShellTaskOrganizer(SyncTransactionQueue syncQueue, TransactionPool transactionPool,
            ShellExecutor mainExecutor, ShellExecutor animExecutor) {
        this(null, syncQueue, transactionPool, mainExecutor, animExecutor);
            ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) {
        this(null, syncQueue, transactionPool, mainExecutor, animExecutor, context);
    }

    @VisibleForTesting
    ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController,
            SyncTransactionQueue syncQueue, TransactionPool transactionPool,
            ShellExecutor mainExecutor, ShellExecutor animExecutor) {
            ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) {
        super(taskOrganizerController, mainExecutor);
        addListenerForType(new FullscreenTaskListener(syncQueue), TASK_LISTENER_TYPE_FULLSCREEN);
        addListenerForType(new LetterboxTaskListener(syncQueue), TASK_LISTENER_TYPE_LETTERBOX);
        mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor);
        if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions);
        // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled
        //  by a controller, that class should be create while porting
        //  ActivityRecord#addStartingWindow to WMShell.
        mStartingSurfaceDrawer = new StartingSurfaceDrawer(context);
    }

    @Override
@@ -234,6 +241,16 @@ public class ShellTaskOrganizer extends TaskOrganizer {
        }
    }

    @Override
    public void addStartingWindow(RunningTaskInfo taskInfo, IBinder appToken) {
        mStartingSurfaceDrawer.addStartingWindow(taskInfo, appToken);
    }

    @Override
    public void removeStartingWindow(RunningTaskInfo taskInfo) {
        mStartingSurfaceDrawer.removeStartingWindow(taskInfo);
    }

    @Override
    public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
        synchronized (mLock) {
+343 −0
Original line number Diff line number Diff line
/*
 * 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.content.res.Configuration.EMPTY;
import static android.view.Display.DEFAULT_DISPLAY;

import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.window.TaskOrganizer;

import com.android.internal.R;
import com.android.internal.policy.PhoneWindow;

import java.util.function.Consumer;

/**
 * Implementation to draw the starting window to an application, and remove the starting window
 * until the application displays its own window.
 *
 * When receive {@link TaskOrganizer#addStartingWindow} callback, use this class to create a
 * starting window and attached to the Task, then when the Task want to remove the starting window,
 * the TaskOrganizer will receive {@link TaskOrganizer#removeStartingWindow} callback then use this
 * class to remove the starting window of the Task.
 * @hide
 */
public class StartingSurfaceDrawer {
    private static final String TAG = StartingSurfaceDrawer.class.getSimpleName();
    private static final boolean DEBUG_SPLASH_SCREEN = false;

    private final Context mContext;
    private final DisplayManager mDisplayManager;

    // TODO(b/131727939) remove this when clearing ActivityRecord
    private static final int REMOVE_WHEN_TIMEOUT = 2000;

    public StartingSurfaceDrawer(Context context) {
        mContext = context;
        mDisplayManager = mContext.getSystemService(DisplayManager.class);
    }

    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final SparseArray<TaskScreenView> mTaskScreenViews = new SparseArray<>();

    /** Obtain proper context for showing splash screen on the provided display. */
    private Context getDisplayContext(Context context, int displayId) {
        if (displayId == DEFAULT_DISPLAY) {
            // The default context fits.
            return context;
        }

        final Display targetDisplay = mDisplayManager.getDisplay(displayId);
        if (targetDisplay == null) {
            // Failed to obtain the non-default display where splash screen should be shown,
            // lets not show at all.
            return null;
        }

        return context.createDisplayContext(targetDisplay);
    }

    /**
     * Called when a task need a starting window.
     */
    public void addStartingWindow(ActivityManager.RunningTaskInfo taskInfo, IBinder appToken) {

        final ActivityInfo activityInfo = taskInfo.topActivityInfo;
        final int displayId = taskInfo.displayId;
        if (activityInfo.packageName == null) {
            return;
        }

        CharSequence nonLocalizedLabel = activityInfo.nonLocalizedLabel;
        int labelRes = activityInfo.labelRes;
        if (activityInfo.nonLocalizedLabel == null && activityInfo.labelRes == 0) {
            ApplicationInfo app = activityInfo.applicationInfo;
            nonLocalizedLabel = app.nonLocalizedLabel;
            labelRes = app.labelRes;
        }

        Context context = mContext;
        final int theme = activityInfo.getThemeResource();
        if (DEBUG_SPLASH_SCREEN) {
            Slog.d(TAG, "addSplashScreen " + activityInfo.packageName
                    + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme="
                    + Integer.toHexString(theme) + " task= " + taskInfo.taskId);
        }

        // Obtain proper context to launch on the right display.
        final Context displayContext = getDisplayContext(context, displayId);
        if (displayContext == null) {
            // Can't show splash screen on requested display, so skip showing at all.
            return;
        }
        context = displayContext;

        if (theme != context.getThemeResId() || labelRes != 0) {
            try {
                context = context.createPackageContext(
                        activityInfo.packageName, CONTEXT_RESTRICTED);
                context.setTheme(theme);
            } catch (PackageManager.NameNotFoundException e) {
                // Ignore
            }
        }

        final Configuration taskConfig = taskInfo.getConfiguration();
        if (taskConfig != null && !taskConfig.equals(EMPTY)) {
            if (DEBUG_SPLASH_SCREEN) {
                Slog.d(TAG, "addSplashScreen: creating context based"
                        + " on task Configuration " + taskConfig + " for splash screen");
            }
            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);
            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.
                if (DEBUG_SPLASH_SCREEN) {
                    Slog.d(TAG, "addSplashScreen: apply overrideConfig"
                            + taskConfig + " to starting window resId=" + resId);
                }
                context = overrideContext;
            }
            typedArray.recycle();
        }

        int windowFlags = 0;
        if ((activityInfo.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
            windowFlags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }

        final boolean[] showWallpaper = new boolean[1];
        final int[] splashscreenContentResId = new int[1];
        getWindowResFromContext(context, a -> {
            splashscreenContentResId[0] =
                    a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0);
            showWallpaper[0] = a.getBoolean(R.styleable.Window_windowShowWallpaper, false);
        });
        if (showWallpaper[0]) {
            windowFlags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
        }

        final PhoneWindow win = new PhoneWindow(context);
        win.setIsStartingWindow(true);

        CharSequence label = context.getResources().getText(labelRes, null);
        // Only change the accessibility title if the label is localized
        if (label != null) {
            win.setTitle(label, true);
        } else {
            win.setTitle(nonLocalizedLabel, false);
        }

        win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);

        // 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) {
            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.
        win.setFlags(windowFlags
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                windowFlags
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);

        final int iconRes = activityInfo.getIconResource();
        final int logoRes = activityInfo.getLogoResource();
        win.setDefaultIcon(iconRes);
        win.setDefaultLogo(logoRes);

        win.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT);

        final WindowManager.LayoutParams params = win.getAttributes();
        params.token = appToken;
        params.packageName = activityInfo.packageName;
        params.windowAnimations = win.getWindowStyle().getResourceId(
                com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
        params.privateFlags |=
                WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED;
        params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
        // Setting as trusted overlay to let touches pass through. This is safe because this
        // window is controlled by the system.
        params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;

        final Resources res = context.getResources();
        final boolean supportsScreen = res != null && (res.getCompatibilityInfo() != null
                && res.getCompatibilityInfo().supportsScreen());
        if (!supportsScreen) {
            params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
        }

        params.setTitle("Splash Screen " + activityInfo.packageName);
        addSplashscreenContent(win, context, splashscreenContentResId[0]);

        final View view = win.getDecorView();

        if (DEBUG_SPLASH_SCREEN) {
            Slog.d(TAG, "Adding splash screen window for "
                    + activityInfo.packageName + " / " + appToken + ": " + view);
        }
        final WindowManager wm = context.getSystemService(WindowManager.class);
        postAddWindow(taskInfo.taskId, appToken, view, wm, params);
    }

    /**
     * Called when the content of a task is ready to show, starting window can be removed.
     */
    public void removeStartingWindow(ActivityManager.RunningTaskInfo taskInfo) {
        if (DEBUG_SPLASH_SCREEN) {
            Slog.d(TAG, "Task start finish, remove starting surface for task " + taskInfo.taskId);
        }
        mHandler.post(() -> removeWindowSynced(taskInfo.taskId));
    }

    protected void postAddWindow(int taskId, IBinder appToken,
            View view, WindowManager wm, WindowManager.LayoutParams params) {
        mHandler.post(() -> {
            boolean shouldSaveView = true;
            try {
                wm.addView(view, params);
            } catch (WindowManager.BadTokenException e) {
                // ignore
                Slog.w(TAG, appToken + " already running, starting window not displayed. "
                        + e.getMessage());
                shouldSaveView = false;
            } 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, appToken + " failed creating starting window", e);
                shouldSaveView = false;
            } finally {
                if (view != null && view.getParent() == null) {
                    Slog.w(TAG, "view not successfully added to wm, removing view");
                    wm.removeViewImmediate(view);
                    shouldSaveView = false;
                }
            }

            if (shouldSaveView) {
                removeWindowSynced(taskId);
                mHandler.postDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT);
                final TaskScreenView tView = new TaskScreenView(view);
                mTaskScreenViews.put(taskId, tView);
            }
        });
    }

    protected void removeWindowSynced(int taskId) {
        final TaskScreenView preView = mTaskScreenViews.get(taskId);
        if (preView != null) {
            if (preView.mDecorView != null) {
                if (DEBUG_SPLASH_SCREEN) {
                    Slog.v(TAG, "Removing splash screen window for task: " + taskId);
                }
                final WindowManager wm = preView.mDecorView.getContext()
                        .getSystemService(WindowManager.class);
                wm.removeView(preView.mDecorView);
            }
            mTaskScreenViews.remove(taskId);
        }
    }

    private void getWindowResFromContext(Context ctx, Consumer<TypedArray> consumer) {
        final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
        consumer.accept(a);
        a.recycle();
    }

    /**
     * Record the views in a starting window.
     */
    private static class TaskScreenView {
        private final View mDecorView;

        TaskScreenView(View decorView) {
            mDecorView = decorView;
        }
    }

    private void addSplashscreenContent(PhoneWindow win, Context ctx,
            int splashscreenContentResId) {
        if (splashscreenContentResId == 0) {
            return;
        }
        final Drawable drawable = ctx.getDrawable(splashscreenContentResId);
        if (drawable == null) {
            return;
        }

        // We wrap this into a view so the system insets get applied to the drawable.
        final View v = new View(ctx);
        v.setBackground(drawable);
        win.setContentView(v);
    }
}
Loading