diff options
Diffstat (limited to 'tests/src/com/android/launcher3/util/LauncherModelHelper.java')
-rw-r--r-- | tests/src/com/android/launcher3/util/LauncherModelHelper.java | 569 |
1 files changed, 569 insertions, 0 deletions
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java new file mode 100644 index 0000000000..c9b63aeb11 --- /dev/null +++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -0,0 +1,569 @@ +/* + * 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 androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseOutputStream; +import android.os.Process; +import android.provider.Settings; +import android.test.mock.MockContentResolver; +import android.util.ArrayMap; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.uiautomator.UiDevice; + +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.ItemInstallQueue; +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.pm.InstallSessionHelper; +import com.android.launcher3.pm.UserCache; +import com.android.launcher3.testing.TestInformationProvider; +import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider; +import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.widget.custom.CustomWidgetManager; + +import org.mockito.ArgumentCaptor; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +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 = testContext().getPackageName(); + public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2"; + + // 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<>(); + private final MockContentResolver mMockResolver = new MockContentResolver(); + public final TestLauncherProvider provider; + public final SanboxModelContext sandboxContext; + + public final long defaultProfileId; + + private BgDataModel mDataModel; + private AllAppsList mAllAppsList; + + public LauncherModelHelper() { + Context context = getApplicationContext(); + // System settings cache content provider. Ensure that they are statically initialized + Settings.Secure.getString(context.getContentResolver(), "test"); + Settings.System.getString(context.getContentResolver(), "test"); + Settings.Global.getString(context.getContentResolver(), "test"); + + provider = new TestLauncherProvider(); + sandboxContext = new SanboxModelContext(); + defaultProfileId = UserCache.INSTANCE.get(sandboxContext) + .getSerialNumberForUser(Process.myUserHandle()); + setupProvider(LauncherProvider.AUTHORITY, provider); + } + + protected void setupProvider(String authority, ContentProvider provider) { + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = authority; + providerInfo.applicationInfo = sandboxContext.getApplicationInfo(); + provider.attachInfo(sandboxContext, providerInfo); + mMockResolver.addProvider(providerInfo.authority, provider); + doReturn(providerInfo) + .when(sandboxContext.mPm) + .resolveContentProvider(eq(authority), anyInt()); + } + + public LauncherModel getModel() { + return LauncherAppState.getInstance(sandboxContext).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; + } + + public void destroy() { + // When destroying the context, make sure that the model thread is blocked, so that no + // new jobs get posted while we are cleaning up + CountDownLatch l1 = new CountDownLatch(1); + CountDownLatch l2 = new CountDownLatch(1); + MODEL_EXECUTOR.execute(() -> { + l1.countDown(); + waitOrThrow(l2); + }); + waitOrThrow(l1); + sandboxContext.onDestroy(); + l2.countDown(); + } + + private void waitOrThrow(CountDownLatch latch) { + try { + latch.await(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 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 { + BgDataModel bgDataModel = getBgDataModel(); + AllAppsList allAppsList = getAllAppsList(); + + MODEL_EXECUTOR.submit(() -> { + // Copy apk from resources to a local file and install from there. + Resources resources = testContext().getResources(); + int resId = resources.getIdentifier( + resourceName, "raw", testContext().getPackageName()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + resources.openRawResource(resId)))) { + 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(sandboxContext, + (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, defaultProfileId, 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, defaultProfileId, 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, defaultProfileId, 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) { + int id = LauncherSettings.Settings.call(sandboxContext.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) { + 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); + } + } + + sandboxContext.getContentResolver().insert(contentUri, values); + } + + public int[][][] createGrid(int[][][] typeArray) { + return createGrid(typeArray, 1); + } + + public int[][][] createGrid(int[][][] typeArray, int startScreen) { + LauncherSettings.Settings.call(sandboxContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); + LauncherSettings.Settings.call(sandboxContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); + return createGrid(typeArray, startScreen, defaultProfileId); + } + + /** + * 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) { + 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(sandboxContext.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 { + InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext); + idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; + idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; + + UiDevice.getInstance(getInstrumentation()).executeShellCommand( + "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY); + ContentProvider cp = new TestInformationProvider() { + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + builder.build(new OutputStreamWriter(bos)); + outputStream.write(bos.toByteArray()); + outputStream.flush(); + outputStream.close(); + return pipe[0]; + } catch (Exception e) { + throw new FileNotFoundException(e.getMessage()); + } + } + }; + setupProvider(TEST_PROVIDER_AUTHORITY, cp); + return this; + } + + /** + * Loads the model in memory synchronously + */ + public void loadModelSync() throws ExecutionException, InterruptedException { + Callbacks mockCb = new Callbacks() { }; + Executors.MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get(); + + Executors.MODEL_EXECUTOR.submit(() -> { }).get(); + Executors.MAIN_EXECUTOR.submit(() -> { }).get(); + Executors.MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get(); + } + + /** + * 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; + } + } + + public static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + success = false; + } + } + } + return success; + } + + public class SanboxModelContext extends SandboxContext { + + private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>(); + private final PackageManager mPm; + private final File mDbDir; + + SanboxModelContext() { + super(ApplicationProvider.getApplicationContext(), + UserCache.INSTANCE, InstallSessionHelper.INSTANCE, + LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, + DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, + SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, + ItemInstallQueue.INSTANCE); + mPm = spy(getBaseContext().getPackageManager()); + mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); + } + + public SanboxModelContext allow(MainThreadInitializedObject object) { + mAllowedObjects.add(object); + return this; + } + + @Override + public File getDatabasePath(String name) { + if (!mDbDir.exists()) { + mDbDir.mkdirs(); + } + return new File(mDbDir, name); + } + + @Override + public ContentResolver getContentResolver() { + return mMockResolver; + } + + @Override + public void onDestroy() { + if (deleteContents(mDbDir)) { + mDbDir.delete(); + } + super.onDestroy(); + } + + + @Override + protected <T> T createObject(ObjectProvider<T> provider) { + return spy(provider.get(this)); + } + + @Override + public PackageManager getPackageManager() { + return mPm; + } + + @Override + public Object getSystemService(String name) { + Object service = mSpiedServices.get(name); + return service != null ? service : super.getSystemService(name); + } + + public <T> T spyService(Class<T> tClass) { + String name = getSystemServiceName(tClass); + Object service = mSpiedServices.get(name); + if (service != null) { + return (T) service; + } + + T result = spy(getSystemService(tClass)); + mSpiedServices.put(name, result); + return result; + } + } + + private static Context testContext() { + return getInstrumentation().getContext(); + } +} |