Files
Flexera-Launcher/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
T
Tony Wickham b87f3cdc1c Add support for having more hotseat icons in the DB than we show
Split InvariantDeviceProfile#numHotseatIcons into two variables:
numDatabaseHotseatIcons and numShownHotseatIcons. These are generally
the same, but different DisplayOptions within the same GridOption
can choose to show different numbers of hotseat icons while sharing
the same database.

numDatabaseHotseatIcons is used for all reading/writing/migrating
purposes, while numShownHotseatIcons determines how many Hotseat
icons to show in the UI.

Test: Existing tests pass, added two new migration tests
Bug: 184789479
Bug: 171917176

Change-Id: I54583504f61a47a4444b6a637ebb7e3ab31528b7
2021-04-12 16:08:15 -07:00

441 lines
18 KiB
Java

/*
* 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.launcher3.util;
import static android.content.Intent.ACTION_CREATE_SHORTCUT;
import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Process;
import android.provider.Settings;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shadows.ShadowLooperExecutor;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.function.Function;
/**
* Utility class to help manage Launcher Model and related objects for test.
*/
public class LauncherModelHelper {
public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
public static final int NO__ICON = -1;
public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
// Authority for providing a test default-workspace-layout data.
private static final String TEST_PROVIDER_AUTHORITY =
LauncherModelHelper.class.getName().toLowerCase();
private static final int DEFAULT_BITMAP_SIZE = 10;
private static final int DEFAULT_GRID_SIZE = 4;
private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
public final TestLauncherProvider provider;
private final long mDefaultProfileId;
private BgDataModel mDataModel;
private AllAppsList mAllAppsList;
public LauncherModelHelper() {
provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
mDefaultProfileId = UserCache.INSTANCE.get(RuntimeEnvironment.application)
.getSerialNumberForUser(Process.myUserHandle());
ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
}
public LauncherModel getModel() {
return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel();
}
public synchronized BgDataModel getBgDataModel() {
if (mDataModel == null) {
mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel");
}
return mDataModel;
}
public synchronized AllAppsList getAllAppsList() {
if (mAllAppsList == null) {
mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList");
}
return mAllAppsList;
}
/**
* Synchronously executes the task and returns all the UI callbacks posted.
*/
public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
LauncherModel model = getModel();
if (!model.isModelLoaded()) {
ReflectionHelpers.setField(model, "mModelLoaded", true);
}
Executor mockExecutor = mock(Executor.class);
model.enqueueModelUpdateTask(new ModelUpdateTask() {
@Override
public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
AllAppsList allAppsList, Executor uiExecutor) {
task.init(app, model, dataModel, allAppsList, mockExecutor);
}
@Override
public void run() {
task.run();
}
});
MODEL_EXECUTOR.submit(() -> null).get();
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(mockExecutor, atLeast(0)).execute(captor.capture());
return captor.getAllValues();
}
/**
* Synchronously executes a task on the model
*/
public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception {
BgDataModel dataModel = getBgDataModel();
return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get();
}
/**
* Initializes mock data for the test.
*/
public void initializeData(String resourceName) throws Exception {
Context targetContext = RuntimeEnvironment.application;
BgDataModel bgDataModel = getBgDataModel();
AllAppsList allAppsList = getAllAppsList();
MODEL_EXECUTOR.submit(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
this.getClass().getResourceAsStream(resourceName)))) {
String line;
HashMap<String, Class> classMap = new HashMap<>();
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.startsWith("#") || line.isEmpty()) {
continue;
}
String[] commands = line.split(" ");
switch (commands[0]) {
case "classMap":
classMap.put(commands[1], Class.forName(commands[2]));
break;
case "bgItem":
bgDataModel.addItem(targetContext,
(ItemInfo) initItem(classMap.get(commands[1]), commands, 2),
false);
break;
case "allApps":
allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
break;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}).get();
}
private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
HashMap<String, Field> cache = mFieldCache.get(clazz);
if (cache == null) {
cache = new HashMap<>();
Class c = clazz;
while (c != null) {
for (Field f : c.getDeclaredFields()) {
f.setAccessible(true);
cache.put(f.getName(), f);
}
c = c.getSuperclass();
}
mFieldCache.put(clazz, cache);
}
Object item = clazz.newInstance();
for (int i = startIndex; i < fieldDef.length; i++) {
String[] fieldData = fieldDef[i].split("=", 2);
Field f = cache.get(fieldData[0]);
Class type = f.getType();
if (type == int.class || type == long.class) {
f.set(item, Integer.parseInt(fieldData[1]));
} else if (type == CharSequence.class || type == String.class) {
f.set(item, fieldData[1]);
} else if (type == Intent.class) {
if (!fieldData[1].startsWith("#Intent")) {
fieldData[1] = "#Intent;" + fieldData[1] + ";end";
}
f.set(item, Intent.parseUri(fieldData[1], 0));
} else if (type == ComponentName.class) {
f.set(item, ComponentName.unflattenFromString(fieldData[1]));
} else {
throw new Exception("Added parsing logic for "
+ f.getName() + " of type " + f.getType());
}
}
return item;
}
public int addItem(int type, int screen, int container, int x, int y) {
return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE);
}
public int addItem(int type, int screen, int container, int x, int y, long profileId) {
return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE);
}
public int addItem(int type, int screen, int container, int x, int y, String packageName) {
return addItem(type, screen, container, x, y, mDefaultProfileId, packageName);
}
public int addItem(int type, int screen, int container, int x, int y, String packageName,
int id, Uri contentUri) {
addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri);
return id;
}
/**
* Adds a mock item in the DB.
* @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
* folder (where the type represents the number of items in the folder).
*/
public int addItem(int type, int screen, int container, int x, int y, long profileId,
String packageName) {
Context context = RuntimeEnvironment.application;
int id = LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI);
return id;
}
public void addItem(int type, int screen, int container, int x, int y, long profileId,
String packageName, int id, Uri contentUri) {
Context context = RuntimeEnvironment.application;
ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites._ID, id);
values.put(LauncherSettings.Favorites.CONTAINER, container);
values.put(LauncherSettings.Favorites.SCREEN, screen);
values.put(LauncherSettings.Favorites.CELLX, x);
values.put(LauncherSettings.Favorites.CELLY, y);
values.put(LauncherSettings.Favorites.SPANX, 1);
values.put(LauncherSettings.Favorites.SPANY, 1);
values.put(LauncherSettings.Favorites.PROFILE_ID, profileId);
if (type == APP_ICON || type == SHORTCUT) {
values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
values.put(LauncherSettings.Favorites.INTENT,
new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
} else {
values.put(LauncherSettings.Favorites.ITEM_TYPE,
LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
// Add folder items.
for (int i = 0; i < type; i++) {
addItem(APP_ICON, 0, id, 0, 0, profileId);
}
}
context.getContentResolver().insert(contentUri, values);
}
public int[][][] createGrid(int[][][] typeArray) {
return createGrid(typeArray, 1);
}
public int[][][] createGrid(int[][][] typeArray, int startScreen) {
final Context context = RuntimeEnvironment.application;
LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
return createGrid(typeArray, startScreen, mDefaultProfileId);
}
/**
* Initializes the DB with mock elements to represent the provided grid structure.
* @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
* type definitions. The first dimension represents the screens and the next
* two represent the workspace grid.
* @param startScreen First screen id from where the icons will be added.
* @return the same grid representation where each entry is the corresponding item id.
*/
public int[][][] createGrid(int[][][] typeArray, int startScreen, long profileId) {
Context context = RuntimeEnvironment.application;
int[][][] ids = new int[typeArray.length][][];
for (int i = 0; i < typeArray.length; i++) {
// Add screen to DB
int screenId = startScreen + i;
// Keep the screen id counter up to date
LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
ids[i] = new int[typeArray[i].length][];
for (int y = 0; y < typeArray[i].length; y++) {
ids[i][y] = new int[typeArray[i][y].length];
for (int x = 0; x < typeArray[i][y].length; x++) {
if (typeArray[i][y][x] < 0) {
// Empty cell
ids[i][y][x] = -1;
} else {
ids[i][y][x] = addItem(
typeArray[i][y][x], screenId, DESKTOP, x, y, profileId);
}
}
}
}
return ids;
}
/**
* Sets up a mock provider to load the provided layout by default, next time the layout loads
*/
public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
throws Exception {
Context context = RuntimeEnvironment.application;
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
Settings.Secure.putString(context.getContentResolver(),
"launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
shadowOf(context.getPackageManager())
.addProviderIfNotPresent(new ComponentName("com.test", "Mock")).authority =
TEST_PROVIDER_AUTHORITY;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
builder.build(new OutputStreamWriter(bos));
Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
new ByteArrayInputStream(bos.toByteArray()));
return this;
}
/**
* Simulates an apk install with a default main activity with same class and package name
*/
public void installApp(String component) throws NameNotFoundException {
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
filter.addCategory(Intent.CATEGORY_LAUNCHER);
installApp(component, component, filter);
}
/**
* Simulates a custom shortcut install
*/
public void installCustomShortcut(String pkg, String clazz) throws NameNotFoundException {
installApp(pkg, clazz, new IntentFilter(ACTION_CREATE_SHORTCUT));
}
private void installApp(String pkg, String clazz, IntentFilter filter)
throws NameNotFoundException {
ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
ComponentName cn = new ComponentName(pkg, clazz);
spm.addActivityIfNotPresent(cn);
filter.addCategory(Intent.CATEGORY_DEFAULT);
spm.addIntentFilterForActivity(cn, filter);
}
/**
* Loads the model in memory synchronously
*/
public void loadModelSync() throws ExecutionException, InterruptedException {
// Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
// so that we can wait appropriately for the loader to complete.
ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR);
sle.setHandler(Executors.UI_HELPER_EXECUTOR.getHandler());
Callbacks mockCb = mock(Callbacks.class);
getModel().addCallbacksAndLoad(mockCb);
Executors.MODEL_EXECUTOR.submit(() -> { }).get();
Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get();
sle.setHandler(null);
getModel().removeCallbacks(mockCb);
}
/**
* An extension of LauncherProvider backed up by in-memory database.
*/
public static class TestLauncherProvider extends LauncherProvider {
@Override
public boolean onCreate() {
return true;
}
public SQLiteDatabase getDb() {
createDbIfNotExists();
return mOpenHelper.getWritableDatabase();
}
public DatabaseHelper getHelper() {
return mOpenHelper;
}
}
}