blob: 9212c4b86105d9384c48680bf6a4c5c8a140faaf [file] [log] [blame]
/*
* 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.RunningTaskInfo;
import android.app.ActivityTaskManager;
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.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.hardware.display.DisplayManager;
import android.os.IBinder;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowManager;
import android.window.SplashScreenView;
import android.window.SplashScreenView.SplashScreenViewParcelable;
import android.window.StartingWindowInfo;
import android.window.TaskSnapshot;
import com.android.internal.R;
import com.android.internal.policy.PhoneWindow;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import java.util.function.Consumer;
/**
* A class which able to draw splash screen or snapshot as the starting window for a task.
* @hide
*/
public class StartingSurfaceDrawer {
static final String TAG = StartingSurfaceDrawer.class.getSimpleName();
static final boolean DEBUG_SPLASH_SCREEN = StartingWindowController.DEBUG_SPLASH_SCREEN;
static final boolean DEBUG_TASK_SNAPSHOT = StartingWindowController.DEBUG_TASK_SNAPSHOT;
private final Context mContext;
private final DisplayManager mDisplayManager;
private final ShellExecutor mSplashScreenExecutor;
private final SplashscreenContentDrawer mSplashscreenContentDrawer;
public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
TransactionPool pool) {
mContext = context;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mSplashScreenExecutor = splashScreenExecutor;
final int maxAnimatableIconDuration = context.getResources().getInteger(
com.android.wm.shell.R.integer.max_starting_window_intro_icon_anim_duration);
final int iconExitAnimDuration = context.getResources().getInteger(
com.android.wm.shell.R.integer.starting_window_icon_exit_anim_duration);
final int appRevealAnimDuration = context.getResources().getInteger(
com.android.wm.shell.R.integer.starting_window_app_reveal_anim_duration);
mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext,
maxAnimatableIconDuration, iconExitAnimDuration, appRevealAnimDuration, pool);
}
private final SparseArray<StartingWindowRecord> mStartingWindowRecords = 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 splash screen starting window.
*/
public void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) {
final RunningTaskInfo taskInfo = windowInfo.taskInfo;
final ActivityInfo activityInfo = taskInfo.topActivityInfo;
if (activityInfo == null) {
return;
}
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;
// replace with the default theme if the application didn't set
final int theme = windowInfo.splashScreenThemeResId != 0
? windowInfo.splashScreenThemeResId
: activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource()
: com.android.internal.R.style.Theme_DeviceDefault_DayNight;
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.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);
}
}
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;
final boolean enableHardAccelerated =
(activityInfo.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0;
if (enableHardAccelerated) {
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.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
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 && 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.
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;
params.format = PixelFormat.RGBA_8888;
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);
// TODO(b/173975965) tracking performance
final int taskId = taskInfo.taskId;
SplashScreenView sView = null;
try {
sView = mSplashscreenContentDrawer.makeSplashScreenContentView(context, iconRes,
splashscreenContentResId[0]);
final View view = win.getDecorView();
final WindowManager wm = mContext.getSystemService(WindowManager.class);
if (postAddWindow(taskId, appToken, view, wm, params)) {
win.setContentView(sView);
sView.cacheRootWindow(win);
}
} 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", e);
} finally {
setSplashScreenRecord(taskId, sView);
}
}
/**
* Called when a task need a snapshot starting window.
*/
void makeTaskSnapshotWindow(StartingWindowInfo startingWindowInfo, IBinder appToken,
TaskSnapshot snapshot) {
final int taskId = startingWindowInfo.taskInfo.taskId;
final TaskSnapshotWindow surface = TaskSnapshotWindow.create(startingWindowInfo, appToken,
snapshot, mSplashScreenExecutor, () -> removeWindowNoAnimate(taskId));
final StartingWindowRecord tView = new StartingWindowRecord(null/* decorView */, surface);
mStartingWindowRecords.put(taskId, tView);
}
/**
* Called when the content of a task is ready to show, starting window can be removed.
*/
public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
boolean playRevealAnimation) {
if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId);
}
removeWindowSynced(taskId, leash, frame, playRevealAnimation);
}
/**
* Called when the Task wants to copy the splash screen.
* @param taskId
*/
public void copySplashScreenView(int taskId) {
final StartingWindowRecord preView = mStartingWindowRecords.get(taskId);
SplashScreenViewParcelable parcelable;
if (preView != null && preView.mContentView != null
&& preView.mContentView.isCopyable()) {
parcelable = new SplashScreenViewParcelable(preView.mContentView);
} else {
parcelable = null;
}
if (DEBUG_SPLASH_SCREEN) {
Slog.v(TAG, "Copying splash screen window view for task: " + taskId
+ " parcelable? " + parcelable);
}
ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable);
}
protected boolean postAddWindow(int taskId, IBinder appToken, View view, WindowManager wm,
WindowManager.LayoutParams params) {
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;
} 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) {
removeWindowNoAnimate(taskId);
saveSplashScreenRecord(taskId, view);
}
return shouldSaveView;
}
private void saveSplashScreenRecord(int taskId, View view) {
final StartingWindowRecord tView = new StartingWindowRecord(view,
null/* TaskSnapshotWindow */);
mStartingWindowRecords.put(taskId, tView);
}
private void setSplashScreenRecord(int taskId, SplashScreenView splashScreenView) {
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
if (record != null) {
record.setSplashScreenView(splashScreenView);
splashScreenView.startIntroAnimation();
}
}
private void removeWindowNoAnimate(int taskId) {
removeWindowSynced(taskId, null, null, false);
}
protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame,
boolean playRevealAnimation) {
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
if (record != null) {
if (record.mDecorView != null) {
if (DEBUG_SPLASH_SCREEN) {
Slog.v(TAG, "Removing splash screen window for task: " + taskId);
}
if (record.mContentView != null) {
final HandleExitFinish exitFinish = new HandleExitFinish(record.mDecorView);
if (leash != null || playRevealAnimation) {
mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
leash, frame, record.isEarlyExit(), exitFinish);
} else {
// the SplashScreenView has been copied to client, skip default exit
// animation
exitFinish.run();
}
}
}
if (record.mTaskSnapshotWindow != null) {
if (DEBUG_TASK_SNAPSHOT) {
Slog.v(TAG, "Removing task snapshot window for " + taskId);
}
record.mTaskSnapshotWindow.remove();
}
mStartingWindowRecords.remove(taskId);
}
}
private static class HandleExitFinish implements Runnable {
private View mDecorView;
HandleExitFinish(View decorView) {
mDecorView = decorView;
}
@Override
public void run() {
if (mDecorView == null) {
return;
}
final WindowManager wm = mDecorView.getContext().getSystemService(WindowManager.class);
if (wm != null) {
wm.removeView(mDecorView);
}
mDecorView = null;
}
}
private void getWindowResFromContext(Context ctx, Consumer<TypedArray> consumer) {
final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
consumer.accept(a);
a.recycle();
}
/**
* Record the view or surface for a starting window.
*/
private static class StartingWindowRecord {
private static final long EARLY_START_MINIMUM_TIME_MS = 250;
private final View mDecorView;
private final TaskSnapshotWindow mTaskSnapshotWindow;
private SplashScreenView mContentView;
private boolean mSetSplashScreen;
private long mContentCreateTime;
StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow) {
mDecorView = decorView;
mTaskSnapshotWindow = taskSnapshotWindow;
}
private void setSplashScreenView(SplashScreenView splashScreenView) {
if (mSetSplashScreen) {
return;
}
mContentView = splashScreenView;
mContentCreateTime = SystemClock.uptimeMillis();
mSetSplashScreen = true;
}
boolean isEarlyExit() {
return SystemClock.uptimeMillis() - mContentCreateTime < EARLY_START_MINIMUM_TIME_MS;
}
}
}