diff --git a/tests/Android.bp b/tests/Android.bp index 3670c37add..c329eceead 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -23,7 +23,7 @@ package { // Source code used for test filegroup { name: "launcher-tests-src", - srcs: ["src/**/*.java"], + srcs: ["src/**/*.java", "src/**/*.kt"], } // Source code used for oop test helpers diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java deleted file mode 100644 index 8a4590a388..0000000000 --- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java +++ /dev/null @@ -1,201 +0,0 @@ -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 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> 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 { - // 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_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 notAnimated = ArgumentCaptor.forClass(ArrayList.class); - ArgumentCaptor 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/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt new file mode 100644 index 0000000000..e315658d37 --- /dev/null +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt @@ -0,0 +1,325 @@ +/* + * 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.model + +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.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.* +import com.android.launcher3.util.IntArray +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.* +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.verify +import kotlin.collections.ArrayList + +/** + * Tests for [AddWorkspaceItemsTask] + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class AddWorkspaceItemsTaskTest { + + @Captor + private lateinit var animatedItemArgumentCaptor: ArgumentCaptor> + + @Captor + private lateinit var notAnimatedItemArgumentCaptor: ArgumentCaptor> + + @Mock + private lateinit var dataModelCallbacks: BgDataModel.Callbacks + + private lateinit var mTargetContext: Context + private lateinit var mIdp: InvariantDeviceProfile + private lateinit var mAppState: LauncherAppState + private lateinit var mModelHelper: LauncherModelHelper + private lateinit var mExistingScreens: IntArray + private lateinit var mNewScreens: IntArray + private lateinit var mScreenOccupancy: IntSparseArrayMap + + private val emptyScreenHoles = listOf(Rect(0, 0, 5, 5)) + private val fullScreenHoles = emptyList() + + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mModelHelper = LauncherModelHelper() + mTargetContext = mModelHelper.sandboxContext + mIdp = InvariantDeviceProfile.INSTANCE[mTargetContext] + mIdp.numRows = 5 + mIdp.numColumns = mIdp.numRows + mAppState = LauncherAppState.getInstance(mTargetContext) + mExistingScreens = IntArray() + mScreenOccupancy = IntSparseArrayMap() + mNewScreens = IntArray() + Executors.MAIN_EXECUTOR.submit { mModelHelper.model.addCallbacks(dataModelCallbacks) }.get() + } + + @After + fun tearDown() { + mModelHelper.destroy() + } + + @Test + fun justEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnFirstScreenId() { + setupWorkspacesWithHoles( + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole + // 2 holes of sizes 3x2 and 2x3 + screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + ) + + val spaceFound = newTask().findSpaceForItem( + mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 1, 1) + assertEquals(1, spaceFound[0]) + assertTrue(mScreenOccupancy[spaceFound[0]] + .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1)) + } + + @Test + fun notEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnSecondScreenId() { + setupWorkspacesWithHoles( + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole + // 2 holes of sizes 3x2 and 2x3 + screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + ) + + // Find a larger space + val spaceFound = newTask().findSpaceForItem( + mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 2, 3) + assertEquals(2, spaceFound[0]) + assertTrue(mScreenOccupancy[spaceFound[0]] + .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3)) + } + + @Test + fun notEnoughSpaceOnExistingScreens_whenFindSpaceForItem_thenReturnNewScreenId() { + setupWorkspacesWithHoles( + // 2 holes of sizes 3x2 and 2x3 + screen1 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)), + // 2 holes of sizes 1x2 and 2x2 + screen2 = listOf(Rect(1, 0, 2, 2), Rect(3, 2, 5, 4)), + ) + + val oldScreens = mExistingScreens.clone() + val spaceFound = newTask().findSpaceForItem( + mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 3, 3) + assertFalse(oldScreens.contains(spaceFound[0])) + assertTrue(mNewScreens.contains(spaceFound[0])) + } + + @Test + fun enoughSpaceOnFirstScreen_whenTaskRuns_thenAddItemToFirstScreen() { + val workspaceHoles = createWorkspaceHoles( + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 space + screen2 = listOf(Rect(2, 0, 5, 2)), // 3x2 space + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(1, addedItems.first().itemInfo.screenId) + } + + @Test + fun firstPageIsFull_whenTaskRuns_thenAddItemToSecondScreen() { + val workspaceHoles = createWorkspaceHoles( + screen1 = fullScreenHoles, + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(2, addedItems.first().itemInfo.screenId) + } + + @Test + fun firstScreenIsEmptyButSecondIsNotEmpty_whenTaskRuns_thenAddItemToSecondScreen() { + val workspaceHoles = createWorkspaceHoles( + screen1 = emptyScreenHoles, + screen2 = listOf(Rect(2, 0, 5, 2)), // 3x2 space + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(2, addedItems.first().itemInfo.screenId) + } + + @Test + fun twoEmptyMiddleScreens_whenTaskRuns_thenAddItemToThirdScreen() { + val workspaceHoles = createWorkspaceHoles( + screen1 = emptyScreenHoles, + screen2 = emptyScreenHoles, + screen3 = listOf(Rect(1, 1, 4, 4)), // 3x3 space + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(3, addedItems.first().itemInfo.screenId) + } + + @Test + fun allPagesAreFull_whenTaskRuns_thenAddItemToNewScreen() { + val workspaceHoles = createWorkspaceHoles( + screen1 = fullScreenHoles, + screen2 = fullScreenHoles, + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(3, addedItems.first().itemInfo.screenId) + } + + @Test + fun firstTwoPagesAreFull_and_ThirdPageIsEmpty_whenTaskRuns_thenAddItemToThirdPage() { + val workspaceHoles = createWorkspaceHoles( + screen1 = fullScreenHoles, + screen2 = fullScreenHoles, + screen3 = emptyScreenHoles + ) + val addedItems = testAddItems(workspaceHoles, getNewItem()) + assertEquals(1, addedItems.size) + assertEquals(3, addedItems.first().itemInfo.screenId) + } + + @Test + fun itemIsAlreadyAdded_whenTaskRun_thenIgnoreItem() { + val task = newTask(getExistingItem()) + setupWorkspacesWithHoles( + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole + ) + + // Nothing was added + assertTrue(mModelHelper.executeTaskForTest(task).isEmpty()) + } + + @Test + fun newAndExistingItems_whenTaskRun_thenAddOnlyTheNewOne() { + val newItem = getNewItem() + val workspaceHoles = createWorkspaceHoles( + screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole + ) + val addedItems = testAddItems(workspaceHoles, getExistingItem(), newItem) + assertEquals(1, addedItems.size) + val addedItem = addedItems.first() + assert(addedItem.isAnimated) + val addedItemInfo = addedItem.itemInfo + assertEquals(1, addedItemInfo.screenId) + assertEquals(newItem, addedItemInfo) + } + + private fun testAddItems( + workspaceHoles: List>, + vararg itemsToAdd: WorkspaceItemInfo + ): List { + setupWorkspaces(workspaceHoles) + mModelHelper.executeTaskForTest(newTask(*itemsToAdd))[0].run() + + verify(dataModelCallbacks).bindAppsAdded(any(), + notAnimatedItemArgumentCaptor.capture(), animatedItemArgumentCaptor.capture()) + + val addedItems = mutableListOf() + addedItems.addAll(animatedItemArgumentCaptor.value.map { AddedItem(it, true) }) + addedItems.addAll(notAnimatedItemArgumentCaptor.value.map { AddedItem(it, false) }) + return addedItems + } + + private fun setupWorkspaces(workspaceHoles: List>) { + var nextItemId = 1 + var screenId = 1 + workspaceHoles.forEach { holes -> + nextItemId = setupWorkspace(nextItemId, screenId++, *holes.toTypedArray()) + } + } + + private fun setupWorkspace(startId: Int, screenId: Int, vararg holes: Rect): Int { + return mModelHelper.executeSimpleTask { dataModel -> + writeWorkspaceWithHoles(dataModel, startId, screenId, *holes) + } + } + + private fun writeWorkspaceWithHoles( + bgDataModel: BgDataModel, + itemStartId: Int, + screenId: Int, + vararg holes: Rect, + ): Int { + var itemId = itemStartId + val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows) + occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true) + holes.forEach { holeRect -> + occupancy.markCells(holeRect, false) + } + mExistingScreens.add(screenId) + mScreenOccupancy.append(screenId, occupancy) + for (x in 0 until mIdp.numColumns) { + for (y in 0 until mIdp.numRows) { + if (!occupancy.cells[x][y]) { + continue + } + val info = getExistingItem() + info.id = itemId++ + info.screenId = screenId + info.cellX = x + info.cellY = y + info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP + bgDataModel.addItem(mTargetContext, info, false) + val writer = ContentWriter(mTargetContext) + info.writeToValues(writer) + writer.put(LauncherSettings.Favorites._ID, info.id) + mTargetContext.contentResolver.insert(LauncherSettings.Favorites.CONTENT_URI, + writer.getValues(mTargetContext)) + } + } + return itemId + } + + private fun setupWorkspacesWithHoles( + screen1: List? = null, + screen2: List? = null, + screen3: List? = null, + ) = createWorkspaceHoles(screen1, screen2, screen3) + .let(this::setupWorkspaces) + + private fun createWorkspaceHoles( + screen1: List? = null, + screen2: List? = null, + screen3: List? = null, + ): List> = listOfNotNull(screen1, screen2, screen3) + + private fun newTask(vararg items: ItemInfo): AddWorkspaceItemsTask = + items.map { Pair.create(it, Any()) } + .toMutableList() + .let(::AddWorkspaceItemsTask) + + private fun getExistingItem() = WorkspaceItemInfo() + .apply { intent = Intent().setComponent(ComponentName("a", "b")) } + + private fun getNewItem() = WorkspaceItemInfo() + .apply { intent = Intent().setComponent(ComponentName("b", "b")) } +} + +private data class AddedItem( + val itemInfo: ItemInfo, + val isAnimated: Boolean +) \ No newline at end of file