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

Commit 7117c555 authored by Stefan Andonian's avatar Stefan Andonian Committed by Android (Google) Code Review
Browse files

Merge "Robustly fixed issue where migration from different versions of DB data...

Merge "Robustly fixed issue where migration from different versions of DB data tables was causing crashes upon grid size switching." into main
parents 3222387d e8b86db3
Loading
Loading
Loading
Loading
+50 −22
Original line number Diff line number Diff line
@@ -19,8 +19,13 @@ package com.android.launcher3;
import android.database.sqlite.SQLiteDatabase;
import android.provider.BaseColumns;

import androidx.annotation.NonNull;

import com.android.launcher3.model.data.ItemInfo;

import java.util.LinkedHashMap;
import java.util.stream.Collectors;

/**
 * Settings related utilities.
 */
@@ -289,28 +294,51 @@ public class LauncherSettings {

        public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
                String tableName) {
            String ifNotExists = optional ? " IF NOT EXISTS " : "";
            db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
                    "_id INTEGER PRIMARY KEY," +
                    "title TEXT," +
                    "intent TEXT," +
                    "container INTEGER," +
                    "screen INTEGER," +
                    "cellX INTEGER," +
                    "cellY INTEGER," +
                    "spanX INTEGER," +
                    "spanY INTEGER," +
                    "itemType INTEGER," +
                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
                    "icon BLOB," +
                    "appWidgetProvider TEXT," +
                    "modified INTEGER NOT NULL DEFAULT 0," +
                    "restored INTEGER NOT NULL DEFAULT 0," +
                    "profileId INTEGER DEFAULT " + myProfileId + "," +
                    "rank INTEGER NOT NULL DEFAULT 0," +
                    "options INTEGER NOT NULL DEFAULT 0," +
                    APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
                    ");");
            db.execSQL("CREATE TABLE " + (optional ? " IF NOT EXISTS " : "") + tableName + " ("
                    + getJoinedColumnsToTypes(myProfileId) + ");");
        }

        // LinkedHashMap maintains Order of Insertion
        @NonNull
        private static LinkedHashMap<String, String> getColumnsToTypes(long profileId) {
            final LinkedHashMap<String, String> columnsToTypes = new LinkedHashMap<>();
            columnsToTypes.put(_ID, "INTEGER PRIMARY KEY");
            columnsToTypes.put(TITLE, "TEXT");
            columnsToTypes.put(INTENT, "TEXT");
            columnsToTypes.put(CONTAINER, "INTEGER");
            columnsToTypes.put(SCREEN, "INTEGER");
            columnsToTypes.put(CELLX, "INTEGER");
            columnsToTypes.put(CELLY, "INTEGER");
            columnsToTypes.put(SPANX, "INTEGER");
            columnsToTypes.put(SPANY, "INTEGER");
            columnsToTypes.put(ITEM_TYPE, "INTEGER");
            columnsToTypes.put(APPWIDGET_ID, "INTEGER NOT NULL DEFAULT -1");
            columnsToTypes.put(ICON, "BLOB");
            columnsToTypes.put(APPWIDGET_PROVIDER, "TEXT");
            columnsToTypes.put(MODIFIED, "INTEGER NOT NULL DEFAULT 0");
            columnsToTypes.put(RESTORED, "INTEGER NOT NULL DEFAULT 0");
            columnsToTypes.put(PROFILE_ID, "INTEGER DEFAULT " + profileId);
            columnsToTypes.put(RANK, "INTEGER NOT NULL DEFAULT 0");
            columnsToTypes.put(OPTIONS, "INTEGER NOT NULL DEFAULT 0");
            columnsToTypes.put(APPWIDGET_SOURCE, "INTEGER NOT NULL DEFAULT -1");
            return columnsToTypes;
        }

        private static String getJoinedColumnsToTypes(long profileId) {
            return getColumnsToTypes(profileId)
                    .entrySet()
                    .stream()
                    .map(it -> it.getKey() + " " + it.getValue())
                    .collect(Collectors.joining(", "));
        }

        /**
         * Returns an ordered list of columns in the Favorites table as one string, ready to use in
         * an SQL statement.
         */
        @NonNull
        public static String getColumns(long profileId) {
            return String.join(", ", getColumnsToTypes(profileId).keySet());
        }
    }

+5 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.launcher3.provider;

import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;

import android.content.ContentValues;
@@ -48,7 +49,6 @@ import com.android.launcher3.util.IntSet;
 * A set of utility methods for Launcher DB used for DB updates and migration.
 */
public class LauncherDbUtils {

    /**
     * Returns a string which can be used as a where clause for DB query to match the given itemId
     */
@@ -90,10 +90,12 @@ public class LauncherDbUtils {
        if (fromDb != toDb) {
            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
            toDb.execSQL(
                    "INSERT INTO " + toTable + " SELECT * FROM from_db." + fromTable);
                    "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
                        + " FROM from_db." + fromTable);
            toDb.execSQL("DETACH DATABASE 'from_db'");
        } else {
            toDb.execSQL("INSERT INTO " + toTable + " SELECT * FROM " + fromTable);
            toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
                    + fromTable);
        }
    }

