summaryrefslogtreecommitdiff
path: root/tests/src
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src')
-rw-r--r--tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java221
-rw-r--r--tests/src/com/android/launcher3/model/BackupRestoreTest.java211
-rw-r--r--tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java151
-rw-r--r--tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java215
-rw-r--r--tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java133
-rw-r--r--tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java278
-rw-r--r--tests/src/com/android/launcher3/model/LoaderCursorTest.java229
-rw-r--r--tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java234
-rw-r--r--tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java83
-rw-r--r--tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java115
-rw-r--r--tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java53
-rw-r--r--tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java182
-rw-r--r--tests/src/com/android/launcher3/util/LauncherModelHelper.java569
-rw-r--r--tests/src/com/android/launcher3/util/ReflectionHelpers.java58
14 files changed, 2732 insertions, 0 deletions
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
new file mode 100644
index 0000000000..16f024e421
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -0,0 +1,221 @@
+package com.android.launcher3.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link AddWorkspaceItemsTask}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AddWorkspaceItemsTaskTest {
+
+ private final ComponentName mComponent1 = new ComponentName("a", "b");
+ private final ComponentName mComponent2 = new ComponentName("b", "b");
+
+ private Context mTargetContext;
+ private InvariantDeviceProfile mIdp;
+ private LauncherAppState mAppState;
+ private LauncherModelHelper mModelHelper;
+
+ private IntArray mExistingScreens;
+ private IntArray mNewScreens;
+ private IntSparseArrayMap<GridOccupancy> mScreenOccupancy;
+
+ @Before
+ public void setup() {
+ mModelHelper = new LauncherModelHelper();
+ mTargetContext = mModelHelper.sandboxContext;
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+ mIdp.numColumns = mIdp.numRows = 5;
+ mAppState = LauncherAppState.getInstance(mTargetContext);
+
+ mExistingScreens = new IntArray();
+ mScreenOccupancy = new IntSparseArrayMap<>();
+ mNewScreens = new IntArray();
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ private AddWorkspaceItemsTask newTask(ItemInfo... items) {
+ List<Pair<ItemInfo, Object>> list = new ArrayList<>();
+ for (ItemInfo item : items) {
+ list.add(Pair.create(item, null));
+ }
+ return new AddWorkspaceItemsTask(list);
+ }
+
+ @Test
+ public void testFindSpaceForItem_prefers_second() throws Exception {
+ mIdp.isSplitDisplay = false;
+
+ // First screen has only one hole of size 1
+ int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+ // Second screen has 2 holes of sizes 3x2 and 2x3
+ setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+
+ int[] spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
+ assertEquals(1, spaceFound[0]);
+ assertTrue(mScreenOccupancy.get(spaceFound[0])
+ .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
+
+ // Find a larger space
+ spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3);
+ assertEquals(2, spaceFound[0]);
+ assertTrue(mScreenOccupancy.get(spaceFound[0])
+ .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3));
+ }
+
+ @Test
+ public void testFindSpaceForItem_prefers_third_on_split_display() throws Exception {
+ mIdp.isSplitDisplay = true;
+ // First screen has only one hole of size 1
+ int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+ // Second screen has 2 holes of sizes 3x2 and 2x3
+ setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+
+ int[] spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
+ // For split display, it picks the next screen, even if there is enough space
+ // on previous screen
+ assertEquals(2, spaceFound[0]);
+ assertTrue(mScreenOccupancy.get(spaceFound[0])
+ .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
+ }
+
+ @Test
+ public void testFindSpaceForItem_adds_new_screen() throws Exception {
+ // First screen has 2 holes of sizes 3x2 and 2x3
+ setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+
+ IntArray oldScreens = mExistingScreens.clone();
+ int[] spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3);
+ assertFalse(oldScreens.contains(spaceFound[0]));
+ assertTrue(mNewScreens.contains(spaceFound[0]));
+ }
+
+ @Test
+ public void testAddItem_existing_item_ignored() throws Exception {
+ WorkspaceItemInfo info = new WorkspaceItemInfo();
+ info.intent = new Intent().setComponent(mComponent1);
+
+ // Setup a screen with a hole
+ setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+ // Nothing was added
+ assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty());
+ }
+
+ @Test
+ public void testAddItem_some_items_added() throws Exception {
+ Callbacks callbacks = mock(Callbacks.class);
+ Executors.MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(callbacks)).get();
+
+ WorkspaceItemInfo info = new WorkspaceItemInfo();
+ info.intent = new Intent().setComponent(mComponent1);
+
+ WorkspaceItemInfo info2 = new WorkspaceItemInfo();
+ info2.intent = new Intent().setComponent(mComponent2);
+
+ // Setup a screen with a hole
+ setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+ mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run();
+ ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
+ ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
+
+ // only info2 should be added because info was already added to the workspace
+ // in setupWorkspaceWithHoles()
+ verify(callbacks).bindAppsAdded(any(IntArray.class), notAnimated.capture(),
+ animated.capture());
+ assertTrue(notAnimated.getValue().isEmpty());
+
+ assertEquals(1, animated.getValue().size());
+ assertTrue(animated.getValue().contains(info2));
+ }
+
+ private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception {
+ return mModelHelper.executeSimpleTask(
+ model -> writeWorkspaceWithHoles(model, startId, screenId, holes));
+ }
+
+ private int writeWorkspaceWithHoles(
+ BgDataModel bgDataModel, int startId, int screenId, Rect... holes) {
+ GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows);
+ occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true);
+ for (Rect r : holes) {
+ occupancy.markCells(r, false);
+ }
+
+ mExistingScreens.add(screenId);
+ mScreenOccupancy.append(screenId, occupancy);
+
+ for (int x = 0; x < mIdp.numColumns; x++) {
+ for (int y = 0; y < mIdp.numRows; y++) {
+ if (!occupancy.cells[x][y]) {
+ continue;
+ }
+
+ WorkspaceItemInfo info = new WorkspaceItemInfo();
+ info.intent = new Intent().setComponent(mComponent1);
+ info.id = startId++;
+ info.screenId = screenId;
+ info.cellX = x;
+ info.cellY = y;
+ info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ bgDataModel.addItem(mTargetContext, info, false);
+
+ ContentWriter writer = new ContentWriter(mTargetContext);
+ info.writeToValues(writer);
+ writer.put(Favorites._ID, info.id);
+ mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI,
+ writer.getValues(mTargetContext));
+ }
+ }
+ return startId;
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/BackupRestoreTest.java b/tests/src/com/android/launcher3/model/BackupRestoreTest.java
new file mode 100644
index 0000000000..41914de1ab
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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.launcher3.model;
+
+import static android.content.pm.PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+import static android.os.Process.myUserHandle;
+
+import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.NO__ICON;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.ReflectionHelpers.getField;
+import static com.android.launcher3.util.ReflectionHelpers.setField;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.app.backup.BackupManager;
+import android.content.pm.PackageInstaller;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.LongSparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.SafeCloseable;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify backup and restore flow.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BackupRestoreTest {
+
+ private static final int PER_USER_RANGE = 200000;
+
+
+ private long mCurrentMyProfileId;
+ private long mOldMyProfileId;
+
+ private long mCurrentWorkProfileId;
+ private long mOldWorkProfileId;
+
+ private BackupManager mBackupManager;
+ private LauncherModelHelper mModelHelper;
+ private SQLiteDatabase mDb;
+ private InvariantDeviceProfile mIdp;
+
+ private UserHandle mWorkUserHandle;
+
+ private SafeCloseable mUserChangeListener;
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+
+ mCurrentMyProfileId = mModelHelper.defaultProfileId;
+ mOldMyProfileId = mCurrentMyProfileId + 1;
+ mCurrentWorkProfileId = mOldMyProfileId + 1;
+ mOldWorkProfileId = mCurrentWorkProfileId + 1;
+
+ mWorkUserHandle = UserHandle.getUserHandleForUid(PER_USER_RANGE);
+ mUserChangeListener = UserCache.INSTANCE.get(mModelHelper.sandboxContext)
+ .addUserChangeListener(() -> { });
+
+ setupUserManager();
+ setupBackupManager();
+ RestoreDbTask.setPending(mModelHelper.sandboxContext);
+ mDb = mModelHelper.provider.getDb();
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mModelHelper.sandboxContext);
+
+ }
+
+ @After
+ public void tearDown() {
+ mUserChangeListener.close();
+ mModelHelper.destroy();
+ }
+
+ private void setupUserManager() {
+ UserCache cache = UserCache.INSTANCE.get(mModelHelper.sandboxContext);
+ synchronized (cache) {
+ LongSparseArray<UserHandle> users = getField(cache, "mUsers");
+ users.clear();
+ users.put(mCurrentMyProfileId, myUserHandle());
+ users.put(mCurrentWorkProfileId, mWorkUserHandle);
+
+ ArrayMap<UserHandle, Long> userMap = getField(cache, "mUserToSerialMap");
+ userMap.clear();
+ userMap.put(myUserHandle(), mCurrentMyProfileId);
+ userMap.put(mWorkUserHandle, mCurrentWorkProfileId);
+ }
+ }
+
+ private void setupBackupManager() {
+ mBackupManager = spy(new BackupManager(mModelHelper.sandboxContext));
+ doReturn(myUserHandle()).when(mBackupManager)
+ .getUserForAncestralSerialNumber(eq(mOldMyProfileId));
+ doReturn(mWorkUserHandle).when(mBackupManager)
+ .getUserForAncestralSerialNumber(eq(mOldWorkProfileId));
+ }
+
+ @Test
+ public void testOnCreateDbIfNotExists_CreatesBackup() {
+ assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
+ }
+
+ @Test
+ public void testOnRestoreSessionWithValidCondition_PerformsRestore() throws Exception {
+ setupBackup();
+ verifyTableIsFilled(BACKUP_TABLE_NAME, false);
+ verifyTableIsEmpty(TABLE_NAME);
+ createRestoreSession();
+ verifyTableIsFilled(TABLE_NAME, true);
+ }
+
+ private void setupBackup() {
+ createTableUsingOldProfileId();
+ // setup grid for main user on first screen
+ mModelHelper.createGrid(new int[][][]{{
+ { APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
+ { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
+ { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
+ { APP_ICON, SHORTCUT, SHORTCUT, APP_ICON},
+ }}, 1, mOldMyProfileId);
+ // setup grid for work profile on second screen
+ mModelHelper.createGrid(new int[][][]{{
+ { NO__ICON, APP_ICON, SHORTCUT, SHORTCUT},
+ { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
+ { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
+ { APP_ICON, SHORTCUT, SHORTCUT, NO__ICON},
+ }}, 2, mOldWorkProfileId);
+ // simulates the creation of backup upon restore
+ new GridBackupTable(mModelHelper.sandboxContext, mDb, mIdp.numDatabaseHotseatIcons,
+ mIdp.numColumns, mIdp.numRows).doBackup(
+ mOldMyProfileId, GridBackupTable.OPTION_REQUIRES_SANITIZATION);
+ // reset favorites table
+ createTableUsingOldProfileId();
+ }
+
+ private void verifyTableIsEmpty(String tableName) {
+ assertEquals(0, getCount(mDb, "SELECT * FROM " + tableName));
+ }
+
+ private void verifyTableIsFilled(String tableName, boolean sanitized) {
+ assertEquals(sanitized ? 12 : 13, getCount(mDb,
+ "SELECT * FROM " + tableName + " WHERE profileId = "
+ + (sanitized ? mCurrentMyProfileId : mOldMyProfileId)));
+ assertEquals(10, getCount(mDb, "SELECT * FROM " + tableName + " WHERE profileId = "
+ + (sanitized ? mCurrentWorkProfileId : mOldWorkProfileId)));
+ }
+
+ private void createTableUsingOldProfileId() {
+ // simulates the creation of favorites table on old device
+ dropTable(mDb, TABLE_NAME);
+ addTableToDb(mDb, mOldMyProfileId, false);
+ }
+
+ private void createRestoreSession() throws Exception {
+ final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ final PackageInstaller installer = mModelHelper.sandboxContext.getPackageManager()
+ .getPackageInstaller();
+ final int sessionId = installer.createSession(params);
+ final PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId);
+ setField(info, "installReason", INSTALL_REASON_DEVICE_RESTORE);
+ // TODO: (b/148410677) we should verify the following call instead
+ // InstallSessionHelper.INSTANCE.get(getContext()).restoreDbIfApplicable(info);
+ RestoreDbTask.restoreIfPossible(mModelHelper.sandboxContext,
+ mModelHelper.provider.getHelper(), mBackupManager);
+ }
+
+ private static int getCount(SQLiteDatabase db, String sql) {
+ try (Cursor c = db.rawQuery(sql, null)) {
+ return c.getCount();
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
new file mode 100644
index 0000000000..dba0a4063f
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -0,0 +1,151 @@
+package com.android.launcher3.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Tests for {@link CacheDataUpdatedTask}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CacheDataUpdatedTaskTest {
+
+ private static final String NEW_LABEL_PREFIX = "new-label-";
+
+ private LauncherModelHelper mModelHelper;
+
+ @Before
+ public void setup() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.initializeData("cache_data_updated_task_data");
+
+ // Add placeholder entries in the cache to simulate update
+ Context context = mModelHelper.sandboxContext;
+ IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
+ CachingLogic<ItemInfo> placeholderLogic = new CachingLogic<ItemInfo>() {
+ @Override
+ public ComponentName getComponent(ItemInfo info) {
+ return info.getTargetComponent();
+ }
+
+ @Override
+ public UserHandle getUser(ItemInfo info) {
+ return info.user;
+ }
+
+ @Override
+ public CharSequence getLabel(ItemInfo info) {
+ return NEW_LABEL_PREFIX + info.id;
+ }
+
+ @NonNull
+ @Override
+ public BitmapInfo loadIcon(Context context, ItemInfo info) {
+ return BitmapInfo.of(Bitmap.createBitmap(1, 1, Config.ARGB_8888), Color.RED);
+ }
+ };
+
+ UserManager um = context.getSystemService(UserManager.class);
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+ iconCache.addIconToDBAndMemCache(info, placeholderLogic, new PackageInfo(),
+ um.getSerialNumberForUser(info.user), true);
+ }
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ private CacheDataUpdatedTask newTask(int op, String... pkg) {
+ return new CacheDataUpdatedTask(op, Process.myUserHandle(),
+ new HashSet<>(Arrays.asList(pkg)));
+ }
+
+ @Test
+ public void testCacheUpdate_update_apps() throws Exception {
+ // Clear all icons from apps list so that its easy to check what was updated
+ for (AppInfo info : mModelHelper.getAllAppsList().data) {
+ info.bitmap = BitmapInfo.LOW_RES_INFO;
+ }
+
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+
+ // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
+ // is not updated
+ verifyUpdate(1, 2);
+
+ // Verify that only app1 var updated in allAppsList
+ assertFalse(mModelHelper.getAllAppsList().data.isEmpty());
+ for (AppInfo info : mModelHelper.getAllAppsList().data) {
+ if (info.componentName.getPackageName().equals("app1")) {
+ assertFalse(info.bitmap.isNullOrLowRes());
+ } else {
+ assertTrue(info.bitmap.isNullOrLowRes());
+ }
+ }
+ }
+
+ @Test
+ public void testSessionUpdate_ignores_normal_apps() throws Exception {
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+
+ // app1 has no restored shortcuts. Verify that nothing was updated.
+ verifyUpdate();
+ }
+
+ @Test
+ public void testSessionUpdate_updates_pending_apps() throws Exception {
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+
+ // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
+ // were updated
+ verifyUpdate(5, 6);
+ }
+
+ private void verifyUpdate(Integer... idsUpdated) {
+ HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+ if (updates.contains(info.id)) {
+ assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
+ assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
+ } else {
+ assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
+ assertTrue(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
new file mode 100644
index 0000000000..d849c8fff9
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2017 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.model;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Tests for {@link DbDowngradeHelper}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DbDowngradeHelperTest {
+
+ private static final String SCHEMA_FILE = "test_schema.json";
+ private static final String DB_FILE = "test.db";
+
+ private Context mContext;
+ private File mSchemaFile;
+ private File mDbFile;
+
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mSchemaFile = mContext.getFileStreamPath(SCHEMA_FILE);
+ mDbFile = mContext.getDatabasePath(DB_FILE);
+ }
+
+ @Test
+ public void testDowngradeSchemaMatchesVersion() throws Exception {
+ mSchemaFile.delete();
+ assertFalse(mSchemaFile.exists());
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, 0, mContext);
+ assertEquals(LauncherProvider.SCHEMA_VERSION, DbDowngradeHelper.parse(mSchemaFile).version);
+ }
+
+ @Test
+ public void testUpdateSchemaFile() throws Exception {
+ // Setup mock resources
+ Resources res = spy(mContext.getResources());
+ Resources myRes = getContext().getResources();
+ doAnswer(i -> myRes.openRawResource(
+ myRes.getIdentifier("db_schema_v10", "raw", getContext().getPackageName())))
+ .when(res).openRawResource(R.raw.downgrade_schema);
+ Context context = spy(mContext);
+ when(context.getResources()).thenReturn(res);
+
+ mSchemaFile.delete();
+ assertFalse(mSchemaFile.exists());
+
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, 10, context);
+ assertTrue(mSchemaFile.exists());
+ assertEquals(10, DbDowngradeHelper.parse(mSchemaFile).version);
+
+ // Schema is updated on version upgrade
+ assertTrue(mSchemaFile.setLastModified(0));
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, 11, context);
+ assertNotSame(0, mSchemaFile.lastModified());
+
+ // Schema is not updated when version is same
+ assertTrue(mSchemaFile.setLastModified(0));
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, 10, context);
+ assertEquals(0, mSchemaFile.lastModified());
+
+ // Schema is not updated on version downgrade
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, 3, context);
+ assertEquals(0, mSchemaFile.lastModified());
+ }
+
+ @Test
+ public void testDowngrade_success_v24() throws Exception {
+ setupTestDb();
+
+ TestOpenHelper helper = new TestOpenHelper(24);
+ assertEquals(24, helper.getReadableDatabase().getVersion());
+ helper.close();
+ }
+
+ @Test
+ public void testDowngrade_success_v22() throws Exception {
+ setupTestDb();
+
+ SQLiteOpenHelper helper = new TestOpenHelper(22);
+ assertEquals(22, helper.getWritableDatabase().getVersion());
+
+ // Check column does not exist
+ try (Cursor c = helper.getWritableDatabase().query(Favorites.TABLE_NAME,
+ null, null, null, null, null, null)) {
+ assertEquals(-1, c.getColumnIndex(Favorites.OPTIONS));
+
+ // Check data is present
+ assertEquals(10, c.getCount());
+ }
+ helper.close();
+
+ helper = new DatabaseHelper(mContext, DB_FILE, false) {
+ @Override
+ public void onOpen(SQLiteDatabase db) { }
+ };
+ assertEquals(LauncherProvider.SCHEMA_VERSION, helper.getWritableDatabase().getVersion());
+
+ try (Cursor c = helper.getWritableDatabase().query(Favorites.TABLE_NAME,
+ null, null, null, null, null, null)) {
+ // Check column exists
+ assertNotSame(-1, c.getColumnIndex(Favorites.OPTIONS));
+
+ // Check data is present
+ assertEquals(10, c.getCount());
+ }
+ helper.close();
+ }
+
+ @Test(expected = DowngradeFailException.class)
+ public void testDowngrade_fail_v20() throws Exception {
+ setupTestDb();
+
+ TestOpenHelper helper = new TestOpenHelper(20);
+ helper.getReadableDatabase().getVersion();
+ }
+
+ private void setupTestDb() throws Exception {
+ mSchemaFile.delete();
+ mDbFile.delete();
+
+ DbDowngradeHelper.updateSchemaFile(mSchemaFile, LauncherProvider.SCHEMA_VERSION, mContext);
+
+ DatabaseHelper dbHelper = new DatabaseHelper(mContext, DB_FILE, false) {
+ @Override
+ public void onOpen(SQLiteDatabase db) { }
+ };
+ // Insert mock data
+ for (int i = 0; i < 10; i++) {
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, i);
+ values.put(Favorites.TITLE, "title " + i);
+ dbHelper.getWritableDatabase().insert(Favorites.TABLE_NAME, null, values);
+ }
+ dbHelper.close();
+ }
+
+ private class TestOpenHelper extends SQLiteOpenHelper {
+
+ public TestOpenHelper(int version) {
+ super(mContext, DB_FILE, null, version);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase sqLiteDatabase) {
+ throw new RuntimeException("DB should already be created");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ throw new RuntimeException("Only downgrade supported");
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ try {
+ DbDowngradeHelper.parse(mSchemaFile).onDowngrade(db, oldVersion, newVersion);
+ } catch (Exception e) {
+ throw new DowngradeFailException(e);
+ }
+ }
+ }
+
+ private static class DowngradeFailException extends RuntimeException {
+ public DowngradeFailException(Exception e) {
+ super(e);
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
new file mode 100644
index 0000000000..004ed06b32
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.model;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionParams;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for layout parser for remote layout
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DefaultLayoutProviderTest {
+
+ private LauncherModelHelper mModelHelper;
+ private Context mTargetContext;
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mTargetContext = mModelHelper.sandboxContext;
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ @Test
+ public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
+ writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
+ .putApp(TEST_PACKAGE, TEST_ACTIVITY));
+
+ // Verify one item in hotseat
+ assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+ ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+ assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
+ assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
+ }
+
+ @Test
+ public void testCustomProfileLoaded_with_folder() throws Exception {
+ writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .build());
+
+ // Verify folder
+ assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+ ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+ assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
+ assertEquals(3, ((FolderInfo) info).contents.size());
+ }
+
+ @Test
+ public void testCustomProfileLoaded_with_folder_custom_title() throws Exception {
+ writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder("CustomFolder")
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .addApp(TEST_PACKAGE, TEST_ACTIVITY)
+ .build());
+
+ // Verify folder
+ assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+ ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+ assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
+ assertEquals(3, ((FolderInfo) info).contents.size());
+ assertEquals("CustomFolder", info.title.toString());
+ }
+
+ @Test
+ public void testCustomProfileLoaded_with_widget() throws Exception {
+ String pendingAppPkg = "com.test.pending";
+
+ // Add a placeholder session info so that the widget exists
+ SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+ params.setAppPackageName(pendingAppPkg);
+ params.setAppIcon(BitmapInfo.LOW_RES_ICON);
+
+ PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
+ installer.createSession(params);
+
+ writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
+ .putWidget(pendingAppPkg, "PlaceholderWidget", 2, 2));
+
+ // Verify widget
+ assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
+ ItemInfo info = mModelHelper.getBgDataModel().appWidgets.get(0);
+ assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
+ assertEquals(2, info.spanX);
+ assertEquals(2, info.spanY);
+ }
+
+ private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
+ mModelHelper.setupDefaultLayoutProvider(builder).loadModelSync();
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
new file mode 100644
index 0000000000..005389e5bb
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -0,0 +1,278 @@
+/*
+ * 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.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_CONTENT_URI;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+/** Unit tests for {@link GridSizeMigrationTaskV2} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GridSizeMigrationTaskV2Test {
+
+ private LauncherModelHelper mModelHelper;
+ private Context mContext;
+ private SQLiteDatabase mDb;
+
+ private HashSet<String> mValidPackages;
+ private InvariantDeviceProfile mIdp;
+
+ private final String testPackage1 = "com.android.launcher3.validpackage1";
+ private final String testPackage2 = "com.android.launcher3.validpackage2";
+ private final String testPackage3 = "com.android.launcher3.validpackage3";
+ private final String testPackage4 = "com.android.launcher3.validpackage4";
+ private final String testPackage5 = "com.android.launcher3.validpackage5";
+ private final String testPackage6 = "com.android.launcher3.validpackage6";
+ private final String testPackage7 = "com.android.launcher3.validpackage7";
+ private final String testPackage8 = "com.android.launcher3.validpackage8";
+ private final String testPackage9 = "com.android.launcher3.validpackage9";
+ private final String testPackage10 = "com.android.launcher3.validpackage10";
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mContext = mModelHelper.sandboxContext;
+ mDb = mModelHelper.provider.getDb();
+
+ mValidPackages = new HashSet<>();
+ mValidPackages.add(TEST_PACKAGE);
+ mValidPackages.add(testPackage1);
+ mValidPackages.add(testPackage2);
+ mValidPackages.add(testPackage3);
+ mValidPackages.add(testPackage4);
+ mValidPackages.add(testPackage5);
+ mValidPackages.add(testPackage6);
+ mValidPackages.add(testPackage7);
+ mValidPackages.add(testPackage8);
+ mValidPackages.add(testPackage9);
+ mValidPackages.add(testPackage10);
+
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+ long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+ Process.myUserHandle());
+ dropTable(mDb, LauncherSettings.Favorites.TMP_TABLE);
+ LauncherSettings.Favorites.addTableToDb(mDb, userSerial, false,
+ LauncherSettings.Favorites.TMP_TABLE);
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ @Test
+ public void testMigration() throws Exception {
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ -1,
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ };
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage5, 5, TMP_CONTENT_URI);
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage6, 6, TMP_CONTENT_URI);
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage8, 8, TMP_CONTENT_URI);
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage9, 9, TMP_CONTENT_URI);
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 3, testPackage10, 10, TMP_CONTENT_URI);
+
+ int[] destHotseatItems = {
+ -1,
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2),
+ -1,
+ };
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
+
+ mIdp.numDatabaseHotseatIcons = 4;
+ mIdp.numColumns = 4;
+ mIdp.numRows = 4;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate(mIdp);
+
+ // Check hotseat items
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null);
+ assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 3);
+ assertTrue(c.getString(intentIndex).contains(testPackage4));
+ c.close();
+
+ // Check workspace items
+ c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY,
+ LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_DESKTOP, null, null, null);
+ intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX);
+ int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY);
+
+ HashMap<String, Point> locMap = new HashMap<>();
+ while (c.moveToNext()) {
+ locMap.put(
+ Intent.parseUri(c.getString(intentIndex), 0).getPackage(),
+ new Point(c.getInt(cellXIndex), c.getInt(cellYIndex)));
+ }
+ c.close();
+
+ assertEquals(locMap.size(), 6);
+ assertEquals(new Point(0, 2), locMap.get(testPackage8));
+ assertEquals(new Point(0, 3), locMap.get(testPackage6));
+ assertEquals(new Point(1, 3), locMap.get(testPackage10));
+ assertEquals(new Point(2, 3), locMap.get(testPackage5));
+ assertEquals(new Point(3, 3), locMap.get(testPackage9));
+ }
+
+ @Test
+ public void migrateToLargerHotseat() {
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 2, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ };
+
+ int numSrcDatabaseHotseatIcons = srcHotseatItems.length;
+ mIdp.numDatabaseHotseatIcons = 6;
+ mIdp.numColumns = 4;
+ mIdp.numRows = 4;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate(mIdp);
+
+ // Check hotseat items
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null);
+ assertEquals(c.getCount(), numSrcDatabaseHotseatIcons);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 3);
+ assertTrue(c.getString(intentIndex).contains(testPackage4));
+
+ c.close();
+ }
+
+ @Test
+ public void migrateFromLargerHotseat() {
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ -1,
+ mModelHelper.addItem(SHORTCUT, 2, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 5, HOTSEAT, 0, 0, testPackage5, 5, TMP_CONTENT_URI),
+ };
+
+ mIdp.numDatabaseHotseatIcons = 4;
+ mIdp.numColumns = 4;
+ mIdp.numRows = 4;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate(mIdp);
+
+ // Check hotseat items
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, LauncherSettings.Favorites.SCREEN, null);
+ assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 3);
+ assertTrue(c.getString(intentIndex).contains(testPackage4));
+
+ c.close();
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
new file mode 100644
index 0000000000..6444ef6927
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.model;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CELLX;
+import static com.android.launcher3.LauncherSettings.Favorites.CELLY;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON_PACKAGE;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON_RESOURCE;
+import static com.android.launcher3.LauncherSettings.Favorites.INTENT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID;
+import static com.android.launcher3.LauncherSettings.Favorites.RESTORED;
+import static com.android.launcher3.LauncherSettings.Favorites.SCREEN;
+import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
+import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.MatrixCursor;
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.PackageManagerHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link LoaderCursor}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LoaderCursorTest {
+
+ private LauncherModelHelper mModelHelper;
+ private LauncherAppState mApp;
+
+ private MatrixCursor mCursor;
+ private InvariantDeviceProfile mIDP;
+ private Context mContext;
+
+ private LoaderCursor mLoaderCursor;
+
+ @Before
+ public void setup() {
+ mModelHelper = new LauncherModelHelper();
+ mContext = mModelHelper.sandboxContext;
+ mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
+ mApp = LauncherAppState.getInstance(mContext);
+
+ mCursor = new MatrixCursor(new String[] {
+ ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
+ _ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
+ SCREEN, CELLX, CELLY, RESTORED, INTENT
+ });
+
+ UserManagerState ums = new UserManagerState();
+ mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
+ ums.allUsers.put(0, Process.myUserHandle());
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ private void initCursor(int itemType, String title) {
+ mCursor.newRow()
+ .add(_ID, 1)
+ .add(PROFILE_ID, 0)
+ .add(ITEM_TYPE, itemType)
+ .add(TITLE, title)
+ .add(CONTAINER, CONTAINER_DESKTOP);
+ }
+
+ @Test
+ public void getAppShortcutInfo_dontAllowMissing_invalidComponent() {
+ initCursor(ITEM_TYPE_APPLICATION, "");
+ assertTrue(mLoaderCursor.moveToNext());
+ ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
+ assertNull(mLoaderCursor.getAppShortcutInfo(
+ new Intent().setComponent(cn), false /* allowMissingTarget */, true));
+ }
+
+ @Test
+ public void getAppShortcutInfo_dontAllowMissing_validComponent() throws Exception {
+ ComponentName cn = new ComponentName(getContext(), TEST_ACTIVITY);
+ initCursor(ITEM_TYPE_APPLICATION, "");
+ assertTrue(mLoaderCursor.moveToNext());
+
+ WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+ mLoaderCursor.getAppShortcutInfo(
+ new Intent().setComponent(cn), false /* allowMissingTarget */, true))
+ .get();
+ assertNotNull(info);
+ assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
+ }
+
+ @Test
+ public void getAppShortcutInfo_allowMissing_invalidComponent() throws Exception {
+ initCursor(ITEM_TYPE_APPLICATION, "");
+ assertTrue(mLoaderCursor.moveToNext());
+
+ ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
+ WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+ mLoaderCursor.getAppShortcutInfo(
+ new Intent().setComponent(cn), true /* allowMissingTarget */, true))
+ .get();
+ assertNotNull(info);
+ assertTrue(PackageManagerHelper.isLauncherAppTarget(info.getIntent()));
+ }
+
+ @Test
+ public void loadSimpleShortcut() {
+ initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
+ assertTrue(mLoaderCursor.moveToNext());
+
+ WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
+ assertTrue(mApp.getIconCache().isDefaultIcon(info.bitmap, info.user));
+ assertEquals("my-shortcut", info.title);
+ assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
+ }
+
+ @Test
+ public void checkItemPlacement_outsideBounds() {
+ mIDP.numRows = 4;
+ mIDP.numColumns = 4;
+ mIDP.numDatabaseHotseatIcons = 3;
+
+ // Item outside screen bounds are not placed
+ assertFalse(mLoaderCursor.checkItemPlacement(
+ newItemInfo(4, 4, 1, 1, CONTAINER_DESKTOP, 1)));
+ }
+
+ @Test
+ public void checkItemPlacement_overlappingItems() {
+ mIDP.numRows = 4;
+ mIDP.numColumns = 4;
+ mIDP.numDatabaseHotseatIcons = 3;
+
+ // Overlapping mItems are not placed
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
+ assertFalse(mLoaderCursor.checkItemPlacement(
+ newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
+
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2)));
+ assertFalse(mLoaderCursor.checkItemPlacement(
+ newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2)));
+
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(1, 1, 1, 1, CONTAINER_DESKTOP, 1)));
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(2, 2, 2, 2, CONTAINER_DESKTOP, 1)));
+
+ assertFalse(mLoaderCursor.checkItemPlacement(
+ newItemInfo(3, 2, 1, 2, CONTAINER_DESKTOP, 1)));
+ }
+
+ @Test
+ public void checkItemPlacement_hotseat() {
+ mIDP.numRows = 4;
+ mIDP.numColumns = 4;
+ mIDP.numDatabaseHotseatIcons = 3;
+
+ // Hotseat mItems are only placed based on screenId
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
+ assertTrue(mLoaderCursor.checkItemPlacement(
+ newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 2)));
+
+ assertFalse(mLoaderCursor.checkItemPlacement(
+ newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3)));
+ }
+
+ private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY,
+ int container, int screenId) {
+ ItemInfo info = new ItemInfo();
+ info.cellX = cellX;
+ info.cellY = cellY;
+ info.spanX = spanX;
+ info.spanY = spanY;
+ info.container = container;
+ info.screenId = screenId;
+ return info;
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000000..42c9f11a52
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.launcher3.model;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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.util.Executors;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.TestUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ModelMultiCallbacksTest {
+
+ private LauncherModelHelper mModelHelper;
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mModelHelper.destroy();
+ TestUtil.uninstallDummyApp();
+ }
+
+ @Test
+ public void testTwoCallbacks_loadedTogether() throws Exception {
+ setupWorkspacePages(3);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1));
+
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+
+ // Add a new callback
+ cb1.reset();
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ cb2.mPageToBindSync = IntSet.wrap(2);
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2));
+
+ waitForLoaderAndTempMainThread();
+ assertFalse(cb1.bindStarted);
+ cb2.verifySynchronouslyBound(3);
+
+ // Remove callbacks
+ cb1.reset();
+ cb2.reset();
+
+ // No effect on callbacks when removing an callback
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
+ waitForLoaderAndTempMainThread();
+ assertNull(cb1.mPendingTasks);
+ assertNull(cb2.mPendingTasks);
+
+ // Reloading only loads registered callbacks
+ mModelHelper.getModel().startLoader();
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+ assertNull(cb2.mPendingTasks);
+ }
+
+ @Test
+ public void testTwoCallbacks_receiveUpdates() throws Exception {
+ TestUtil.uninstallDummyApp();
+
+ setupWorkspacePages(1);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1));
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2));
+ waitForLoaderAndTempMainThread();
+
+ assertTrue(cb1.allApps().contains(TEST_PACKAGE));
+ assertTrue(cb2.allApps().contains(TEST_PACKAGE));
+
+ // Install package 1
+ TestUtil.installDummyApp();
+ mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
+ assertTrue(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
+
+ // Uninstall package 2
+ TestUtil.uninstallDummyApp();
+ mModelHelper.getModel().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ assertFalse(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
+ assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
+
+ // Unregister a callback and verify updates no longer received
+ Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
+ TestUtil.installDummyApp();
+ mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+
+ // cb2 didn't get the update
+ assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
+ assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
+ }
+
+ private void waitForLoaderAndTempMainThread() throws Exception {
+ Executors.MAIN_EXECUTOR.submit(() -> { }).get();
+ Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+ Executors.MAIN_EXECUTOR.submit(() -> { }).get();
+ }
+
+ private void setupWorkspacePages(int pageCount) throws Exception {
+ // Create a layout with 3 pages
+ LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+ for (int i = 0; i < pageCount; i++) {
+ builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+ }
+ mModelHelper.setupDefaultLayoutProvider(builder);
+ }
+
+ private abstract static class MyCallbacks implements Callbacks {
+
+ final List<ItemInfo> mItems = new ArrayList<>();
+ IntSet mPageToBindSync = IntSet.wrap(0);
+ IntSet mPageBoundSync = new IntSet();
+ RunnableList mPendingTasks;
+ AppInfo[] mAppInfos;
+ boolean bindStarted;
+
+ MyCallbacks() { }
+
+ @Override
+ public void startBinding() {
+ bindStarted = true;
+ }
+
+ @Override
+ public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+ mPageBoundSync = boundPages;
+ mPendingTasks = pendingTasks;
+ }
+
+ @Override
+ public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+ mItems.addAll(shortcuts);
+ }
+
+ @Override
+ public void bindAllApplications(AppInfo[] apps, int flags) {
+ mAppInfos = apps;
+ }
+
+ @Override
+ public IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
+ return mPageToBindSync;
+ }
+
+ public void reset() {
+ mItems.clear();
+ mPageBoundSync = new IntSet();
+ mPendingTasks = null;
+ mAppInfos = null;
+ bindStarted = false;
+ }
+
+ public void verifySynchronouslyBound(int totalItems) {
+ // Verify that the requested page is bound synchronously
+ assertTrue(bindStarted);
+ assertEquals(mPageToBindSync, mPageBoundSync);
+ assertEquals(mItems.size(), 1);
+ assertEquals(IntSet.wrap(mItems.get(0).screenId), mPageBoundSync);
+ assertNotNull(mPendingTasks);
+
+ // Verify that all other pages are bound properly
+ mPendingTasks.executeAllAndDestroy();
+ assertEquals(mItems.size(), totalItems);
+ }
+
+ public Set<String> allApps() {
+ return Arrays.stream(mAppInfos)
+ .map(ai -> ai.getTargetComponent().getPackageName())
+ .collect(Collectors.toSet());
+ }
+
+ public void verifyApps(String... apps) {
+ assertTrue(allApps().containsAll(Arrays.asList(apps)));
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
new file mode 100644
index 0000000000..519191e251
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -0,0 +1,83 @@
+package com.android.launcher3.model;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Tests for {@link PackageInstallStateChangedTask}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PackageInstallStateChangedTaskTest {
+
+ private LauncherModelHelper mModelHelper;
+
+ @Before
+ public void setup() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.initializeData("package_install_state_change_task_data");
+ }
+
+ @After
+ public void tearDown() {
+ mModelHelper.destroy();
+ }
+
+ private PackageInstallStateChangedTask newTask(String pkg, int progress) {
+ int state = PackageInstallInfo.STATUS_INSTALLING;
+ PackageInstallInfo installInfo = new PackageInstallInfo(pkg, state, progress,
+ android.os.Process.myUserHandle());
+ return new PackageInstallStateChangedTask(installInfo);
+ }
+
+ @Test
+ public void testSessionUpdate_ignore_installed() throws Exception {
+ mModelHelper.executeTaskForTest(newTask("app1", 30));
+
+ // No shortcuts were updated
+ verifyProgressUpdate(0);
+ }
+
+ @Test
+ public void testSessionUpdate_shortcuts_updated() throws Exception {
+ mModelHelper.executeTaskForTest(newTask("app3", 30));
+
+ verifyProgressUpdate(30, 5, 6, 7);
+ }
+
+ @Test
+ public void testSessionUpdate_widgets_updated() throws Exception {
+ mModelHelper.executeTaskForTest(newTask("app4", 30));
+
+ verifyProgressUpdate(30, 8, 9);
+ }
+
+ private void verifyProgressUpdate(int progress, Integer... idsUpdated) {
+ HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+ if (info instanceof WorkspaceItemInfo) {
+ assertEquals(updates.contains(info.id) ? progress: 100,
+ ((WorkspaceItemInfo) info).getProgressLevel());
+ } else {
+ assertEquals(updates.contains(info.id) ? progress: -1,
+ ((LauncherAppWidgetInfo) info).installProgress);
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
new file mode 100644
index 0000000000..48305eebdf
--- /dev/null
+++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.provider;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link RestoreDbTask}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RestoreDbTaskTest {
+
+ @Test
+ public void testGetProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
+ assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
+ }
+
+ @Test
+ public void testMigrateProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+ // Add some mock data
+ for (int i = 0; i < 5; i++) {
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, i);
+ values.put(Favorites.TITLE, "item " + i);
+ db.insert(Favorites.TABLE_NAME, null, values);
+ }
+ // Verify item add
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+
+ new RestoreDbTask().migrateProfileId(db, 42, 33);
+
+ // verify data migrated
+ assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+ }
+
+ @Test
+ public void testChangeDefaultColumn() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+ // Add some mock data
+ for (int i = 0; i < 5; i++) {
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, i);
+ values.put(Favorites.TITLE, "item " + i);
+ db.insert(Favorites.TABLE_NAME, null, values);
+ }
+ // Verify default column is 42
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+
+ new RestoreDbTask().changeDefaultColumn(db, 33);
+
+ // Verify default value changed
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, 100);
+ values.put(Favorites.TITLE, "item 100");
+ db.insert(Favorites.TABLE_NAME, null, values);
+ assertEquals(1, getCount(db, "select * from favorites where profileId = 33"));
+ }
+
+ private int getCount(SQLiteDatabase db, String sql) {
+ try (Cursor c = db.rawQuery(sql, null)) {
+ return c.getCount();
+ }
+ }
+
+ private class MyDatabaseHelper extends DatabaseHelper {
+
+ private final long mProfileId;
+
+ MyDatabaseHelper(long profileId) {
+ super(InstrumentationRegistry.getInstrumentation().getTargetContext(), null, false);
+ mProfileId = profileId;
+ }
+
+ @Override
+ public long getDefaultUserSerial() {
+ return mProfileId;
+ }
+
+ @Override
+ protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
+
+ protected void onEmptyDbCreated() { }
+ }
+}
diff --git a/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java b/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java
new file mode 100644
index 0000000000..fd86cf1144
--- /dev/null
+++ b/tests/src/com/android/launcher3/secondarydisplay/SDLauncherTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 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.secondarydisplay;
+
+import static androidx.test.core.app.ActivityScenario.launch;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.intent.Intents;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link SecondaryDisplayLauncher}
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SDLauncherTest {
+
+ @Before
+ public void setUp() {
+ Intents.init();
+ }
+
+ @After
+ public void tearDown() {
+ Intents.release();
+ }
+
+ @Test
+ public void testAllAppsListOpens() {
+ ActivityScenario<SecondaryDisplayLauncher> launcher =
+ launch(SecondaryDisplayLauncher.class);
+ launcher.onActivity(l -> l.showAppDrawer(true));
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
new file mode 100644
index 0000000000..4e21dce021
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
@@ -0,0 +1,182 @@
+/**
+ * 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 android.text.TextUtils;
+import android.util.Pair;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper class to build xml for Launcher Layout
+ */
+public class LauncherLayoutBuilder {
+
+ // Object Tags
+ private static final String TAG_WORKSPACE = "workspace";
+ private static final String TAG_AUTO_INSTALL = "autoinstall";
+ private static final String TAG_FOLDER = "folder";
+ private static final String TAG_APPWIDGET = "appwidget";
+ private static final String TAG_EXTRA = "extra";
+
+ private static final String ATTR_CONTAINER = "container";
+ private static final String ATTR_RANK = "rank";
+
+ private static final String ATTR_PACKAGE_NAME = "packageName";
+ private static final String ATTR_CLASS_NAME = "className";
+ private static final String ATTR_TITLE = "title";
+ private static final String ATTR_TITLE_TEXT = "titleText";
+ private static final String ATTR_SCREEN = "screen";
+
+ // x and y can be specified as negative integers, in which case -1 represents the
+ // last row / column, -2 represents the second last, and so on.
+ private static final String ATTR_X = "x";
+ private static final String ATTR_Y = "y";
+ private static final String ATTR_SPAN_X = "spanX";
+ private static final String ATTR_SPAN_Y = "spanY";
+
+ private static final String ATTR_CHILDREN = "children";
+
+
+ // Style attrs -- "Extra"
+ private static final String ATTR_KEY = "key";
+ private static final String ATTR_VALUE = "value";
+
+ private static final String CONTAINER_DESKTOP = "desktop";
+ private static final String CONTAINER_HOTSEAT = "hotseat";
+
+ private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
+
+ public Location atHotseat(int rank) {
+ Location l = new Location();
+ l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
+ l.items.put(ATTR_RANK, Integer.toString(rank));
+ return l;
+ }
+
+ public Location atWorkspace(int x, int y, int screen) {
+ Location l = new Location();
+ l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
+ l.items.put(ATTR_X, Integer.toString(x));
+ l.items.put(ATTR_Y, Integer.toString(y));
+ l.items.put(ATTR_SCREEN, Integer.toString(screen));
+ return l;
+ }
+
+ public String build() throws IOException {
+ StringWriter writer = new StringWriter();
+ build(writer);
+ return writer.toString();
+ }
+
+ public void build(Writer writer) throws IOException {
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(writer);
+
+ serializer.startDocument("UTF-8", true);
+ serializer.startTag(null, TAG_WORKSPACE);
+ writeNodes(serializer, mNodes);
+ serializer.endTag(null, TAG_WORKSPACE);
+ serializer.endDocument();
+ serializer.flush();
+ }
+
+ private static void writeNodes(XmlSerializer serializer,
+ ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
+ for (Pair<String, HashMap<String, Object>> node : nodes) {
+ ArrayList<Pair<String, HashMap<String, Object>>> children = null;
+
+ serializer.startTag(null, node.first);
+ for (Map.Entry<String, Object> attr : node.second.entrySet()) {
+ if (ATTR_CHILDREN.equals(attr.getKey())) {
+ children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
+ } else {
+ serializer.attribute(null, attr.getKey(), (String) attr.getValue());
+ }
+ }
+
+ if (children != null) {
+ writeNodes(serializer, children);
+ }
+ serializer.endTag(null, node.first);
+ }
+ }
+
+ public class Location {
+
+ final HashMap<String, Object> items = new HashMap<>();
+
+ public LauncherLayoutBuilder putApp(String packageName, String className) {
+ items.put(ATTR_PACKAGE_NAME, packageName);
+ items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
+ mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
+ return LauncherLayoutBuilder.this;
+ }
+
+ public LauncherLayoutBuilder putWidget(String packageName, String className,
+ int spanX, int spanY) {
+ items.put(ATTR_PACKAGE_NAME, packageName);
+ items.put(ATTR_CLASS_NAME, className);
+ items.put(ATTR_SPAN_X, Integer.toString(spanX));
+ items.put(ATTR_SPAN_Y, Integer.toString(spanY));
+ mNodes.add(Pair.create(TAG_APPWIDGET, items));
+ return LauncherLayoutBuilder.this;
+ }
+
+ public FolderBuilder putFolder(int titleResId) {
+ items.put(ATTR_TITLE, Integer.toString(titleResId));
+ return putFolder();
+ }
+
+ public FolderBuilder putFolder(String title) {
+ items.put(ATTR_TITLE_TEXT, title);
+ return putFolder();
+ }
+
+ private FolderBuilder putFolder() {
+ FolderBuilder folderBuilder = new FolderBuilder();
+ items.put(ATTR_CHILDREN, folderBuilder.mChildren);
+ mNodes.add(Pair.create(TAG_FOLDER, items));
+ return folderBuilder;
+ }
+ }
+
+ public class FolderBuilder {
+
+ final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
+
+ public FolderBuilder addApp(String packageName, String className) {
+ HashMap<String, Object> items = new HashMap<>();
+ items.put(ATTR_PACKAGE_NAME, packageName);
+ items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
+ mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
+ return this;
+ }
+
+ public LauncherLayoutBuilder build() {
+ return LauncherLayoutBuilder.this;
+ }
+ }
+}
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();
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/ReflectionHelpers.java b/tests/src/com/android/launcher3/util/ReflectionHelpers.java
new file mode 100644
index 0000000000..d89975de8a
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/ReflectionHelpers.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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 java.lang.reflect.Field;
+
+public class ReflectionHelpers {
+
+ /**
+ * Reflectively get the value of a field.
+ *
+ * @param object Target object.
+ * @param fieldName The field name.
+ * @param <R> The return type.
+ * @return Value of the field on the object.
+ */
+ public static <R> R getField(Object object, String fieldName) {
+ try {
+ Field field = object.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (R) field.get(object);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Reflectively set the value of a field.
+ *
+ * @param object Target object.
+ * @param fieldName The field name.
+ * @param fieldNewValue New value.
+ */
+ public static void setField(Object object, String fieldName, Object fieldNewValue) {
+ try {
+ Field field = object.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(object, fieldNewValue);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}