diff options
| author | 2020-11-11 02:30:02 +0000 | |
|---|---|---|
| committer | 2020-11-11 02:30:02 +0000 | |
| commit | 079f596c4ea9ac47d940c40a24ef293639fa408f (patch) | |
| tree | 2b423cb450ffb06f6d328a38a7b7198cf9430f01 /libs | |
| parent | a0131174f933c0c48452b925bc3e2bf6ea0a1eb2 (diff) | |
| parent | c1c4c5a6612fc3134f1ce1233e5851037719c491 (diff) | |
Merge "Delegate splash screen starting window to SystemUI(1/N)"
Diffstat (limited to 'libs')
4 files changed, 514 insertions, 4 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 2d20feeb0832..ece063cabf7c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -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 @@ -235,6 +242,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) { onTaskAppeared(new TaskAppearedInfo(taskInfo, leash)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java new file mode 100644 index 000000000000..ee79824ff4a7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -0,0 +1,343 @@ +/* + * 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); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index e4155a257cee..fdf4d31f0281 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; import android.content.pm.ParceledListSlice; import android.graphics.Rect; import android.os.Binder; @@ -71,6 +72,8 @@ public class ShellTaskOrganizerTests { @Mock private ITaskOrganizerController mTaskOrganizerController; + @Mock + private Context mContext; ShellTaskOrganizer mOrganizer; private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); @@ -106,7 +109,7 @@ public class ShellTaskOrganizerTests { .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue, - mTransactionPool, mTestExecutor, mTestExecutor)); + mTransactionPool, mTestExecutor, mTestExecutor, mContext)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java new file mode 100644 index 000000000000..f5628abb100f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -0,0 +1,147 @@ +/* + * 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 unittest.src.com.android.wm.shell.startingsurface; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +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.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.testing.TestableContext; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.startingsurface.StartingSurfaceDrawer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the starting surface drawer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StartingSurfaceDrawerTests { + @Mock + private IBinder mBinder; + @Mock + private WindowManager mMockWindowManager; + + TestStartingSurfaceDrawer mStartingSurfaceDrawer; + + static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ + int mAddWindowForTask = 0; + + TestStartingSurfaceDrawer(Context context) { + super(context); + } + + @Override + protected void postAddWindow(int taskId, IBinder appToken, + View view, WindowManager wm, WindowManager.LayoutParams params) { + // listen for addView + mAddWindowForTask = taskId; + } + + @Override + protected void removeWindowSynced(int taskId) { + // listen for removeView + if (mAddWindowForTask == taskId) { + mAddWindowForTask = 0; + } + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + final TestableContext context = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + final WindowManager realWindowManager = context.getSystemService(WindowManager.class); + final WindowMetrics metrics = realWindowManager.getMaximumWindowMetrics(); + context.addMockSystemService(WindowManager.class, mMockWindowManager); + + spyOn(context); + spyOn(realWindowManager); + try { + doReturn(context).when(context).createPackageContext(anyString(), anyInt()); + } catch (PackageManager.NameNotFoundException e) { + // + } + doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics(); + doNothing().when(mMockWindowManager).addView(any(), any()); + + mStartingSurfaceDrawer = spy(new TestStartingSurfaceDrawer(context)); + } + + @Test + public void testAddSplashScreenSurface() { + final int taskId = 1; + final Handler mainLoop = new Handler(Looper.getMainLooper()); + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN); + mStartingSurfaceDrawer.addStartingWindow(taskInfo, mBinder); + waitHandlerIdle(mainLoop); + verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); + + mStartingSurfaceDrawer.removeStartingWindow(taskInfo); + waitHandlerIdle(mainLoop); + verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId)); + assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); + } + + private ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + final ActivityInfo info = new ActivityInfo(); + info.applicationInfo = new ApplicationInfo(); + info.packageName = "test"; + info.theme = android.R.style.Theme; + taskInfo.topActivityInfo = info; + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + return taskInfo; + } + + private static void waitHandlerIdle(Handler handler) { + handler.runWithScissors(() -> { }, 0 /* timeout */); + } +} |