Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 68e1af2b authored by Andras Kloczl's avatar Andras Kloczl
Browse files

Separate workspace item finding logic

Extract the item finding logic from AddWorkspaceItemsTask
to a separate class and write tests.

Test: AddWorkspaceItemsTaskTest.kt , WorkspaceItemSpaceFinderTest.kt
Bug: 199160559
Change-Id: Ie1bc4fcd4f94cd7cb0601c21bbdf273452b9dd1f
parent 60dc19cb
Loading
Loading
Loading
Loading
+14 −87
Original line number Diff line number Diff line
@@ -15,22 +15,17 @@
 */
package com.android.launcher3.model;

import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;

import android.content.Intent;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageInstaller.SessionInfo;
import android.os.UserHandle;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Pair;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
@@ -41,9 +36,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.PackageManagerHelper;

import java.util.ArrayList;
@@ -58,11 +51,23 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask {

    private final List<Pair<ItemInfo, Object>> mItemList;

    private final WorkspaceItemSpaceFinder mItemSpaceFinder;

    /**
     * @param itemList items to add on the workspace
     */
    public AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList) {
        this(itemList, new WorkspaceItemSpaceFinder());
    }

    /**
     * @param itemList items to add on the workspace
     * @param itemSpaceFinder inject WorkspaceItemSpaceFinder dependency for testing
     */
    public AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList,
            WorkspaceItemSpaceFinder itemSpaceFinder) {
        mItemList = itemList;
        mItemSpaceFinder = itemSpaceFinder;
    }

    @Override
@@ -117,7 +122,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask {

            for (ItemInfo item : filteredItems) {
                // Find appropriate space for the item.
                int[] coords = findSpaceForItem(app, dataModel, workspaceScreens,
                int[] coords = mItemSpaceFinder.findSpaceForItem(app, dataModel, workspaceScreens,
                        addedWorkspaceScreensFinal, item.spanX, item.spanY);
                int screenId = coords[0];

@@ -288,82 +293,4 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
        }
        return false;
    }

    /**
     * Find a position on the screen for the given size or adds a new screen.
     * @return screenId and the coordinates for the item in an int array of size 3.
     */
    protected int[] findSpaceForItem( LauncherAppState app, BgDataModel dataModel,
            IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) {
        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();

        // Use sBgItemsIdMap as all the items are already loaded.
        synchronized (dataModel) {
            for (ItemInfo info : dataModel.itemsIdMap) {
                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
                    if (items == null) {
                        items = new ArrayList<>();
                        screenItems.put(info.screenId, items);
                    }
                    items.add(info);
                }
            }
        }

        // Find appropriate space for the item.
        int screenId = 0;
        int[] coordinates = new int[2];
        boolean found = false;

        int screenCount = workspaceScreens.size();
        // First check the preferred screen.
        IntSet screensToExclude = new IntSet();
        if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
            screensToExclude.add(FIRST_SCREEN_ID);
        }

        for (int screen = 0; screen < screenCount; screen++) {
            screenId = workspaceScreens.get(screen);
            if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen(
                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
                // We found a space for it
                found = true;
                break;
            }
        }

        if (!found) {
            // Still no position found. Add a new screen to the end.
            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);

            // Save the screen id for binding in the workspace
            workspaceScreens.add(screenId);
            addedWorkspaceScreensFinal.add(screenId);

            // If we still can't find an empty space, then God help us all!!!
            if (!findNextAvailableIconSpaceInScreen(
                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
                throw new RuntimeException("Can't find space to add the item");
            }
        }
        return new int[] {screenId, coordinates[0], coordinates[1]};
    }

    private boolean findNextAvailableIconSpaceInScreen(
            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
            int[] xy, int spanX, int spanY) {
        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();

        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
        if (occupiedPos != null) {
            for (ItemInfo r : occupiedPos) {
                occupied.markCells(r, true);
            }
        }
        return occupied.findVacantCell(xy, spanX, spanY);
    }

}
+115 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.WorkspaceLayoutManager.FIRST_SCREEN_ID;

import android.util.LongSparseArray;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;

import java.util.ArrayList;

/**
 * Utility class to help find space for new workspace items
 */
public class WorkspaceItemSpaceFinder {

    /**
     * Find a position on the screen for the given size or adds a new screen.
     *
     * @return screenId and the coordinates for the item in an int array of size 3.
     */
    public int[] findSpaceForItem(LauncherAppState app, BgDataModel dataModel,
            IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) {
        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();

        // Use sBgItemsIdMap as all the items are already loaded.
        synchronized (dataModel) {
            for (ItemInfo info : dataModel.itemsIdMap) {
                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
                    if (items == null) {
                        items = new ArrayList<>();
                        screenItems.put(info.screenId, items);
                    }
                    items.add(info);
                }
            }
        }

