/* * 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 )