+5 −0
Original line number Diff line number Diff line
DROP TABLE IF EXISTS 'favorites';
CREATE TABLE favorites (_id INTEGER PRIMARY KEY,title TEXT,intent TEXT,container INTEGER,screen INTEGER,cellX INTEGER,cellY INTEGER,spanX INTEGER,spanY INTEGER,itemType INTEGER,appWidgetId INTEGER NOT NULL DEFAULT -1,iconPackage TEXT,iconResource TEXT,icon BLOB,appWidgetProvider TEXT,modified INTEGER NOT NULL DEFAULT 0,restored INTEGER NOT NULL DEFAULT 0,profileId INTEGER DEFAULT 0,rank INTEGER NOT NULL DEFAULT 0,options INTEGER NOT NULL DEFAULT 0,appWidgetSource INTEGER NOT NULL DEFAULT -1);
INSERT INTO 'favorites' VALUES(1,'Phone','#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.dialer/.extensions.GoogleDialtactsActivity;end',-101,0,0,0,1,1,0,-1,"iconPackage1","iconResource1",NULL,NULL,0,0,0,0,0,-1);
INSERT INTO 'favorites' VALUES(2,'Messages','#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.apps.messaging/.ui.ConversationListActivity;end',-101,1,1,0,1,1,0,-1,"iconPackage2","iconResource2",NULL,NULL,0,0,0,0,0,-1);
INSERT INTO 'favorites' VALUES(3,'Play Store','#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.android.vending/.AssetBrowserActivity;end',-101,2,2,0,1,1,0,-1,"iconPackage3","iconResource3",NULL,NULL,0,0,0,0,0,-1);
 No newline at end of file
+79 −0
Original line number Diff line number Diff line
package com.android.launcher3.model

import android.database.sqlite.SQLiteDatabase
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME
import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE
import com.android.launcher3.LauncherSettings.Favorites.addTableToDb
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
import java.util.function.ToLongFunction
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
import org.junit.runner.RunWith

private const val INSERTION_SQL = "databases/v30_workspace_items.sql"

private const val ICON_PACKAGE = "iconPackage"
private const val ICON_RESOURCE = "iconResource"

@SmallTest
@RunWith(AndroidJUnit4::class)
class DatabaseHelperTest {

    /**
     * b/304687723 occurred when a return was accidentally added to a case statement in
     * DatabaseHelper.onUpgrade, which stopped the final data migration from successfully occurring.
     * This test loads an in-memory db from a text file containing SQL statements, and then performs
     * the migration on the db, and verifies that the correct columns have been deleted.
     */
    @Test
    fun onUpgrade_to_version_32_from_30() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val userSerialProvider =
            ToLongFunction<UserHandle> {
                UserCache.INSTANCE.get(context).getSerialNumberForUser(it)
            }
        val dbHelper = DatabaseHelper(context, null, userSerialProvider) {}
        val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb

        dbHelper.onUpgrade(db, 30, 32)

        assertFalse(hasFavoritesColumn(db, ICON_PACKAGE))
        assertFalse(hasFavoritesColumn(db, ICON_RESOURCE))
    }

    /**
     * b/304687723 causes a crash due to copying a table with 21 columns to a table with 19 columns.
     * This test loads an in-memory db from a text file containing SQL statements, and then copies
     * data from the created table into a temporary one, and verifies that no exception is thrown.
     */
    @Test
    fun after_migrating_from_db_v30_to_v32_copy_table() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb // v30 - 21 columns

        addTableToDb(db, 1, true, TMP_TABLE)
        LauncherDbUtils.copyTable(db, TABLE_NAME, db, TMP_TABLE, context)

        val c1 = db.query(TABLE_NAME, null, null, null, null, null, null)
        val c2 = db.query(TMP_TABLE, null, null, null, null, null, null)

        assertEquals(21, c1.columnCount)
        assertEquals(19, c2.columnCount)
        assertEquals(c1.count, c2.count)

        c1.close()
        c2.close()
    }

    private fun hasFavoritesColumn(db: SQLiteDatabase, columnName: String): Boolean {
        db.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
            return c.getColumnIndex(columnName) >= 0
        }
    }
}
+3 −7
Original line number Diff line number Diff line
@@ -29,18 +29,14 @@ private val All_COLUMNS =
        "options",
        "appWidgetSource"
    )
private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"

class FactitiousDbController(context: Context) : ModelDbController(context) {
class FactitiousDbController(context: Context, insertFile: String) : ModelDbController(context) {

    private val inMemoryDb: SQLiteDatabase by lazy {
    val inMemoryDb: SQLiteDatabase by lazy {
        SQLiteDatabase.createInMemory(SQLiteDatabase.OpenParams.Builder().build()).also { db ->
            BufferedReader(
                    InputStreamReader(
                        InstrumentationRegistry.getInstrumentation()
                            .context
                            .assets
                            .open(INSERTION_STATEMENT_FILE)
                        InstrumentationRegistry.getInstrumentation().context.assets.open(insertFile)
                    )
                )
                .lines()
Loading