blob: 0f5a1c8589b0c72eddc8b2a922b3780dba9061db [file] [log] [blame]
/*
* 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 androidx.test.InstrumentationRegistry.getTargetContext;
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.ContentResolver;
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.filters.LargeTest;
import androidx.test.filters.Suppress;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.tapl.Background;
import com.android.launcher3.testcomponent.ListViewService;
import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory;
import com.android.launcher3.testcomponent.TestCommandReceiver;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.ui.TestViewHelpers;
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.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 ViewInflationDuringSwipeUp extends AbstractQuickStepTest {
private ContentResolver mResolver;
private SparseArray<ViewConfiguration> mConfigMap;
private InitTracker mInitTracker;
@Before
public void setUp() throws Exception {
super.setUp();
// 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));
TaplTestsLauncher3.initialize(this);
mResolver = mTargetContext.getContentResolver();
LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
// 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.getBackground().switchToOverview().dismissAllTasks();
// Track view creations
mInitTracker.startTracking();
startTestActivity(2);
mLauncher.getBackground().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 {
// Clear all existing data
LauncherSettings.Settings.call(mResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
LauncherSettings.Settings.call(mResolver,
LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, 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;
LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
addItemToScreen(item);
assertTrue("Widget is not present",
mLauncher.pressHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
int widgetId = item.appWidgetId;
// Verify widget id
widgetIdCreationCallback.accept(widgetId);
// Go to overview once so that all views are initialized and cached
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
mLauncher.getBackground().switchToOverview().dismissAllTasks();
// Track view creations
mInitTracker.startTracking();
startTestActivity(2);
Background background = mLauncher.getBackground();
// Update widget
updateBeforeSwipeUp.accept(widgetId);
background.switchToOverview();
assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);
// Widget is updated when going home
mInitTracker.disableLog();
mLauncher.pressHome();
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();
}
}
}