diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2018-11-07 16:54:02 -0800 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2018-11-12 10:49:59 -0800 |
commit | 06a07e9748d07941be5f49f3388e94e5fcdeb840 (patch) | |
tree | e83cb6d85badbe22ef33e225e4d883932e25da1f /robolectric_tests | |
parent | f1982fcaae1c24c93eab2309cb06905d25a90c00 (diff) | |
download | android_packages_apps_Trebuchet-06a07e9748d07941be5f49f3388e94e5fcdeb840.tar.gz android_packages_apps_Trebuchet-06a07e9748d07941be5f49f3388e94e5fcdeb840.tar.bz2 android_packages_apps_Trebuchet-06a07e9748d07941be5f49f3388e94e5fcdeb840.zip |
Moving come tests to Roboelectric
> Fixing resource loading in robo tests
Change-Id: Id5b8a0e4916a2a200da7a41b03f19846834beb1f
Diffstat (limited to 'robolectric_tests')
11 files changed, 1143 insertions, 2 deletions
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk index 501176475..a57b97a9c 100644 --- a/robolectric_tests/Android.mk +++ b/robolectric_tests/Android.mk @@ -29,6 +29,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ LOCAL_JAVA_LIBRARIES := \ platform-robolectric-3.6.1-prebuilt +LOCAL_JAVA_RESOURCE_DIRS := resources config + LOCAL_INSTRUMENTATION_FOR := Launcher3 LOCAL_MODULE_TAGS := optional diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties new file mode 100644 index 000000000..782b8cb75 --- /dev/null +++ b/robolectric_tests/config/robolectric.properties @@ -0,0 +1,2 @@ +manifest=packages/apps/Launcher3/AndroidManifest.xml +sdk=28
\ No newline at end of file diff --git a/robolectric_tests/resources/cache_data_updated_task_data.txt b/robolectric_tests/resources/cache_data_updated_task_data.txt new file mode 100644 index 000000000..819968709 --- /dev/null +++ b/robolectric_tests/resources/cache_data_updated_task_data.txt @@ -0,0 +1,28 @@ +# Model data used by CacheDataUpdatedTaskTest + +classMap s com.android.launcher3.ShortcutInfo + +# Items for the BgDataModel + +# App shortcuts +bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1 +bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2 +bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3 +bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4 + +# Auto install app shortcut +bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5 +bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6 + +# Custom shortcuts +bgItem s itemType=1 title=app1-shrt intent=component=app1/class3 id=7 +bgItem s itemType=1 title=app4-shrt intent=component=app4/class1 id=8 + +# Restored custom shortcut +bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=9 +bgItem s itemType=1 status=1 title=app5-shrt intent=component=app5/class1 id=10 + +allApps componentName=app1/class1 intent=component=app1/class1 +allApps componentName=app1/class2 intent=component=app1/class2 +allApps componentName=app2/class1 intent=component=app2/class1 +allApps componentName=app2/class2 intent=component=app2/class2
\ No newline at end of file diff --git a/robolectric_tests/resources/package_install_state_change_task_data.txt b/robolectric_tests/resources/package_install_state_change_task_data.txt new file mode 100644 index 000000000..84f9c161e --- /dev/null +++ b/robolectric_tests/resources/package_install_state_change_task_data.txt @@ -0,0 +1,24 @@ +# Model data used by PackageInstallStateChangeTaskTest + +classMap s com.android.launcher3.ShortcutInfo +classMap w com.android.launcher3.LauncherAppWidgetInfo + +# Items for the BgDataModel + +# App shortcuts +bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1 +bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2 +bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3 +bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4 + +# Promise icons for app3 +bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5 +bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6 +bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7 + +# Promise icon for app4 +bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8 + +# Widget +bgItem w providerName=app4/provider1 id=9 +bgItem w providerName=app5/provider1 id=10
\ No newline at end of file diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java new file mode 100644 index 000000000..fd31033db --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java @@ -0,0 +1,191 @@ +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.verify; + +import android.content.ComponentName; +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.net.Uri; +import android.util.Pair; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherProvider; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.util.GridOccupancy; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link AddWorkspaceItemsTask} + */ +@RunWith(RobolectricTestRunner.class) +public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { + + private final ComponentName mComponent1 = new ComponentName("a", "b"); + private final ComponentName mComponent2 = new ComponentName("b", "b"); + + private IntArray existingScreens; + private IntArray newScreens; + private IntSparseArrayMap<GridOccupancy> screenOccupancy; + + @Before + public void initData() throws Exception { + existingScreens = new IntArray(); + screenOccupancy = new IntSparseArrayMap<>(); + newScreens = new IntArray(); + + idp.numColumns = 5; + idp.numRows = 5; + } + + 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) { + + @Override + protected void updateScreens(Context context, IntArray workspaceScreens) { } + }; + } + + @Test + public void testFindSpaceForItem_prefers_second() { + // 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(appState, bgDataModel, existingScreens, newScreens, 1, 1); + assertEquals(2, spaceFound[0]); + assertTrue(screenOccupancy.get(spaceFound[0]) + .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1)); + + // Find a larger space + spaceFound = newTask() + .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3); + assertEquals(2, spaceFound[0]); + assertTrue(screenOccupancy.get(spaceFound[0]) + .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3)); + } + + @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)); + commitScreensToDb(); + + IntArray oldScreens = existingScreens.clone(); + int[] spaceFound = newTask() + .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3); + assertFalse(oldScreens.contains(spaceFound[0])); + assertTrue(newScreens.contains(spaceFound[0])); + } + + @Test + public void testAddItem_existing_item_ignored() throws Exception { + ShortcutInfo info = new ShortcutInfo(); + info.intent = new Intent().setComponent(mComponent1); + + // Setup a screen with a hole + setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3)); + commitScreensToDb(); + + // Nothing was added + assertTrue(executeTaskForTest(newTask(info)).isEmpty()); + } + + @Test + public void testAddItem_some_items_added() throws Exception { + ShortcutInfo info = new ShortcutInfo(); + info.intent = new Intent().setComponent(mComponent1); + + ShortcutInfo info2 = new ShortcutInfo(); + info2.intent = new Intent().setComponent(mComponent2); + + // Setup a screen with a hole + setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3)); + commitScreensToDb(); + + 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) { + GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows); + occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true); + for (Rect r : holes) { + occupancy.markCells(r, false); + } + + existingScreens.add(screenId); + screenOccupancy.append(screenId, occupancy); + + for (int x = 0; x < idp.numColumns; x++) { + for (int y = 0; y < idp.numRows; y++) { + if (!occupancy.cells[x][y]) { + continue; + } + + ShortcutInfo info = new ShortcutInfo(); + 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(targetContext, info, false); + } + } + return startId; + } + + private void commitScreensToDb() throws Exception { + LauncherSettings.Settings.call(targetContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); + + Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); + // Clear the table + ops.add(ContentProviderOperation.newDelete(uri).build()); + int count = existingScreens.size(); + for (int i = 0; i < count; i++) { + ContentValues v = new ContentValues(); + int screenId = existingScreens.get(i); + v.put(LauncherSettings.WorkspaceScreens._ID, screenId); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build()); + } + targetContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, ops); + } +} diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java new file mode 100644 index 000000000..92d065e30 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java @@ -0,0 +1,222 @@ +package com.android.launcher3.model; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Color; +import android.os.Process; +import android.os.UserHandle; + +import com.android.launcher3.AllAppsList; +import com.android.launcher3.AppFilter; +import com.android.launcher3.AppInfo; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherModel.Callbacks; +import com.android.launcher3.LauncherModel.ModelUpdateTask; +import com.android.launcher3.LauncherProvider; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.IconCache; +import com.android.launcher3.icons.cache.CachingLogic; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Provider; +import com.android.launcher3.util.TestLauncherProvider; + +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowContentResolver; +import org.robolectric.shadows.ShadowLog; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; + +import androidx.annotation.NonNull; + +/** + * Base class for writing tests for Model update tasks. + */ +public class BaseModelUpdateTaskTestCase { + + public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>(); + private TestLauncherProvider mProvider; + + public Context targetContext; + public UserHandle myUser; + + public InvariantDeviceProfile idp; + public LauncherAppState appState; + public LauncherModel model; + public ModelWriter modelWriter; + public MyIconCache iconCache; + + public BgDataModel bgDataModel; + public AllAppsList allAppsList; + public Callbacks callbacks; + + @Before + public void setUp() throws Exception { + ShadowLog.stream = System.out; + + mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class); + ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider); + + callbacks = mock(Callbacks.class); + appState = mock(LauncherAppState.class); + model = mock(LauncherModel.class); + modelWriter = mock(ModelWriter.class); + + when(appState.getModel()).thenReturn(model); + when(model.getWriter(anyBoolean(), anyBoolean())).thenReturn(modelWriter); + when(model.getCallback()).thenReturn(callbacks); + + myUser = Process.myUserHandle(); + + bgDataModel = new BgDataModel(); + targetContext = RuntimeEnvironment.application; + + idp = new InvariantDeviceProfile(); + iconCache = new MyIconCache(targetContext, idp); + + allAppsList = new AllAppsList(iconCache, new AppFilter()); + + when(appState.getIconCache()).thenReturn(iconCache); + when(appState.getInvariantDeviceProfile()).thenReturn(idp); + when(appState.getContext()).thenReturn(targetContext); + } + + /** + * Synchronously executes the task and returns all the UI callbacks posted. + */ + public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception { + when(model.isModelLoaded()).thenReturn(true); + + Executor mockExecutor = mock(Executor.class); + + task.init(appState, model, bgDataModel, allAppsList, mockExecutor); + task.run(); + ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); + verify(mockExecutor, atLeast(0)).execute(captor.capture()); + + return captor.getAllValues(); + } + + /** + * Initializes mock data for the test. + */ + public void initializeData(String resourceName) throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + this.getClass().getResourceAsStream(resourceName)))) { + String line; + HashMap<String, Class> classMap = new HashMap<>(); + while((line = reader.readLine()) != null) { + line = line.trim(); + if (line.startsWith("#") || line.isEmpty()) { + continue; + } + String[] commands = line.split(" "); + switch (commands[0]) { + case "classMap": + classMap.put(commands[1], Class.forName(commands[2])); + break; + case "bgItem": + bgDataModel.addItem(targetContext, + (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false); + break; + case "allApps": + allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null); + break; + } + } + } + } + + private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception { + HashMap<String, Field> cache = fieldCache.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(); + } + fieldCache.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 static class MyIconCache extends IconCache { + + private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>(); + + public MyIconCache(Context context, InvariantDeviceProfile idp) { + super(context, idp); + } + + @Override + protected <T> CacheEntry cacheLocked( + @NonNull ComponentName componentName, + UserHandle user, @NonNull Provider<T> infoProvider, + @NonNull CachingLogic<T> cachingLogic, + boolean usePackageIcon, boolean useLowResIcon) { + CacheEntry entry = mCache.get(new ComponentKey(componentName, user)); + if (entry == null) { + entry = new CacheEntry(); + getDefaultIcon(user).applyTo(entry); + } + return entry; + } + + public void addCache(ComponentName key, String title) { + CacheEntry entry = new CacheEntry(); + entry.icon = newIcon(); + entry.color = Color.RED; + entry.title = title; + mCache.put(new ComponentKey(key, Process.myUserHandle()), entry); + } + + public Bitmap newIcon() { + return Bitmap.createBitmap(1, 1, Config.ARGB_8888); + } + } +} diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java new file mode 100644 index 000000000..f17eac740 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -0,0 +1,95 @@ +package com.android.launcher3.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.ShortcutInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * Tests for {@link CacheDataUpdatedTask} + */ +@RunWith(RobolectricTestRunner.class) +public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase { + + private static final String NEW_LABEL_PREFIX = "new-label-"; + + @Before + public void initData() throws Exception { + initializeData("/cache_data_updated_task_data.txt"); + // Add dummy entries in the cache to simulate update + for (ItemInfo info : bgDataModel.itemsIdMap) { + iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id); + } + } + + private CacheDataUpdatedTask newTask(int op, String... pkg) { + return new CacheDataUpdatedTask(op, myUser, 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 : allAppsList.data) { + info.iconBitmap = null; + } + + 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(allAppsList.data.isEmpty()); + for (AppInfo info : allAppsList.data) { + if (info.componentName.getPackageName().equals("app1")) { + assertNotNull(info.iconBitmap); + } else { + assertNull(info.iconBitmap); + } + } + } + + @Test + public void testSessionUpdate_ignores_normal_apps() throws Exception { + 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 { + 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 : bgDataModel.itemsIdMap) { + if (updates.contains(info.id)) { + assertEquals(NEW_LABEL_PREFIX + info.id, info.title); + assertNotNull(((ShortcutInfo) info).iconBitmap); + } else { + assertNotSame(NEW_LABEL_PREFIX + info.id, info.title); + assertNull(((ShortcutInfo) info).iconBitmap); + } + } + } +} diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java new file mode 100644 index 000000000..67580ddd8 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java @@ -0,0 +1,463 @@ +package com.android.launcher3.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Point; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherProvider; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.config.FlagOverrideRule; +import com.android.launcher3.config.FlagOverrideRule.FlagOverride; +import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.TestLauncherProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowContentResolver; + +import java.util.HashSet; +import java.util.LinkedList; + +/** + * Unit tests for {@link GridSizeMigrationTask} + */ +@RunWith(RobolectricTestRunner.class) +public class GridSizeMigrationTaskTest { + + private static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; + private static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; + + private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + + private static final String TEST_PACKAGE = "com.android.launcher3.validpackage"; + + @Rule + public final FlagOverrideRule flags = new FlagOverrideRule(); + + private HashSet<String> mValidPackages; + private InvariantDeviceProfile mIdp; + private Context mContext; + private TestLauncherProvider mProvider; + + @Before + public void setUp() { + + mValidPackages = new HashSet<>(); + mValidPackages.add(TEST_PACKAGE); + mIdp = new InvariantDeviceProfile(); + mContext = RuntimeEnvironment.application; + + mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class); + ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider); + } + + @Test + public void testHotseatMigration_apps_dropped() throws Exception { + int[] hotseatItems = { + addItem(APPLICATION, 0, HOTSEAT, 0, 0), + addItem(SHORTCUT, 1, HOTSEAT, 0, 0), + -1, + addItem(SHORTCUT, 3, HOTSEAT, 0, 0), + addItem(APPLICATION, 4, HOTSEAT, 0, 0), + }; + + mIdp.numHotseatIcons = 3; + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, 5, 3) + .migrateHotseat(); + // First item is dropped as it has the least weight. + verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]); + } + + @Test + public void testHotseatMigration_shortcuts_dropped() throws Exception { + int[] hotseatItems = { + addItem(APPLICATION, 0, HOTSEAT, 0, 0), + addItem(30, 1, HOTSEAT, 0, 0), + -1, + addItem(SHORTCUT, 3, HOTSEAT, 0, 0), + addItem(10, 4, HOTSEAT, 0, 0), + }; + + mIdp.numHotseatIcons = 3; + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, 5, 3) + .migrateHotseat(); + // First item is dropped as it has the least weight. + verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]); + } + + private void verifyHotseat(int... sortedIds) { + int screenId = 0; + int total = 0; + + for (int id : sortedIds) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-101 and screen=" + screenId, null, null, null); + + if (id == -1) { + assertEquals(0, c.getCount()); + } else { + assertEquals(1, c.getCount()); + c.moveToNext(); + assertEquals(id, c.getLong(0)); + total ++; + } + c.close(); + + screenId++; + } + + // Verify that not other entry exist in the DB. + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-101", null, null, null); + assertEquals(total, c.getCount()); + c.close(); + } + + @Test + public void testWorkspace_empty_row_column_removed() throws Exception { + int[][][] ids = createGrid(new int[][][]{{ + { 0, 0, -1, 1}, + { 3, 1, -1, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Column 2 and row 2 got removed. + verifyWorkspace(new int[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }}); + } + + @Test + public void testWorkspace_new_screen_created() throws Exception { + int[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column get moved to new screen + verifyWorkspace(new int[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + @Test + public void testWorkspace_items_merged_in_next_screen() throws Exception { + int[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + },{ + { 0, 0, -1, 1}, + { 3, 1, -1, 4}, + }}); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on the 3rd + // row of the second screen + verifyWorkspace(new int[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + {ids[1][0][0], ids[1][0][1], ids[1][0][3]}, + {ids[1][1][0], ids[1][1][1], ids[1][1][3]}, + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + @Test + public void testWorkspace_items_not_merged_in_next_screen() throws Exception { + // First screen has 2 items that need to be moved, but second screen has only one + // empty space after migration (top-left corner) + int[][][] ids = createGrid(new int[][][]{{ + { 0, 0, 0, 1}, + { 3, 1, 0, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + },{ + { -1, 0, -1, 1}, + { 3, 1, -1, 4}, + { -1, -1, -1, -1}, + { 5, 2, -1, 6}, + }}); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on a new screen. + verifyWorkspace(new int[][][] {{ + {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, + {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, + }, { + { -1, ids[1][0][1], ids[1][0][3]}, + {ids[1][1][0], ids[1][1][1], ids[1][1][3]}, + {ids[1][3][0], ids[1][3][1], ids[1][3][3]}, + }, { + {ids[0][0][2], ids[0][1][2], -1}, + }}); + } + + @FlagOverride(key = "QSB_ON_FIRST_SCREEN", value = true) + @Test + public void testWorkspace_first_row_blocked() throws Exception { + // The first screen has one item on the 4th column which needs moving, as the first row + // will be kept empty. + int[][][] ids = createGrid(new int[][][]{{ + { -1, -1, -1, -1}, + { 3, 1, 7, 0}, + { 8, 7, 7, -1}, + { 5, 2, 7, -1}, + }}, 0); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 4)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on a new screen. + verifyWorkspace(new int[][][] {{ + { -1, -1, -1}, + {ids[0][1][0], ids[0][1][1], ids[0][1][2]}, + {ids[0][2][0], ids[0][2][1], ids[0][2][2]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][2]}, + }, { + {ids[0][1][3]}, + }}); + } + + @FlagOverride(key = "QSB_ON_FIRST_SCREEN", value = true) + @Test + public void testWorkspace_items_moved_to_empty_first_row() throws Exception { + // Items will get moved to the next screen to keep the first screen empty. + int[][][] ids = createGrid(new int[][][]{{ + { -1, -1, -1, -1}, + { 0, 1, 0, 0}, + { 8, 7, 7, -1}, + { 5, 6, 7, -1}, + }}, 0); + + new GridSizeMigrationTask(mContext, mIdp, mValidPackages, + new Point(4, 4), new Point(3, 3)).migrateWorkspace(); + + // Items in the second column of the first screen should get placed on a new screen. + verifyWorkspace(new int[][][] {{ + { -1, -1, -1}, + {ids[0][2][0], ids[0][2][1], ids[0][2][2]}, + {ids[0][3][0], ids[0][3][1], ids[0][3][2]}, + }, { + {ids[0][1][1], ids[0][1][0], ids[0][1][2]}, + {ids[0][1][3]}, + }}); + } + + private int[][][] createGrid(int[][][] typeArray) throws Exception { + return createGrid(typeArray, 1); + } + + /** + * Initializes the DB with dummy 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. + * @return the same grid representation where each entry is the corresponding item id. + */ + private int[][][] createGrid(int[][][] typeArray, int startScreen) throws Exception { + LauncherSettings.Settings.call(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); + 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(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); + + ContentValues v = new ContentValues(); + v.put(LauncherSettings.WorkspaceScreens._ID, screenId); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + mContext.getContentResolver().insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v); + + 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); + } + } + } + } + + IntArray allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + return ids; + } + + /** + * Verifies that the workspace items are arranged in the provided order. + * @param ids A 3d array where the first dimension represents the screen, and the rest two + * represent the workspace grid. + */ + private void verifyWorkspace(int[][][] ids) { + IntArray allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + assertEquals(ids.length, allScreens.size()); + int total = 0; + + for (int i = 0; i < ids.length; i++) { + int screenId = allScreens.get(i); + for (int y = 0; y < ids[i].length; y++) { + for (int x = 0; x < ids[i][y].length; x++) { + int id = ids[i][y][x]; + + Cursor c = mContext.getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-100 and screen=" + screenId + + " and cellX=" + x + " and cellY=" + y, null, null, null); + if (id == -1) { + assertEquals(0, c.getCount()); + } else { + assertEquals(1, c.getCount()); + c.moveToNext(); + assertEquals(String.format("Failed to verify item at %d %d, %d", i, y, x), + id, c.getLong(0)); + total++; + } + c.close(); + } + } + } + + // Verify that not other entry exist in the DB. + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{LauncherSettings.Favorites._ID}, + "container=-100", null, null, null); + assertEquals(total, c.getCount()); + c.close(); + } + + /** + * Adds a dummy item in the DB. + * @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for + * folder (where the type represents the number of items in the folder). + */ + private int addItem(int type, int screen, int container, int x, int y) throws Exception { + int id = LauncherSettings.Settings.call(mContext.getContentResolver(), + LauncherSettings.Settings.METHOD_NEW_ITEM_ID) + .getInt(LauncherSettings.Settings.EXTRA_VALUE); + + 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); + + if (type == APPLICATION || type == SHORTCUT) { + values.put(LauncherSettings.Favorites.ITEM_TYPE, type); + values.put(LauncherSettings.Favorites.INTENT, + new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).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(APPLICATION, 0, id, 0, 0); + } + } + + mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values); + return id; + } + + @Test + public void testMultiStepMigration_small_to_large() throws Exception { + MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier(); + verifier.migrate(new Point(3, 3), new Point(5, 5)); + verifier.assertCompleted(); + } + + @Test + public void testMultiStepMigration_large_to_small() throws Exception { + MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier( + 5, 5, 4, 4, + 4, 4, 3, 4 + ); + verifier.migrate(new Point(5, 5), new Point(3, 4)); + verifier.assertCompleted(); + } + + @Test + public void testMultiStepMigration_zig_zag() throws Exception { + MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier( + 5, 7, 4, 7, + 4, 7, 3, 7 + ); + verifier.migrate(new Point(5, 5), new Point(3, 7)); + verifier.assertCompleted(); + } + + private static class MultiStepMigrationTaskVerifier extends MultiStepMigrationTask { + + private final LinkedList<Point> mPoints; + + public MultiStepMigrationTaskVerifier(int... points) { + super(null, null); + + mPoints = new LinkedList<>(); + for (int i = 0; i < points.length; i += 2) { + mPoints.add(new Point(points[i], points[i + 1])); + } + } + + @Override + protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception { + assertEquals(sourceSize, mPoints.poll()); + assertEquals(nextSize, mPoints.poll()); + return false; + } + + public void assertCompleted() { + assertTrue(mPoints.isEmpty()); + } + } +} diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java new file mode 100644 index 000000000..c7b4613bd --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java @@ -0,0 +1,70 @@ +package com.android.launcher3.model; + +import static org.junit.Assert.assertEquals; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppWidgetInfo; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * Tests for {@link PackageInstallStateChangedTask} + */ +@RunWith(RobolectricTestRunner.class) +public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase { + + @Before + public void initData() throws Exception { + initializeData("/package_install_state_change_task_data.txt"); + } + + private PackageInstallStateChangedTask newTask(String pkg, int progress) { + int state = PackageInstallerCompat.STATUS_INSTALLING; + PackageInstallInfo installInfo = new PackageInstallInfo(pkg, state, progress); + return new PackageInstallStateChangedTask(installInfo); + } + + @Test + public void testSessionUpdate_ignore_installed() throws Exception { + executeTaskForTest(newTask("app1", 30)); + + // No shortcuts were updated + verifyProgressUpdate(0); + } + + @Test + public void testSessionUpdate_shortcuts_updated() throws Exception { + executeTaskForTest(newTask("app3", 30)); + + verifyProgressUpdate(30, 5, 6, 7); + } + + @Test + public void testSessionUpdate_widgets_updated() throws Exception { + 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 : bgDataModel.itemsIdMap) { + if (info instanceof ShortcutInfo) { + assertEquals(updates.contains(info.id) ? progress: 0, + ((ShortcutInfo) info).getInstallProgress()); + } else { + assertEquals(updates.contains(info.id) ? progress: -1, + ((LauncherAppWidgetInfo) info).installProgress); + } + } + } +} diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java index faec380af..8513353dc 100644 --- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java +++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java @@ -21,7 +21,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -31,7 +30,6 @@ import static org.junit.Assert.assertTrue; * Robolectric unit tests for {@link IntSet} */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = 26) public class IntSetTest { @Test diff --git a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java new file mode 100644 index 000000000..32808f572 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java @@ -0,0 +1,46 @@ +package com.android.launcher3.util; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.android.launcher3.LauncherProvider; + +/** + * An extension of LauncherProvider backed up by in-memory database. + */ +public class TestLauncherProvider extends LauncherProvider { + + @Override + public boolean onCreate() { + return true; + } + + @Override + protected synchronized void createDbIfNotExists() { + if (mOpenHelper == null) { + mOpenHelper = new MyDatabaseHelper(getContext()); + } + } + + @Override + protected void notifyListeners() { } + + private static class MyDatabaseHelper extends DatabaseHelper { + public MyDatabaseHelper(Context context) { + super(context, null, null); + initIds(); + } + + @Override + public long getDefaultUserSerial() { + return 0; + } + + @Override + protected void onEmptyDbCreated() { } + + @Override + protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { } + } +} |