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

Commit cc90d1b7 authored by Pinyao Ting's avatar Pinyao Ting
Browse files

Make grid size migration less confusing for users.

Grid size migration is a confusing experience for users because the
content of the grid is not predicatable to the user. Part of the reason
was the dedupe logic which merges items from new grid into old grid,
this mean if user removes an item, change to another grid and go back,
user will see that same item re-appears.

This CL keeps the content of target grid the same as source gird, i.e
user will will get the exact same set of app icons/shortcuts/folders and
widgets (if size permits) across grid sizes. The only difference being
their placement in the grid.

Bug: 256859723
Test: atest GridSizeMigrationUtilTest
Change-Id: I1732c91e441ad44bc43e0a943566a83563d12b07
parent 691c6e51
Loading
Loading
Loading
Loading
+63 −41
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ import java.util.stream.Collectors;
public class GridSizeMigrationUtil {

    private static final String TAG = "GridSizeMigrationUtil";
    private static final boolean DEBUG = false;
    private static final boolean DEBUG = true;

    private GridSizeMigrationUtil() {
        // Util class should not be instantiated
@@ -188,27 +188,54 @@ public class GridSizeMigrationUtil {
            @NonNull final DeviceGridState srcDeviceState,
            @NonNull final DeviceGridState destDeviceState) {

        final List<DbEntry> hotseatItems = destReader.loadHotseatEntries();
        final List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
        final List<DbEntry> hotseatDiff =
                calcDiff(srcReader.loadHotseatEntries(), hotseatItems);
        final List<DbEntry> workspaceDiff =
                calcDiff(srcReader.loadAllWorkspaceEntries(), workspaceItems);
        final List<DbEntry> srcHotseatItems = srcReader.loadHotseatEntries();
        final List<DbEntry> srcWorkspaceItems = srcReader.loadAllWorkspaceEntries();
        final List<DbEntry> dstHotseatItems = destReader.loadHotseatEntries();
        final List<DbEntry> dstWorkspaceItems = destReader.loadAllWorkspaceEntries();
        final List<DbEntry> hotseatToBeAdded = new ArrayList<>(1);
        final List<DbEntry> workspaceToBeAdded = new ArrayList<>(1);
        final IntArray toBeRemoved = new IntArray();

        calcDiff(srcHotseatItems, dstHotseatItems, hotseatToBeAdded, toBeRemoved);
        calcDiff(srcWorkspaceItems, dstWorkspaceItems, workspaceToBeAdded, toBeRemoved);

        final int trgX = targetSize.x;
        final int trgY = targetSize.y;

        if (hotseatDiff.isEmpty() && workspaceDiff.isEmpty()) {
        if (DEBUG) {
            Log.d(TAG, "Start migration:"
                    + "\n Source Device:"
                    + srcWorkspaceItems.stream().map(DbEntry::toString).collect(
                    Collectors.joining(",\n", "[", "]"))
                    + "\n Target Device:"
                    + dstWorkspaceItems.stream().map(DbEntry::toString).collect(
                    Collectors.joining(",\n", "[", "]"))
                    + "\n Removing Items:"
                    + dstWorkspaceItems.stream().filter(entry ->
                            toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
                    Collectors.joining(",\n", "[", "]"))
                    + "\n Adding Workspace Items:"
                    + workspaceToBeAdded.stream().map(DbEntry::toString).collect(
                    Collectors.joining(",\n", "[", "]"))
                    + "\n Adding Hotseat Items:"
                    + hotseatToBeAdded.stream().map(DbEntry::toString).collect(
                    Collectors.joining(",\n", "[", "]"))
            );
        }
        if (!toBeRemoved.isEmpty()) {
            removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
        }
        if (hotseatToBeAdded.isEmpty() && workspaceToBeAdded.isEmpty()) {
            return false;
        }

        // Sort the items by the reading order.
        Collections.sort(hotseatDiff);
        Collections.sort(workspaceDiff);
        Collections.sort(hotseatToBeAdded);
        Collections.sort(workspaceToBeAdded);

        // Migrate hotseat
        solveHotseatPlacement(db, srcReader,
                destReader, context, destHotseatSize, hotseatItems, hotseatDiff);
                destReader, context, destHotseatSize, dstHotseatItems, hotseatToBeAdded);

        // Migrate workspace.
        // First we create a collection of the screens
@@ -229,8 +256,8 @@ public class GridSizeMigrationUtil {
                Log.d(TAG, "Migrating " + screenId);
            }
            solveGridPlacement(db, srcReader,
                    destReader, context, screenId, trgX, trgY, workspaceDiff, false);
            if (workspaceDiff.isEmpty()) {
                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, false);
            if (workspaceToBeAdded.isEmpty()) {
                break;
            }
        }
@@ -238,42 +265,37 @@ public class GridSizeMigrationUtil {
        // In case the new grid is smaller, there might be some leftover items that don't fit on
        // any of the screens, in this case we add them to new screens until all of them are placed.
        int screenId = destReader.mLastScreenId + 1;
        while (!workspaceDiff.isEmpty()) {
        while (!workspaceToBeAdded.isEmpty()) {
            solveGridPlacement(db, srcReader,
                    destReader, context, screenId, trgX, trgY, workspaceDiff, preservePages);
                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
            screenId++;
        }

        return true;
    }

    /** Return what's in the src but not in the dest */
    private static List<DbEntry> calcDiff(List<DbEntry> src, List<DbEntry> dest) {
        Map<String, Integer> destIdSet = new HashMap<>();
        for (DbEntry entry : dest) {
            String entryID = entry.getEntryMigrationId();
            if (destIdSet.containsKey(entryID)) {
                destIdSet.put(entryID, destIdSet.get(entryID) + 1);
            } else {
                destIdSet.put(entryID, 1);
            }
        }
        List<DbEntry> diff = new ArrayList<>();
        for (DbEntry entry : src) {
            String entryID = entry.getEntryMigrationId();
            if (destIdSet.containsKey(entryID)) {
                Integer count = destIdSet.get(entryID);
                if (count <= 0) {
                    diff.add(entry);
                    destIdSet.remove(entryID);
                } else {
                    destIdSet.put(entryID, count - 1);
                }
            } else {
                diff.add(entry);
    /**
     * Calculate the differences between {@code src} (denoted by A) and {@code dest}
     * (denoted by B).
     * All DbEntry in A - B will be added to {@code toBeAdded}
     * All DbEntry.id in B - A will be added to {@code toBeRemoved}
     */
    private static void calcDiff(@NonNull final List<DbEntry> src,
            @NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded,
            @NonNull final IntArray toBeRemoved) {
        src.forEach(entry -> {
            if (!dest.contains(entry)) {
                toBeAdded.add(entry);
            }
        });
        dest.forEach(entry -> {
            if (!src.contains(entry)) {
                toBeRemoved.add(entry.id);
                if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
                    entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
                }
            }
        return diff;
        });
    }

    private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry,
@@ -682,12 +704,12 @@ public class GridSizeMigrationUtil {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            DbEntry entry = (DbEntry) o;
            return Objects.equals(mIntent, entry.mIntent);
            return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
        }

        @Override
        public int hashCode() {
            return Objects.hash(mIntent);
            return Objects.hash(getEntryMigrationId());
        }

        public void updateContentValues(ContentValues values) {
+221 −3
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.launcher3.model

import android.content.Context
import android.content.Intent
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase
import android.graphics.Point
import android.os.Process
@@ -183,15 +184,232 @@ class GridSizeMigrationUtilTest {
        // Expected dest grid icons
        // _ _ _ _
        // 5 6 7 8
        // 9 _ 10_
        // 9 _ _ _
        // _ _ _ _
        assertThat(locMap.size.toLong()).isEqualTo(6)
        assertThat(locMap.size.toLong()).isEqualTo(5)
        assertThat(locMap[testPackage5]).isEqualTo(Point(0, 1))
        assertThat(locMap[testPackage6]).isEqualTo(Point(1, 1))
        assertThat(locMap[testPackage7]).isEqualTo(Point(2, 1))
        assertThat(locMap[testPackage8]).isEqualTo(Point(3, 1))
        assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
        assertThat(locMap[testPackage10]).isEqualTo(Point(2, 2))
    }

    /**
     * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is
     * not needed anymore
     */
    @Test
    @Throws(Exception::class)
    fun testMigrationBackAndForth() {
        // Hotseat items in grid A
        // 1 2 _ 3 4
        modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI)
        modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI)
        modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI)
        modelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
        // Workspace items in grid A
        // _ _ _ _ _
        // _ _ _ _ 5
        // _ _ 6 _ 7
        // _ _ 8 _ _
        // _ _ _ _ _
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage5, 5, TMP_CONTENT_URI)
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage6, 6, TMP_CONTENT_URI)
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage7, 7, TMP_CONTENT_URI)
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage8, 8, TMP_CONTENT_URI)

        // Hotseat items in grid B
        // 2 _ _ _
        modelHelper.addItem(SHORTCUT, 0, HOTSEAT, 0, 0, testPackage2)
        // Workspace items in grid B
        // _ _ _ _
        // _ _ _ 10
        // _ _ _ _
        // _ _ _ _
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 3, testPackage10)

        idp.numDatabaseHotseatIcons = 4
        idp.numColumns = 4
        idp.numRows = 4
        val readerGridA = DbReader(db, TMP_TABLE, context, validPackages)
        val readerGridB = DbReader(db, TABLE_NAME, context, validPackages)
        // migrate from A -> B
        GridSizeMigrationUtil.migrate(
                context,
                db,
                readerGridA,
                readerGridB,
                idp.numDatabaseHotseatIcons,
                Point(idp.numColumns, idp.numRows),
                DeviceGridState(context),
                DeviceGridState(idp)
        )

        // Check hotseat items in grid B
        var c = context.contentResolver.query(
                CONTENT_URI,
                arrayOf(SCREEN, INTENT),
                "container=$CONTAINER_HOTSEAT",
                null,
                SCREEN,
                null
        ) ?: throw IllegalStateException()
        // Expected hotseat items in grid B
        // 2 1 3 4
        verifyHotseat(c, idp,
                mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList())

        // Check workspace items in grid B
        c = context.contentResolver.query(
                CONTENT_URI,
                arrayOf(SCREEN, CELLX, CELLY, INTENT),
                "container=$CONTAINER_DESKTOP",
                null,
                null,
                null
        ) ?: throw IllegalStateException()
        var locMap = parseLocMap(context, c)
        // Expected items in grid B
        // _ _ _ _
        // 5 6 7 8
        // _ _ _ _
        // _ _ _ _
        assertThat(locMap.size.toLong()).isEqualTo(4)
        assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 0, 1))
        assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 1, 1))
        assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 2, 1))
        assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))

        // add item in B
        modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 2, testPackage9)

        // migrate from B -> A
        GridSizeMigrationUtil.migrate(
                context,
                db,
                readerGridB,
                readerGridA,
                5,
                Point(5, 5),
                DeviceGridState(idp),
                DeviceGridState(context)
        )
        // Check hotseat items in grid A
        c = context.contentResolver.query(
                TMP_CONTENT_URI,
                arrayOf(SCREEN, INTENT),
                "container=$CONTAINER_HOTSEAT",
                null,
                SCREEN,
                null
        ) ?: throw IllegalStateException()
        // Expected hotseat items in grid A
        // 1 2 _ 3 4
        verifyHotseat(c, idp, mutableListOf(
                testPackage1, testPackage2, null, testPackage3, testPackage4).toList())

        // Check workspace items in grid A
        c = context.contentResolver.query(
                TMP_CONTENT_URI,
                arrayOf(SCREEN, CELLX, CELLY, INTENT),
                "container=$CONTAINER_DESKTOP",
                null,
                null,
                null
        ) ?: throw IllegalStateException()
        locMap = parseLocMap(context, c)
        // Expected workspace items in grid A
        // _ _ _ _ _
        // _ _ _ _ 5
        // 9 _ 6 _ 7
        // _ _ 8 _ _
        // _ _ _ _ _
        assertThat(locMap.size.toLong()).isEqualTo(5)
        // Verify items that existed in grid A remains in same position
        assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 4, 1))
        assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 2, 2))
        assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 4, 2))
        assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 2, 3))
        // Verify items that didn't exist in grid A are added in new screen
        assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))

        // remove item from B
        modelHelper.deleteItem(7, TMP_TABLE)

        // migrate from A -> B
        GridSizeMigrationUtil.migrate(
                context,
                db,
                readerGridA,
                readerGridB,
                idp.numDatabaseHotseatIcons,
                Point(idp.numColumns, idp.numRows),
                DeviceGridState(context),
                DeviceGridState(idp)
        )

        // Check hotseat items in grid B
        c = context.contentResolver.query(
                CONTENT_URI,
                arrayOf(SCREEN, INTENT),
                "container=$CONTAINER_HOTSEAT",
                null,
                SCREEN,
                null
        ) ?: throw IllegalStateException()
        // Expected hotseat items in grid B
        // 2 1 3 4
        verifyHotseat(c, idp,
                mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList())

        // Check workspace items in grid B
        c = context.contentResolver.query(
                CONTENT_URI,
                arrayOf(SCREEN, CELLX, CELLY, INTENT),
                "container=$CONTAINER_DESKTOP",
                null,
                null,
                null
        ) ?: throw IllegalStateException()
        locMap = parseLocMap(context, c)
        // Expected workspace items in grid B
        // _ _ _ _
        // 5 6 _ 8
        // 9 _ _ _
        // _ _ _ _
        assertThat(locMap.size.toLong()).isEqualTo(4)
        assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 0, 1))
        assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 1, 1))
        assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))
        assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
    }

    private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List<String?>) {
        assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
        val screenIndex = c.getColumnIndex(SCREEN)
        val intentIndex = c.getColumnIndex(INTENT)
        expected.forEachIndexed { idx, pkg ->
            if (pkg == null) return@forEachIndexed
            c.moveToNext()
            assertThat(c.getInt(screenIndex).toLong()).isEqualTo(idx)
            assertThat(c.getString(intentIndex)).contains(pkg)
        }
        c.close()
    }

    private fun parseLocMap(context: Context, c: Cursor): Map<String, Triple<Int, Int, Int>> {
        // Check workspace items
        val intentIndex = c.getColumnIndex(INTENT)
        val screenIndex = c.getColumnIndex(SCREEN)
        val cellXIndex = c.getColumnIndex(CELLX)
        val cellYIndex = c.getColumnIndex(CELLY)
        val locMap = mutableMapOf<String, Triple<Int, Int, Int>>()
        while (c.moveToNext()) {
            locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] =
                    Triple(c.getInt(screenIndex), c.getInt(cellXIndex), c.getInt(cellYIndex))
        }
        c.close()
        return locMap.toMap()
    }

    @Test
+6 −0
Original line number Diff line number Diff line
@@ -362,6 +362,12 @@ public class LauncherModelHelper {
        sandboxContext.getContentResolver().insert(contentUri, values);
    }

    public void deleteItem(int itemId, @NonNull final String tableName) {
        final Uri uri = Uri.parse("content://"
                + LauncherProvider.AUTHORITY + "/" + tableName + "/" + itemId);
        sandboxContext.getContentResolver().delete(uri, null, null);
    }

    public int[][][] createGrid(int[][][] typeArray) {
        return createGrid(typeArray, 1);
    }