Moving DatabaseHelper to it's own class outside of Launcher provider

Bug: 277345535
Test: Presubmit
Flag: N/A
Change-Id: Ib8c94ceb954172dc27e357be2face06d50d399dd
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index d1fa764..f4892b2 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -18,7 +18,6 @@
 
 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
-import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 
 import android.annotation.TargetApi;
@@ -30,83 +29,55 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.pm.ProviderInfo;
 import android.database.Cursor;
-import android.database.DatabaseUtils;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
-import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
-import android.os.UserHandle;
 import android.os.UserManager;
-import android.provider.BaseColumns;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Xml;
 
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.DbDowngradeHelper;
-import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.NoLocaleSQLiteHelper;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Partner;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
 import org.xmlpull.v1.XmlPullParser;
 
-import java.io.File;
 import java.io.FileDescriptor;
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.io.StringReader;
-import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Locale;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
-    private static final boolean LOGD = false;
-
-    private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
-
-    /**
-     * Represents the schema of the database. Changes in scheme need not be backwards compatible.
-     * When increasing the scheme version, ensure that downgrade_schema.json is updated
-     */
-    public static final int SCHEMA_VERSION = 31;
 
     public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings";
-    public static final String KEY_LAYOUT_PROVIDER_AUTHORITY = "KEY_LAYOUT_PROVIDER_AUTHORITY";
 
     private static final int TEST_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test_workspace;
     private static final int TEST2_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test2_workspace;
     private static final int TAPL_WORKSPACE_LAYOUT_RES_XML = R.xml.default_tapl_test_workspace;
 
-    static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
+    public static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
     protected DatabaseHelper mOpenHelper;
     protected String mProviderAuthority;
@@ -193,18 +164,6 @@
         return result;
     }
 
-    @Thunk static int dbInsertAndCheck(DatabaseHelper helper,
-            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
-        if (values == null) {
-            throw new RuntimeException("Error: attempting to insert null values");
-        }
-        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
-            throw new RuntimeException("Error: attempting to add item without specifying an id");
-        }
-        helper.checkId(values);
-        return (int) db.insert(table, nullColumnHack, values);
-    }
-
     private void reloadLauncherIfExternal() {
         if (Binder.getCallingPid() != Process.myPid()) {
             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
@@ -228,7 +187,7 @@
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         addModifiedTime(initialValues);
-        final int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
+        final int rowId = mOpenHelper.dbInsertAndCheck(db, args.table, initialValues);
         if (rowId < 0) return null;
         onAddOrDeleteOp(db);
 
@@ -287,7 +246,7 @@
             int numValues = values.length;
             for (int i = 0; i < numValues; i++) {
                 addModifiedTime(values[i]);
-                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
+                if (mOpenHelper.dbInsertAndCheck(db, args.table, values[i]) < 0) {
                     return 0;
                 }
             }
@@ -616,499 +575,6 @@
                 mOpenHelper, getContext().getResources(), defaultLayout);
     }
 
