Loading quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.javadeleted 100644 → 0 +0 −288 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.quickstep; import static androidx.test.InstrumentationRegistry.getContext; import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE; import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER; import static com.android.launcher3.util.WidgetUtils.createWidgetInfo; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewConfiguration; import android.widget.RemoteViews; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.Suppress; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.Until; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.celllayout.FavoriteItemsTransaction; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.tapl.LaunchedAppState; import com.android.launcher3.testcomponent.ListViewService; import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory; import com.android.launcher3.testcomponent.TestCommandReceiver; import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Executors; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.lang.reflect.Field; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntConsumer; /** * Test to verify view inflation does not happen during swipe up. * To verify view inflation, we setup a stub ViewConfiguration and check if any call to that class * does from a View.init method or not. * * Alternative approaches considered: * Overriding LayoutInflater: This does not cover views initialized * directly (ex: new LinearLayout) * Using ExtendedMockito: Mocking static methods from platform classes (loaded in zygote) makes * the main thread extremely slow and untestable */ @LargeTest @RunWith(AndroidJUnit4.class) public class TaplViewInflationDuringSwipeUp extends AbstractQuickStepTest { private SparseArray<ViewConfiguration> mConfigMap; private InitTracker mInitTracker; private LauncherModel mModel; @Before public void setUp() throws Exception { // Workaround for b/142351228, when there are no activities, the system may not destroy the // activity correctly for activities under instrumentation, which can leave two concurrent // activities, which changes the order in which the activities are cleaned up (overlapping // stop and start) leading to all sort of issues. To workaround this, ensure that the test // is started only after starting another app. startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); super.setUp(); mModel = LauncherAppState.getInstance(mTargetContext).getModel(); Executors.MODEL_EXECUTOR.submit(mModel.getModelDbController()::createEmptyDB).get(); // Get static configuration map Field field = ViewConfiguration.class.getDeclaredField("sConfigurations"); field.setAccessible(true); mConfigMap = (SparseArray<ViewConfiguration>) field.get(null); mInitTracker = new InitTracker(); } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190618549 is fixed public void testSwipeUpFromApp() throws Exception { try { // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); mLauncher.getLaunchedAppState().switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); } finally { mConfigMap.clear(); } } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190729479 is fixed public void testSwipeUpFromApp_widget_update() { String stubText = "Some random stub text"; executeSwipeUpTestWithWidget( widgetId -> { }, widgetId -> AppWidgetManager.getInstance(getContext()) .updateAppWidget(widgetId, createMainWidgetViews(stubText)), stubText); } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190729479 is fixed public void testSwipeUp_with_list_widgets() { SimpleViewsFactory viewFactory = new SimpleViewsFactory(); viewFactory.viewCount = 1; Bundle args = new Bundle(); args.putBinder(EXTRA_VALUE, viewFactory.toBinder()); TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, args); try { executeSwipeUpTestWithWidget( widgetId -> { // Initialize widget RemoteViews views = createMainWidgetViews("List widget title"); views.setRemoteAdapter(android.R.id.list, new Intent(getContext(), ListViewService.class)); AppWidgetManager.getInstance(getContext()).updateAppWidget(widgetId, views); verifyWidget(viewFactory.getLabel(0)); }, widgetId -> { // Update widget viewFactory.viewCount = 2; AppWidgetManager.getInstance(getContext()) .notifyAppWidgetViewDataChanged(widgetId, android.R.id.list); }, viewFactory.getLabel(1) ); } finally { TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, new Bundle()); } } private void executeSwipeUpTestWithWidget(IntConsumer widgetIdCreationCallback, IntConsumer updateBeforeSwipeUp, String finalWidgetText) { try { LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(false); // Make sure the widget is big enough to show a list of items info.minSpanX = 2; info.minSpanY = 2; info.spanX = 2; info.spanY = 2; AtomicInteger widgetId = new AtomicInteger(); commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext) .addItem(() -> { LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, true); item.screenId = FIRST_SCREEN_ID; widgetId.set(item.appWidgetId); return item; })); assertTrue("Widget is not present", mLauncher.goHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null); // Verify widget id widgetIdCreationCallback.accept(widgetId.get()); // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState(); // Update widget updateBeforeSwipeUp.accept(widgetId.get()); launchedAppState.switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); // Widget is updated when going home mInitTracker.disableLog(); mLauncher.goHome(); verifyWidget(finalWidgetText); assertNotEquals(1, mInitTracker.viewInitCount); } finally { mConfigMap.clear(); } } private void verifyWidget(String text) { assertNotNull("Widget not updated", UiDevice.getInstance(getInstrumentation()) .wait(Until.findObject(By.text(text)), DEFAULT_UI_TIMEOUT)); } private RemoteViews createMainWidgetViews(String title) { Context c = getContext(); int layoutId = c.getResources().getIdentifier( "test_layout_widget_list", "layout", c.getPackageName()); RemoteViews views = new RemoteViews(c.getPackageName(), layoutId); views.setTextViewText(android.R.id.text1, title); return views; } private class InitTracker implements Answer { public int viewInitCount = 0; public boolean log = true; @Override public Object answer(InvocationOnMock invocation) throws Throwable { Exception ex = new Exception(); boolean found = false; for (StackTraceElement ste : ex.getStackTrace()) { if ("<init>".equals(ste.getMethodName()) && View.class.getName().equals(ste.getClassName())) { found = true; break; } } if (found) { viewInitCount++; if (log) { Log.d("InitTracker", "New view inflated", ex); } } return invocation.callRealMethod(); } public void disableLog() { log = false; } public void startTracking() { ViewConfiguration vc = ViewConfiguration.get(mTargetContext); ViewConfiguration spyVC = spy(vc); mConfigMap.put(mConfigMap.keyAt(mConfigMap.indexOfValue(vc)), spyVC); doAnswer(this).when(spyVC).getScaledTouchSlop(); } } } Loading
quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.javadeleted 100644 → 0 +0 −288 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.quickstep; import static androidx.test.InstrumentationRegistry.getContext; import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE; import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER; import static com.android.launcher3.util.WidgetUtils.createWidgetInfo; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewConfiguration; import android.widget.RemoteViews; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.Suppress; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.Until; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.celllayout.FavoriteItemsTransaction; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.tapl.LaunchedAppState; import com.android.launcher3.testcomponent.ListViewService; import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory; import com.android.launcher3.testcomponent.TestCommandReceiver; import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Executors; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.lang.reflect.Field; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntConsumer; /** * Test to verify view inflation does not happen during swipe up. * To verify view inflation, we setup a stub ViewConfiguration and check if any call to that class * does from a View.init method or not. * * Alternative approaches considered: * Overriding LayoutInflater: This does not cover views initialized * directly (ex: new LinearLayout) * Using ExtendedMockito: Mocking static methods from platform classes (loaded in zygote) makes * the main thread extremely slow and untestable */ @LargeTest @RunWith(AndroidJUnit4.class) public class TaplViewInflationDuringSwipeUp extends AbstractQuickStepTest { private SparseArray<ViewConfiguration> mConfigMap; private InitTracker mInitTracker; private LauncherModel mModel; @Before public void setUp() throws Exception { // Workaround for b/142351228, when there are no activities, the system may not destroy the // activity correctly for activities under instrumentation, which can leave two concurrent // activities, which changes the order in which the activities are cleaned up (overlapping // stop and start) leading to all sort of issues. To workaround this, ensure that the test // is started only after starting another app. startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); super.setUp(); mModel = LauncherAppState.getInstance(mTargetContext).getModel(); Executors.MODEL_EXECUTOR.submit(mModel.getModelDbController()::createEmptyDB).get(); // Get static configuration map Field field = ViewConfiguration.class.getDeclaredField("sConfigurations"); field.setAccessible(true); mConfigMap = (SparseArray<ViewConfiguration>) field.get(null); mInitTracker = new InitTracker(); } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190618549 is fixed public void testSwipeUpFromApp() throws Exception { try { // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); mLauncher.getLaunchedAppState().switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); } finally { mConfigMap.clear(); } } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190729479 is fixed public void testSwipeUpFromApp_widget_update() { String stubText = "Some random stub text"; executeSwipeUpTestWithWidget( widgetId -> { }, widgetId -> AppWidgetManager.getInstance(getContext()) .updateAppWidget(widgetId, createMainWidgetViews(stubText)), stubText); } @Test @NavigationModeSwitch(mode = ZERO_BUTTON) @Suppress // until b/190729479 is fixed public void testSwipeUp_with_list_widgets() { SimpleViewsFactory viewFactory = new SimpleViewsFactory(); viewFactory.viewCount = 1; Bundle args = new Bundle(); args.putBinder(EXTRA_VALUE, viewFactory.toBinder()); TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, args); try { executeSwipeUpTestWithWidget( widgetId -> { // Initialize widget RemoteViews views = createMainWidgetViews("List widget title"); views.setRemoteAdapter(android.R.id.list, new Intent(getContext(), ListViewService.class)); AppWidgetManager.getInstance(getContext()).updateAppWidget(widgetId, views); verifyWidget(viewFactory.getLabel(0)); }, widgetId -> { // Update widget viewFactory.viewCount = 2; AppWidgetManager.getInstance(getContext()) .notifyAppWidgetViewDataChanged(widgetId, android.R.id.list); }, viewFactory.getLabel(1) ); } finally { TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, new Bundle()); } } private void executeSwipeUpTestWithWidget(IntConsumer widgetIdCreationCallback, IntConsumer updateBeforeSwipeUp, String finalWidgetText) { try { LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(false); // Make sure the widget is big enough to show a list of items info.minSpanX = 2; info.minSpanY = 2; info.spanX = 2; info.spanY = 2; AtomicInteger widgetId = new AtomicInteger(); commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext) .addItem(() -> { LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, true); item.screenId = FIRST_SCREEN_ID; widgetId.set(item.appWidgetId); return item; })); assertTrue("Widget is not present", mLauncher.goHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null); // Verify widget id widgetIdCreationCallback.accept(widgetId.get()); // Go to overview once so that all views are initialized and cached startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); // Track view creations mInitTracker.startTracking(); startTestActivity(2); LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState(); // Update widget updateBeforeSwipeUp.accept(widgetId.get()); launchedAppState.switchToOverview(); assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount); // Widget is updated when going home mInitTracker.disableLog(); mLauncher.goHome(); verifyWidget(finalWidgetText); assertNotEquals(1, mInitTracker.viewInitCount); } finally { mConfigMap.clear(); } } private void verifyWidget(String text) { assertNotNull("Widget not updated", UiDevice.getInstance(getInstrumentation()) .wait(Until.findObject(By.text(text)), DEFAULT_UI_TIMEOUT)); } private RemoteViews createMainWidgetViews(String title) { Context c = getContext(); int layoutId = c.getResources().getIdentifier( "test_layout_widget_list", "layout", c.getPackageName()); RemoteViews views = new RemoteViews(c.getPackageName(), layoutId); views.setTextViewText(android.R.id.text1, title); return views; } private class InitTracker implements Answer { public int viewInitCount = 0; public boolean log = true; @Override public Object answer(InvocationOnMock invocation) throws Throwable { Exception ex = new Exception(); boolean found = false; for (StackTraceElement ste : ex.getStackTrace()) { if ("<init>".equals(ste.getMethodName()) && View.class.getName().equals(ste.getClassName())) { found = true; break; } } if (found) { viewInitCount++; if (log) { Log.d("InitTracker", "New view inflated", ex); } } return invocation.callRealMethod(); } public void disableLog() { log = false; } public void startTracking() { ViewConfiguration vc = ViewConfiguration.get(mTargetContext); ViewConfiguration spyVC = spy(vc); mConfigMap.put(mConfigMap.keyAt(mConfigMap.indexOfValue(vc)), spyVC); doAnswer(this).when(spyVC).getScaledTouchSlop(); } } }