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;