| /* |
| * 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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; |
| import static android.content.res.Configuration.ORIENTATION_PORTRAIT; |
| import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; |
| |
| 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.mockitoSession; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; |
| import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION; |
| import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.app.ActivityManager; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.graphics.ColorSpace; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.hardware.HardwareBuffer; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.testing.TestableContext; |
| import android.view.Display; |
| import android.view.IWindowSession; |
| import android.view.InsetsState; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.WindowManager; |
| import android.view.WindowManagerGlobal; |
| import android.view.WindowMetrics; |
| import android.window.StartingWindowInfo; |
| import android.window.StartingWindowRemovalInfo; |
| import android.window.TaskSnapshot; |
| |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| import androidx.test.filters.SmallTest; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.launcher3.icons.IconProvider; |
| import com.android.wm.shell.ShellTestCase; |
| import com.android.wm.shell.common.HandlerExecutor; |
| import com.android.wm.shell.common.ShellExecutor; |
| import com.android.wm.shell.common.TransactionPool; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.mockito.MockitoSession; |
| |
| import java.util.function.IntSupplier; |
| |
| /** |
| * Tests for the starting surface drawer. |
| */ |
| @SmallTest |
| @RunWith(AndroidJUnit4.class) |
| public class StartingSurfaceDrawerTests extends ShellTestCase { |
| @Mock |
| private IBinder mBinder; |
| @Mock |
| private WindowManager mMockWindowManager; |
| @Mock |
| private IconProvider mIconProvider; |
| @Mock |
| private TransactionPool mTransactionPool; |
| |
| private final Handler mTestHandler = new Handler(Looper.getMainLooper()); |
| private ShellExecutor mTestExecutor; |
| private final TestableContext mTestContext = new TestContext( |
| InstrumentationRegistry.getInstrumentation().getTargetContext()); |
| TestStartingSurfaceDrawer mStartingSurfaceDrawer; |
| |
| static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ |
| int mAddWindowForTask = 0; |
| |
| TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, |
| IconProvider iconProvider, TransactionPool pool) { |
| super(context, splashScreenExecutor, iconProvider, pool); |
| } |
| |
| @Override |
| protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, |
| WindowManager.LayoutParams params, int suggestType) { |
| // listen for addView |
| mAddWindowForTask = taskId; |
| saveSplashScreenRecord(appToken, taskId, view, suggestType); |
| // Do not wait for background color |
| return false; |
| } |
| |
| @Override |
| protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, |
| boolean immediately) { |
| // listen for removeView |
| if (mAddWindowForTask == removalInfo.taskId) { |
| mAddWindowForTask = 0; |
| } |
| mStartingWindowRecords.remove(removalInfo.taskId); |
| } |
| } |
| |
| private static class TestContext extends TestableContext { |
| TestContext(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public Context createPackageContextAsUser(String packageName, int flags, UserHandle user) |
| throws PackageManager.NameNotFoundException { |
| return this; |
| } |
| |
| @Override |
| public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, |
| IntentFilter filter, String broadcastPermission, Handler scheduler) { |
| return null; |
| } |
| } |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| final WindowManager realWindowManager = mTestContext.getSystemService(WindowManager.class); |
| final WindowMetrics metrics = realWindowManager.getMaximumWindowMetrics(); |
| mTestContext.addMockSystemService(WindowManager.class, mMockWindowManager); |
| |
| doReturn(metrics).when(mMockWindowManager).getMaximumWindowMetrics(); |
| doNothing().when(mMockWindowManager).addView(any(), any()); |
| mTestExecutor = new HandlerExecutor(mTestHandler); |
| mStartingSurfaceDrawer = spy( |
| new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, |
| mTransactionPool)); |
| } |
| |
| @Test |
| public void testAddSplashScreenSurface() { |
| final int taskId = 1; |
| final StartingWindowInfo windowInfo = |
| createWindowInfo(taskId, android.R.style.Theme); |
| mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, |
| STARTING_WINDOW_TYPE_SPLASH_SCREEN); |
| waitHandlerIdle(mTestHandler); |
| verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), |
| eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); |
| assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); |
| |
| StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); |
| removalInfo.taskId = windowInfo.taskInfo.taskId; |
| mStartingSurfaceDrawer.removeStartingWindow(removalInfo); |
| waitHandlerIdle(mTestHandler); |
| verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(false)); |
| assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); |
| } |
| |
| @Test |
| public void testFallbackDefaultTheme() { |
| final int taskId = 1; |
| final StartingWindowInfo windowInfo = |
| createWindowInfo(taskId, 0); |
| final int[] theme = new int[1]; |
| doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod()) |
| .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); |
| |
| mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, |
| STARTING_WINDOW_TYPE_SPLASH_SCREEN); |
| waitHandlerIdle(mTestHandler); |
| verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); |
| assertNotEquals(theme[0], 0); |
| } |
| |
| @Test |
| public void testColorCache() { |
| final String packageName = mTestContext.getPackageName(); |
| final int configHash = 1; |
| final int windowBgColor = 0xff000000; |
| final int windowBgResId = 1; |
| final IntSupplier windowBgColorSupplier = () -> windowBgColor; |
| final SplashscreenContentDrawer.ColorCache colorCache = |
| mStartingSurfaceDrawer.mSplashscreenContentDrawer.mColorCache; |
| final SplashscreenContentDrawer.ColorCache.WindowColor windowColor1 = |
| colorCache.getWindowColor(packageName, configHash, windowBgColor, windowBgResId, |
| windowBgColorSupplier); |
| assertEquals(windowBgColor, windowColor1.mBgColor); |
| assertEquals(0, windowColor1.mReuseCount); |
| |
| final SplashscreenContentDrawer.ColorCache.WindowColor windowColor2 = |
| colorCache.getWindowColor(packageName, configHash, windowBgColor, windowBgResId, |
| windowBgColorSupplier); |
| assertEquals(windowColor1, windowColor2); |
| assertEquals(1, windowColor1.mReuseCount); |
| |
| final Intent packageRemoved = new Intent(Intent.ACTION_PACKAGE_REMOVED); |
| packageRemoved.setData(Uri.parse("package:" + packageName)); |
| colorCache.onReceive(mTestContext, packageRemoved); |
| |
| final SplashscreenContentDrawer.ColorCache.WindowColor windowColor3 = |
| colorCache.getWindowColor(packageName, configHash, windowBgColor, windowBgResId, |
| windowBgColorSupplier); |
| assertEquals(0, windowColor3.mReuseCount); |
| } |
| |
| @Test |
| public void testRemoveTaskSnapshotWithImeSurfaceWhenOnImeDrawn() throws Exception { |
| final int taskId = 1; |
| final StartingWindowInfo windowInfo = |
| createWindowInfo(taskId, android.R.style.Theme); |
| TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100), |
| new Rect(0, 0, 0, 50), true /* hasImeSurface */); |
| final IWindowSession session = WindowManagerGlobal.getWindowSession(); |
| spyOn(session); |
| doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay( |
| any() /* window */, any() /* attrs */, |
| anyInt() /* viewVisibility */, anyInt() /* displayId */, |
| any() /* requestedVisibility */, any() /* outInputChannel */, |
| any() /* outInsetsState */, any() /* outActiveControls */, |
| any() /* outAttachedFrame */, any() /* outSizeCompatScale */); |
| TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo, |
| mBinder, |
| snapshot, mTestExecutor, () -> { |
| }); |
| spyOn(mockSnapshotWindow); |
| try (AutoCloseable mockTaskSnapshotSession = new AutoCloseable() { |
| MockitoSession mockSession = mockitoSession() |
| .initMocks(this) |
| .mockStatic(TaskSnapshotWindow.class) |
| .startMocking(); |
| @Override |
| public void close() { |
| mockSession.finishMocking(); |
| } |
| }) { |
| when(TaskSnapshotWindow.create(eq(windowInfo), eq(mBinder), eq(snapshot), any(), |
| any())).thenReturn(mockSnapshotWindow); |
| // Simulate a task snapshot window created with IME snapshot shown. |
| mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, mBinder, snapshot); |
| waitHandlerIdle(mTestHandler); |
| |
| // Verify the task snapshot with IME snapshot will be removed when received the real IME |
| // drawn callback. |
| // makeTaskSnapshotWindow shall call removeWindowSynced before there add a new |
| // StartingWindowRecord for the task. |
| mStartingSurfaceDrawer.onImeDrawnOnTask(1); |
| verify(mStartingSurfaceDrawer, times(2)) |
| .removeWindowSynced(any(), eq(true)); |
| } |
| } |
| |
| @Test |
| public void testClearAllWindows() { |
| final int taskId = 1; |
| final StartingWindowInfo windowInfo = |
| createWindowInfo(taskId, android.R.style.Theme); |
| mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, |
| STARTING_WINDOW_TYPE_SPLASH_SCREEN); |
| waitHandlerIdle(mTestHandler); |
| verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), |
| eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); |
| assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); |
| |
| mStartingSurfaceDrawer.clearAllWindows(); |
| waitHandlerIdle(mTestHandler); |
| verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(true)); |
| assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0); |
| } |
| |
| @Test |
| public void testMinimumAnimationDuration() { |
| final long maxDuration = MAX_ANIMATION_DURATION; |
| final long minDuration = MINIMAL_ANIMATION_DURATION; |
| |
| final long shortDuration = minDuration - 1; |
| final long medianShortDuration = minDuration + 1; |
| final long medianLongDuration = maxDuration - 1; |
| final long longAppDuration = maxDuration + 1; |
| |
| // static icon |
| assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration( |
| 0, shortDuration)); |
| // median launch + static icon |
| assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( |
| 0, medianShortDuration)); |
| // long launch + static icon |
| assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( |
| 0, longAppDuration)); |
| |
| // fast launch + animatable icon |
| assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration( |
| shortDuration, shortDuration)); |
| assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( |
| medianShortDuration, shortDuration)); |
| assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( |
| longAppDuration, shortDuration)); |
| |
| // median launch + animatable icon |
| assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( |
| shortDuration, medianShortDuration)); |
| assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration( |
| medianShortDuration, medianShortDuration)); |
| assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration( |
| longAppDuration, medianShortDuration)); |
| // between min < max launch + animatable icon |
| assertEquals(medianLongDuration, SplashscreenContentDrawer.getShowingDuration( |
| medianShortDuration, medianLongDuration)); |
| assertEquals(maxDuration, SplashscreenContentDrawer.getShowingDuration( |
| medianLongDuration, medianShortDuration)); |
| |
| // long launch + animatable icon |
| assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( |
| shortDuration, longAppDuration)); |
| assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( |
| medianShortDuration, longAppDuration)); |
| assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration( |
| longAppDuration, longAppDuration)); |
| } |
| |
| private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { |
| StartingWindowInfo windowInfo = new StartingWindowInfo(); |
| final ActivityInfo info = new ActivityInfo(); |
| info.applicationInfo = new ApplicationInfo(); |
| info.packageName = "test"; |
| info.theme = themeResId; |
| final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); |
| taskInfo.topActivityInfo = info; |
| taskInfo.taskId = taskId; |
| windowInfo.targetActivityInfo = info; |
| windowInfo.taskInfo = taskInfo; |
| windowInfo.topOpaqueWindowInsetsState = new InsetsState(); |
| windowInfo.mainWindowLayoutParams = new WindowManager.LayoutParams(); |
| windowInfo.topOpaqueWindowLayoutParams = new WindowManager.LayoutParams(); |
| return windowInfo; |
| } |
| |
| private static void waitHandlerIdle(Handler handler) { |
| handler.runWithScissors(() -> { }, 0 /* timeout */); |
| } |
| |
| private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, |
| Rect contentInsets, boolean hasImeSurface) { |
| final HardwareBuffer buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, |
| 1, HardwareBuffer.USAGE_CPU_READ_RARELY); |
| return new TaskSnapshot( |
| System.currentTimeMillis(), |
| new ComponentName("", ""), buffer, |
| ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, |
| Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, |
| false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, |
| 0 /* systemUiVisibility */, false /* isTranslucent */, |
| hasImeSurface /* hasImeSurface */); |
| } |
| } |