-    /**
-     * The class is subclassed in tests to create an in-memory db.
-     */
-    public static class DatabaseHelper extends NoLocaleSQLiteHelper implements
-            LayoutParserCallback {
-        private final Context mContext;
-        private final boolean mForMigration;
-        private int mMaxItemId = -1;
-        private boolean mHotseatRestoreTableExists;
-
-        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
-            return createDatabaseHelper(context, null, forMigration);
-        }
-
-        static DatabaseHelper createDatabaseHelper(Context context, String dbName,
-                boolean forMigration) {
-            if (dbName == null) {
-                dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
-            }
-            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
-            // Table creation sometimes fails silently, which leads to a crash loop.
-            // This way, we will try to create a table every time after crash, so the device
-            // would eventually be able to recover.
-            if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
-                Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
-                // This operation is a no-op if the table already exists.
-                databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
-            }
-            databaseHelper.mHotseatRestoreTableExists = tableExists(
-                    databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
-
-            databaseHelper.initIds();
-            return databaseHelper;
-        }
-
-        /**
-         * Constructor used in tests and for restore.
-         */
-        public DatabaseHelper(Context context, String dbName, boolean forMigration) {
-            super(context, dbName, SCHEMA_VERSION);
-            mContext = context;
-            mForMigration = forMigration;
-        }
-
-        protected void initIds() {
-            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
-            // the DB here
-            if (mMaxItemId == -1) {
-                mMaxItemId = initializeMaxItemId(getWritableDatabase());
-            }
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            if (LOGD) Log.d(TAG, "creating new launcher database");
-
-            mMaxItemId = 1;
-
-            addFavoritesTable(db, false);
-
-            // Fresh and clean launcher DB.
-            mMaxItemId = initializeMaxItemId(db);
-            if (!mForMigration) {
-                onEmptyDbCreated();
-            }
-        }
-
-        protected void onAddOrDeleteOp(SQLiteDatabase db) {
-            if (mHotseatRestoreTableExists) {
-                dropTable(db, Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
-                mHotseatRestoreTableExists = false;
-            }
-        }
-
-        /**
-         * Re-composite given key in respect to database. If the current db is
-         * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
-         * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
-         * string will be "EMPTY_DATABASE_CREATED@minimal.db".
-         */
-        String getKey(final String key) {
-            if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
-                return key;
-            }
-            return key + "@" + getDatabaseName();
-        }
-
-        /**
-         * Overriden in tests.
-         */
-        protected void onEmptyDbCreated() {
-            // Set the flag for empty DB
-            LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
-                    .commit();
-        }
-
-        public long getSerialNumberForUser(UserHandle user) {
-            return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
-        }
-
-        public long getDefaultUserSerial() {
-            return getSerialNumberForUser(Process.myUserHandle());
-        }
-
-        private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
-            Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
-        }
-
-        @Override
-        public void onOpen(SQLiteDatabase db) {
-            super.onOpen(db);
-
-            File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE);
-            if (!schemaFile.exists()) {
-                handleOneTimeDataUpgrade(db);
-            }
-            DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext);
-        }
-
-        /**
-         * One-time data updated before support of onDowngrade was added. This update is backwards
-         * compatible and can safely be run multiple times.
-         * Note: No new logic should be added here after release, as the new logic might not get
-         * executed on an existing device.
-         * TODO: Move this to db upgrade path, once the downgrade path is released.
-         */
-        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
-            // Remove "profile extra"
-            UserCache um = UserCache.INSTANCE.get(mContext);
-            for (UserHandle user : um.getUserProfiles()) {
-                long serial = um.getSerialNumberForUser(user);
-                String sql = "update favorites set intent = replace(intent, "
-                        + "';l.profile=" + serial + ";', ';') where itemType = 0;";
-                db.execSQL(sql);
-            }
-        }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
-            switch (oldVersion) {
-                // The version cannot be lower that 12, as Launcher3 never supported a lower
-                // version of the DB.
-                case 12:
-                    // No-op
-                case 13: {
-                    try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-                        // Insert new column for holding widget provider name
-                        db.execSQL("ALTER TABLE favorites " +
-                                "ADD COLUMN appWidgetProvider TEXT;");
-                        t.commit();
-                    } catch (SQLException ex) {
-                        Log.e(TAG, ex.getMessage(), ex);
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 14: {
-                    if (!addIntegerColumn(db, Favorites.MODIFIED, 0)) {
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 15: {
-                    if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 16:
-                    // No-op
-                case 17:
-                    // No-op
-                case 18:
-                    // No-op
-                case 19: {
-                    // Add userId column
-                    if (!addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial())) {
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 20:
-                    if (!updateFolderItemsRank(db, true)) {
-                        break;
-                    }
-                case 21:
-                    // No-op
-                case 22: {
-                    if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 23:
-                    // No-op
-                case 24:
-                    // No-op
-                case 25:
-                    convertShortcutsToLauncherActivities(db);
-                case 26:
-                    // QSB was moved to the grid. Ignore overlapping items
-                case 27: {
-                    // Update the favorites table so that the screen ids are ordered based on
-                    // workspace page rank.
-                    IntArray finalScreens = LauncherDbUtils.queryIntArray(false, db,
-                            "workspaceScreens", BaseColumns._ID, null, null, "screenRank");
-                    int[] original = finalScreens.toArray();
-                    Arrays.sort(original);
-                    String updatemap = "";
-                    for (int i = 0; i < original.length; i++) {
-                        if (finalScreens.get(i) != original[i]) {
-                            updatemap += String.format(Locale.ENGLISH, " WHEN %1$s=%2$d THEN %3$d",
-                                    Favorites.SCREEN, finalScreens.get(i), original[i]);
-                        }
-                    }
-                    if (!TextUtils.isEmpty(updatemap)) {
-                        String query = String.format(Locale.ENGLISH,
-                                "UPDATE %1$s SET %2$s=CASE %3$s ELSE %2$s END WHERE %4$s = %5$d",
-                                Favorites.TABLE_NAME, Favorites.SCREEN, updatemap,
-                                Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP);
-                        db.execSQL(query);
-                    }
-                    dropTable(db, "workspaceScreens");
-                }
-                case 28: {
-                    boolean columnAdded = addIntegerColumn(
-                            db, Favorites.APPWIDGET_SOURCE, Favorites.CONTAINER_UNKNOWN);
-                    if (!columnAdded) {
-                        // Old version remains, which means we wipe old data
-                        break;
-                    }
-                }
-                case 29: {
-                    // Remove widget panel related leftover workspace items
-                    db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
-                            Favorites.SCREEN, IntArray.wrap(-777, -778)), null);
-                }
-                case 30: {
-                    if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
-                        // Clean up first row in screen 0 as it might contain junk data.
-                        Log.d(TAG, "Cleaning up first row");
-                        db.delete(Favorites.TABLE_NAME,
-                                String.format(Locale.ENGLISH,
-                                        "%1$s = %2$d AND %3$s = %4$d AND %5$s = %6$d",
-                                        Favorites.SCREEN, 0,
-                                        Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP,
-                                        Favorites.CELLY, 0), null);
-                    }
-                    return;
-                }
-                case 31: {
-                    // DB Upgraded successfully
-                    return;
-                }
-            }
-
-            // DB was not upgraded
-            Log.w(TAG, "Destroying all old data.");
-            createEmptyDB(db);
-        }
-
-        @Override
-        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            try {
-                DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE))
-                        .onDowngrade(db, oldVersion, newVersion);
-            } catch (Exception e) {
-                Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion +
-                        ". Wiping databse.", e);
-                createEmptyDB(db);
-            }
-        }
-
-        /**
-         * Clears all the data for a fresh start.
-         */
-        public void createEmptyDB(SQLiteDatabase db) {
-            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-                dropTable(db, Favorites.TABLE_NAME);
-                dropTable(db, "workspaceScreens");
-                onCreate(db);
-                t.commit();
-            }
-        }
-
-        /**
-         * Removes widgets which are registered to the Launcher's host, but are not present
-         * in our model.
-         */
-        public void removeGhostWidgets(SQLiteDatabase db) {
-            // Get all existing widget ids.
-            final LauncherWidgetHolder holder = newLauncherWidgetHolder();
-            try {
-                final int[] allWidgets;
-                try {
-                    // Although the method was defined in O, it has existed since the beginning of
-                    // time, so it might work on older platforms as well.
-                    allWidgets = holder.getAppWidgetIds();
-                } catch (IncompatibleClassChangeError e) {
-                    Log.e(TAG, "getAppWidgetIds not supported", e);
-                    return;
-                }
-                final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db,
-                        Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
-                        "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
-                boolean isAnyWidgetRemoved = false;
-                for (int widgetId : allWidgets) {
-                    if (!validWidgets.contains(widgetId)) {
-                        try {
-                            FileLog.d(TAG, "Deleting invalid widget " + widgetId);
-                            holder.deleteAppWidgetId(widgetId);
-                            isAnyWidgetRemoved = true;
-                        } catch (RuntimeException e) {
-                            // Ignore
-                        }
-                    }
-                }
-                if (isAnyWidgetRemoved) {
-                    final String allWidgetsIds = Arrays.stream(allWidgets).mapToObj(String::valueOf)
-                            .collect(Collectors.joining(",", "[", "]"));
-                    final String validWidgetsIds = Arrays.stream(
-                                    validWidgets.getArray().toArray()).mapToObj(String::valueOf)
-                            .collect(Collectors.joining(",", "[", "]"));
-                    FileLog.d(TAG, "One or more widgets was removed. db_path=" + db.getPath()
-                            + " allWidgetsIds=" + allWidgetsIds
-                            + ", validWidgetsIds=" + validWidgetsIds);
-                }
-            } finally {
-                holder.destroy();
-            }
-        }
-
-        /**
-         * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
-         * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
-         */
-        @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
-            try (SQLiteTransaction t = new SQLiteTransaction(db);
-                 // Only consider the primary user as other users can't have a shortcut.
-                 Cursor c = db.query(Favorites.TABLE_NAME,
-                         new String[] { Favorites._ID, Favorites.INTENT},
-                         "itemType=" + Favorites.ITEM_TYPE_SHORTCUT +
-                                 " AND profileId=" + getDefaultUserSerial(),
-                         null, null, null, null);
-                 SQLiteStatement updateStmt = db.compileStatement("UPDATE favorites SET itemType="
-                         + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?")
-            ) {
-                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
-                final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
-
-                while (c.moveToNext()) {
-                    String intentDescription = c.getString(intentIndex);
-                    Intent intent;
-                    try {
-                        intent = Intent.parseUri(intentDescription, 0);
-                    } catch (URISyntaxException e) {
-                        Log.e(TAG, "Unable to parse intent", e);
-                        continue;
-                    }
-
-                    if (!PackageManagerHelper.isLauncherAppTarget(intent)) {
-                        continue;
-                    }
-
-                    int id = c.getInt(idIndex);
-                    updateStmt.bindLong(1, id);
-                    updateStmt.executeUpdateDelete();
-                }
-                t.commit();
-            } catch (SQLException ex) {
-                Log.w(TAG, "Error deduping shortcuts", ex);
-            }
-        }
-
-        @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
-            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-                if (addRankColumn) {
-                    // Insert new column for holding rank
-                    db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
-                }
-
-                // Get a map for folder ID to folder width
-                Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
-                        + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
-                        + " GROUP BY container;",
-                        new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
-
-                while (c.moveToNext()) {
-                    db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
-                            + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
-                            new Object[] {c.getLong(1) + 1, c.getLong(0)});
-                }
-
-                c.close();
-                t.commit();
-            } catch (SQLException ex) {
-                // Old version remains, which means we wipe old data
-                Log.e(TAG, ex.getMessage(), ex);
-                return false;
-            }
-            return true;
-        }
-
-        private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
-            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
-                db.execSQL("ALTER TABLE favorites ADD COLUMN "
-                        + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
-                t.commit();
-            } catch (SQLException ex) {
-                Log.e(TAG, ex.getMessage(), ex);
-                return false;
-            }
-            return true;
-        }
-
-        // Generates a new ID to use for an object in your database. This method should be only
-        // called from the main UI thread. As an exception, we do call it when we call the
-        // constructor from the worker thread; however, this doesn't extend until after the
-        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
-        // after that point
-        @Override
-        public int generateNewItemId() {
-            if (mMaxItemId < 0) {
-                throw new RuntimeException("Error: max item id was not initialized");
-            }
-            mMaxItemId += 1;
-            return mMaxItemId;
-        }
-
-        /**
-         * @return A new {@link LauncherWidgetHolder} based on the current context
-         */
-        @NonNull
-        public LauncherWidgetHolder newLauncherWidgetHolder() {
-            return LauncherWidgetHolder.newInstance(mContext);
-        }
-
-        @Override
-        public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
-            return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
-        }
-
-        public void checkId(ContentValues values) {
-            int id = values.getAsInteger(Favorites._ID);
-            mMaxItemId = Math.max(id, mMaxItemId);
-        }
-
-        private int initializeMaxItemId(SQLiteDatabase db) {
-            return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID, Favorites.TABLE_NAME);
-        }
-
-        // Returns a new ID to use for an workspace screen in your database that is greater than all
-        // existing screen IDs.
-        private int getNewScreenId() {
-            return getMaxId(getWritableDatabase(),
-                    "SELECT MAX(%1$s) FROM %2$s WHERE %3$s = %4$d AND %1$s >= 0",
-                    Favorites.SCREEN, Favorites.TABLE_NAME, Favorites.CONTAINER,
-                    Favorites.CONTAINER_DESKTOP) + 1;
-        }
-
-        @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
-            // TODO: Use multiple loaders with fall-back and transaction.
-            int count = loader.loadLayout(db, new IntArray());
-
-            // Ensure that the max ids are initialized
-            mMaxItemId = initializeMaxItemId(db);
-            return count;
-        }
-    }
-
-    /**
-     * @return the max _id in the provided table.
-     */
-    @Thunk static int getMaxId(SQLiteDatabase db, String query, Object... args) {
-        int max = 0;
-        try (SQLiteStatement prog = db.compileStatement(
-                String.format(Locale.ENGLISH, query, args))) {
-            max = (int) DatabaseUtils.longForQuery(prog, null);
-            if (max < 0) {
-                throw new RuntimeException("Error: could not query max id");
-            }
-        } catch (IllegalArgumentException exception) {
-            String message = exception.getMessage();
-            if (message.contains("re-open") && message.contains("already-closed")) {
-                // Don't crash trying to end a transaction an an already closed DB. See b/173162852.
-            } else {
-                throw exception;
-            }
-        }
-        return max;
-    }
-
     static class SqlArguments {
         public final String table;
         public final String where;
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
new file mode 100644
index 0000000..3578b67
--- /dev/null
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2023 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.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.os.Process;
+import android.os.UserHandle;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.NoLocaleSQLiteHelper;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherWidgetHolder;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * SqLite database for launcher home-screen model
+ * The class is subclassed in tests to create an in-memory db.
+ */
+public class DatabaseHelper extends NoLocaleSQLiteHelper implements
+        LayoutParserCallback {
+
+    /**
+     * Represents the schema of the database. Changes in scheme need not be backwards compatible.
+     * When increasing the scheme version, ensure that downgrade_schema.json is updated
+     */
+    public static final int SCHEMA_VERSION = 31;
+    private static final String TAG = "DatabaseHelper";
+    private static final boolean LOGD = false;
+
+    private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
+
+    private final Context mContext;
+    private final boolean mForMigration;
+    private int mMaxItemId = -1;
+    public boolean mHotseatRestoreTableExists;
+
+    public static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
+        return createDatabaseHelper(context, null, forMigration);
+    }
+
+    public static DatabaseHelper createDatabaseHelper(Context context, String dbName,
+            boolean forMigration) {
+        if (dbName == null) {
+            dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
+        }
+        DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
+        // Table creation sometimes fails silently, which leads to a crash loop.
+        // This way, we will try to create a table every time after crash, so the device
+        // would eventually be able to recover.
+        if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
+            Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
+            // This operation is a no-op if the table already exists.
+            databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
+        }
+        databaseHelper.mHotseatRestoreTableExists = tableExists(
+                databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+
+        databaseHelper.initIds();
+        return databaseHelper;
+    }
+
+    /**
+     * Constructor used in tests and for restore.
+     */
+    public DatabaseHelper(Context context, String dbName, boolean forMigration) {
+        super(context, dbName, SCHEMA_VERSION);
+        mContext = context;
+        mForMigration = forMigration;
+    }
+
+    protected void initIds() {
+        // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
+        // the DB here
+        if (mMaxItemId == -1) {
+            mMaxItemId = initializeMaxItemId(getWritableDatabase());
+        }
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        if (LOGD) Log.d(TAG, "creating new launcher database");
+
+        mMaxItemId = 1;
+
+        addFavoritesTable(db, false);
+
+        // Fresh and clean launcher DB.
+        mMaxItemId = initializeMaxItemId(db);
+        if (!mForMigration) {
+            onEmptyDbCreated();
+        }
+    }
+
+    public void onAddOrDeleteOp(SQLiteDatabase db) {
+        if (mHotseatRestoreTableExists) {
+            dropTable(db, Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+            mHotseatRestoreTableExists = false;
+        }
+    }
+
+    /**
+     * Re-composite given key in respect to database. If the current db is
+     * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
+     * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
+     * string will be "EMPTY_DATABASE_CREATED@minimal.db".
+     */
+    public String getKey(final String key) {
+        if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
+            return key;
+        }
+        return key + "@" + getDatabaseName();
+    }
+
+    /**
+     * Overridden in tests.
+     */
+    protected void onEmptyDbCreated() {
+        // Set the flag for empty DB
+        LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(
+                        LauncherProvider.EMPTY_DATABASE_CREATED), true)
+                .commit();
+    }
+
+    public long getSerialNumberForUser(UserHandle user) {
+        return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
+    }
+
+    public long getDefaultUserSerial() {
+        return getSerialNumberForUser(Process.myUserHandle());
+    }
+
+    private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
+        Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
+    }
+
+    @Override
+    public void onOpen(SQLiteDatabase db) {
+        super.onOpen(db);
+
+        File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE);
+        if (!schemaFile.exists()) {
+            handleOneTimeDataUpgrade(db);
+        }
+        DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext);
+    }
+
+    /**
+     * One-time data updated before support of onDowngrade was added. This update is backwards
+     * compatible and can safely be run multiple times.
+     * Note: No new logic should be added here after release, as the new logic might not get
+     * executed on an existing device.
+     * TODO: Move this to db upgrade path, once the downgrade path is released.
+     */
+    protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
+        // Remove "profile extra"
+        UserCache um = UserCache.INSTANCE.get(mContext);
+        for (UserHandle user : um.getUserProfiles()) {
+            long serial = um.getSerialNumberForUser(user);
+            String sql = "update favorites set intent = replace(intent, "
+                    + "';l.profile=" + serial + ";', ';') where itemType = 0;";
+            db.execSQL(sql);
+        }
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        if (LOGD) {
+            Log.d(TAG, "onUpgrade triggered: " + oldVersion);
+        }
+        switch (oldVersion) {
+            // The version cannot be lower that 12, as Launcher3 never supported a lower
+            // version of the DB.
+            case 12:
+                // No-op
+            case 13: {
+                try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+                    // Insert new column for holding widget provider name
+                    db.execSQL("ALTER TABLE favorites ADD COLUMN appWidgetProvider TEXT;");
+                    t.commit();
+                } catch (SQLException ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 14: {
+                if (!addIntegerColumn(db, Favorites.MODIFIED, 0)) {
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 15: {
+                if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 16:
+                // No-op
+            case 17:
+                // No-op
+            case 18:
+                // No-op
+            case 19: {
+                // Add userId column
+                if (!addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial())) {
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 20:
+                if (!updateFolderItemsRank(db, true)) {
+                    break;
+                }
+            case 21:
+                // No-op
+            case 22: {
+                if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 23:
+                // No-op
+            case 24:
+                // No-op
+            case 25:
+                convertShortcutsToLauncherActivities(db);
+            case 26:
+                // QSB was moved to the grid. Ignore overlapping items
+            case 27: {
+                // Update the favorites table so that the screen ids are ordered based on
+                // workspace page rank.
+                IntArray finalScreens = LauncherDbUtils.queryIntArray(false, db,
+                        "workspaceScreens", BaseColumns._ID, null, null, "screenRank");
+                int[] original = finalScreens.toArray();
+                Arrays.sort(original);
+                String updatemap = "";
+                for (int i = 0; i < original.length; i++) {
+                    if (finalScreens.get(i) != original[i]) {
+                        updatemap += String.format(Locale.ENGLISH, " WHEN %1$s=%2$d THEN %3$d",
+                                Favorites.SCREEN, finalScreens.get(i), original[i]);
+                    }
+                }
+                if (!TextUtils.isEmpty(updatemap)) {
+                    String query = String.format(Locale.ENGLISH,
+                            "UPDATE %1$s SET %2$s=CASE %3$s ELSE %2$s END WHERE %4$s = %5$d",
+                            Favorites.TABLE_NAME, Favorites.SCREEN, updatemap,
+                            Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP);
+                    db.execSQL(query);
+                }
+                dropTable(db, "workspaceScreens");
+            }
+            case 28: {
+                boolean columnAdded = addIntegerColumn(
+                        db, Favorites.APPWIDGET_SOURCE, Favorites.CONTAINER_UNKNOWN);
+                if (!columnAdded) {
+                    // Old version remains, which means we wipe old data
+                    break;
+                }
+            }
+            case 29: {
+                // Remove widget panel related leftover workspace items
+                db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
+                        Favorites.SCREEN, IntArray.wrap(-777, -778)), null);
+            }
+            case 30: {
+                if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
+                    // Clean up first row in screen 0 as it might contain junk data.
+                    Log.d(TAG, "Cleaning up first row");
+                    db.delete(Favorites.TABLE_NAME,
+                            String.format(Locale.ENGLISH,
+                                    "%1$s = %2$d AND %3$s = %4$d AND %5$s = %6$d",
+                                    Favorites.SCREEN, 0,
+                                    Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP,
+                                    Favorites.CELLY, 0), null);
+                }
+                return;
+            }
+            case 31: {
+                // DB Upgraded successfully
+                return;
+            }
+        }
+
+        // DB was not upgraded
+        Log.w(TAG, "Destroying all old data.");
+        createEmptyDB(db);
+    }
+
+    @Override
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        try {
+            DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE))
+                    .onDowngrade(db, oldVersion, newVersion);
+        } catch (Exception e) {
+            Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion
+                    + ". Wiping database.", e);
+            createEmptyDB(db);
+        }
+    }
+
+    /**
+     * Clears all the data for a fresh start.
+     */
+    public void createEmptyDB(SQLiteDatabase db) {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            dropTable(db, Favorites.TABLE_NAME);
+            dropTable(db, "workspaceScreens");
+            onCreate(db);
+            t.commit();
+        }
+    }
+
+    /**
+     * Removes widgets which are registered to the Launcher's host, but are not present
+     * in our model.
+     */
+    public void removeGhostWidgets(SQLiteDatabase db) {
+        // Get all existing widget ids.
+        final LauncherWidgetHolder holder = newLauncherWidgetHolder();
+        try {
+            final int[] allWidgets;
+            try {
+                // Although the method was defined in O, it has existed since the beginning of
+                // time, so it might work on older platforms as well.
+                allWidgets = holder.getAppWidgetIds();
+            } catch (IncompatibleClassChangeError e) {
+                Log.e(TAG, "getAppWidgetIds not supported", e);
+                return;
+            }
+            final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db,
+                    Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
+                    "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
+            boolean isAnyWidgetRemoved = false;
+            for (int widgetId : allWidgets) {
+                if (!validWidgets.contains(widgetId)) {
+                    try {
+                        FileLog.d(TAG, "Deleting invalid widget " + widgetId);
+                        holder.deleteAppWidgetId(widgetId);
+                        isAnyWidgetRemoved = true;
+                    } catch (RuntimeException e) {
+                        // Ignore
+                    }
+                }
+            }
+            if (isAnyWidgetRemoved) {
+                final String allWidgetsIds = Arrays.stream(allWidgets).mapToObj(String::valueOf)
+                        .collect(Collectors.joining(",", "[", "]"));
+                final String validWidgetsIds = Arrays.stream(
+                                validWidgets.getArray().toArray()).mapToObj(String::valueOf)
+                        .collect(Collectors.joining(",", "[", "]"));
+                FileLog.d(TAG,
+                        "One or more widgets was removed. db_path=" + db.getPath()
+                                + " allWidgetsIds=" + allWidgetsIds
+                                + ", validWidgetsIds=" + validWidgetsIds);
+            }
+        } finally {
+            holder.destroy();
+        }
+    }
+
+    /**
+     * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
+     * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
+     */
+    @Thunk
+    void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
+        try (SQLiteTransaction t = new SQLiteTransaction(db);
+             // Only consider the primary user as other users can't have a shortcut.
+             Cursor c = db.query(Favorites.TABLE_NAME,
+                     new String[]{Favorites._ID, Favorites.INTENT},
+                     "itemType=" + Favorites.ITEM_TYPE_SHORTCUT
+                             + " AND profileId=" + getDefaultUserSerial(),
+                     null, null, null, null);
+             SQLiteStatement updateStmt = db.compileStatement("UPDATE favorites SET itemType="
+                     + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?")
+        ) {
+            final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
+            final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
+
+            while (c.moveToNext()) {
+                String intentDescription = c.getString(intentIndex);
+                Intent intent;
+                try {
+                    intent = Intent.parseUri(intentDescription, 0);
+                } catch (URISyntaxException e) {
+                    Log.e(TAG, "Unable to parse intent", e);
+                    continue;
+                }
+
+                if (!PackageManagerHelper.isLauncherAppTarget(intent)) {
+                    continue;
+                }
+
+                int id = c.getInt(idIndex);
+                updateStmt.bindLong(1, id);
+                updateStmt.executeUpdateDelete();
+            }
+            t.commit();
+        } catch (SQLException ex) {
+            Log.w(TAG, "Error deduping shortcuts", ex);
+        }
+    }
+
+    @Thunk
+    boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            if (addRankColumn) {
+                // Insert new column for holding rank
+                db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
+            }
+
+            // Get a map for folder ID to folder width
+            Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
+                            + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
+                            + " GROUP BY container;",
+                    new String[]{Integer.toString(Favorites.ITEM_TYPE_FOLDER)});
+
+            while (c.moveToNext()) {
+                db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
+                                + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
+                        new Object[]{c.getLong(1) + 1, c.getLong(0)});
+            }
+
+            c.close();
+            t.commit();
+        } catch (SQLException ex) {
+            // Old version remains, which means we wipe old data
+            Log.e(TAG, ex.getMessage(), ex);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+            db.execSQL("ALTER TABLE favorites ADD COLUMN "
+                    + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
+            t.commit();
+        } catch (SQLException ex) {
+            Log.e(TAG, ex.getMessage(), ex);
+            return false;
+        }
+        return true;
+    }
+
+    // Generates a new ID to use for an object in your database. This method should be only
+    // called from the main UI thread. As an exception, we do call it when we call the
+    // constructor from the worker thread; however, this doesn't extend until after the
+    // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
+    // after that point
+    @Override
+    public int generateNewItemId() {
+        if (mMaxItemId < 0) {
+            throw new RuntimeException("Error: max item id was not initialized");
+        }
+        mMaxItemId += 1;
+        return mMaxItemId;
+    }
+
+    /**
+     * @return A new {@link LauncherWidgetHolder} based on the current context
+     */
+    @NonNull
+    public LauncherWidgetHolder newLauncherWidgetHolder() {
+        return LauncherWidgetHolder.newInstance(mContext);
+    }
+
+    @Override
+    public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
+        return dbInsertAndCheck(db, Favorites.TABLE_NAME, values);
+    }
+
+    public int dbInsertAndCheck(SQLiteDatabase db, String table, ContentValues values) {
+        if (values == null) {
+            throw new RuntimeException("Error: attempting to insert null values");
+        }
+        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
+            throw new RuntimeException("Error: attempting to add item without specifying an id");
+        }
+        checkId(values);
+        return (int) db.insert(table, null, values);
+    }
+
+    public void checkId(ContentValues values) {
+        int id = values.getAsInteger(Favorites._ID);
+        mMaxItemId = Math.max(id, mMaxItemId);
+    }
+
+    private int initializeMaxItemId(SQLiteDatabase db) {
+        return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID,
+                Favorites.TABLE_NAME);
+    }
+
+    /**
+     * Returns a new ID to use for a workspace screen in your database that is greater than all
+     * existing screen IDs
+     */
+    public int getNewScreenId() {
+        return getMaxId(getWritableDatabase(),
+                "SELECT MAX(%1$s) FROM %2$s WHERE %3$s = %4$d AND %1$s >= 0",
+                Favorites.SCREEN, Favorites.TABLE_NAME, Favorites.CONTAINER,
+                Favorites.CONTAINER_DESKTOP) + 1;
+    }
+
+    public int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
+        // TODO: Use multiple loaders with fall-back and transaction.
+        int count = loader.loadLayout(db, new IntArray());
+
+        // Ensure that the max ids are initialized
+        mMaxItemId = initializeMaxItemId(db);
+        return count;
+    }
+
+    /**
+     * @return the max _id in the provided table.
+     */
+    private static int getMaxId(SQLiteDatabase db, String query, Object... args) {
+        int max = 0;
+        try (SQLiteStatement prog = db.compileStatement(
+                String.format(Locale.ENGLISH, query, args))) {
+            max = (int) DatabaseUtils.longForQuery(prog, null);
+            if (max < 0) {
+                throw new RuntimeException("Error: could not query max id");
+            }
+        } catch (IllegalArgumentException exception) {
+            String message = exception.getMessage();
+            if (message.contains("re-open") && message.contains("already-closed")) {
+                // Don't crash trying to end a transaction an an already closed DB. See b/173162852.
+            } else {
+                throw exception;
+            }
+        }
+        return max;
+    }
+}
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 6450c8e..c4eb14f 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -44,10 +44,10 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.model.GridBackupTable;
 import com.android.launcher3.model.data.AppInfo;