        // Find appropriate space for the item.
        int screenId = 0;
        int[] coordinates = new int[2];
        boolean found = false;

        int screenCount = workspaceScreens.size();
        // First check the preferred screen.
        IntSet screensToExclude = new IntSet();
        if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
            screensToExclude.add(FIRST_SCREEN_ID);
        }

        for (int screen = 0; screen < screenCount; screen++) {
            screenId = workspaceScreens.get(screen);
            if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen(
                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
                // We found a space for it
                found = true;
                break;
            }
        }

        if (!found) {
            // Still no position found. Add a new screen to the end.
            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);

            // Save the screen id for binding in the workspace
            workspaceScreens.add(screenId);
            addedWorkspaceScreensFinal.add(screenId);

            // If we still can't find an empty space, then God help us all!!!
            if (!findNextAvailableIconSpaceInScreen(
                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
                throw new RuntimeException("Can't find space to add the item");
            }
        }
        return new int[]{screenId, coordinates[0], coordinates[1]};
    }

    private boolean findNextAvailableIconSpaceInScreen(
            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
            int[] xy, int spanX, int spanY) {
        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();

        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
        if (occupiedPos != null) {
            for (ItemInfo r : occupiedPos) {
                occupied.markCells(r, true);
            }
        }
        return occupied.findVacantCell(xy, spanX, spanY);
    }
}
+162 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.ContentWriter
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 java.util.UUID

/**
 * Base class for workspace related tests.
 */
abstract class AbstractWorkspaceModelTest {
    companion object {
        val emptyScreenSpaces = listOf(Rect(0, 0, 5, 5))
        val fullScreenSpaces = emptyList<Rect>()
        val nonEmptyScreenSpaces = listOf(Rect(1, 2, 3, 4))
    }

    protected lateinit var mTargetContext: Context
    protected lateinit var mIdp: InvariantDeviceProfile
    protected lateinit var mAppState: LauncherAppState
    protected lateinit var mModelHelper: LauncherModelHelper
    protected lateinit var mExistingScreens: IntArray
    protected lateinit var mNewScreens: IntArray
    protected lateinit var mScreenOccupancy: IntSparseArrayMap<GridOccupancy>

    open fun setup() {
        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()
    }

    open fun tearDown() {
        mModelHelper.destroy()
    }


    /**
     * Sets up workspaces with the given screen IDs with some items and a 2x2 space.
     */
    fun setupWorkspaces(screenIdsWithItems: List<Int>) {
        var nextItemId = 1
        screenIdsWithItems.forEach { screenId ->
            nextItemId = setupWorkspace(nextItemId, screenId, nonEmptyScreenSpaces)
        }
    }

    /**
     * Sets up the given workspaces with the given spaces, and fills the remaining space with items.
     */
    fun setupWorkspacesWithSpaces(
        screen0: List<Rect>? = null,
        screen1: List<Rect>? = null,
        screen2: List<Rect>? = null,
        screen3: List<Rect>? = null,
    ) = listOf(screen0, screen1, screen2, screen3)
        .let(this::setupWithSpaces)

    private fun setupWithSpaces(workspaceSpaces: List<List<Rect>?>) {
        var nextItemId = 1
        workspaceSpaces.forEachIndexed { screenId, spaces ->
            if (spaces != null) {
                nextItemId = setupWorkspace(nextItemId, screenId, spaces)
            }
        }
    }

    private fun setupWorkspace(startId: Int, screenId: Int, spaces: List<Rect>): Int {
        return mModelHelper.executeSimpleTask { dataModel ->
            writeWorkspaceWithSpaces(dataModel, startId, screenId, spaces)
        }
    }

    private fun writeWorkspaceWithSpaces(
        bgDataModel: BgDataModel,
        itemStartId: Int,
        screenId: Int,
        spaces: List<Rect>,
    ): Int {
        var itemId = itemStartId
        val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows)
        occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true)
        spaces.forEach { spaceRect ->
            occupancy.markCells(spaceRect, 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
    }

    fun getExistingItem() = WorkspaceItemInfo()
        .apply { intent = Intent().setComponent(ComponentName("a", "b")) }

    fun getNewItem(): WorkspaceItemInfo {
        val itemPackage = UUID.randomUUID().toString()
        return WorkspaceItemInfo()
            .apply { intent = Intent().setComponent(ComponentName(itemPackage, itemPackage)) }
    }
}

data class NewItemSpace(
    val screenId: Int,
    val cellX: Int,
    val cellY: Int
) {
    fun toIntArray() = intArrayOf(screenId, cellX, cellY)

    companion object {
        fun fromIntArray(array: kotlin.IntArray) = NewItemSpace(array[0], array[1], array[2])
    }
}
 No newline at end of file
+0 −201
Original line number Diff line number Diff line
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 {
        // 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<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;
    }
}
+245 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading