Adding support for backing up favorites table

Favorites table is copied as a separate table name during the first grid migration.
On subsequent migrations this backup table is used if it exists, otherwise new
backup is created. The backup table is also removed if there is any insert or
delete operation on the db (outside of the migration operation itself).

Bug: 111850268
Bug: 121048571
Change-Id: I6f02f4a355c369ee99d89430971be258f7516f6e
This commit is contained in:
Sunny Goyal 2018-10-29 14:02:20 -07:00
parent efb8b83666
commit 161a214ede
11 changed files with 555 additions and 213 deletions

View File

@ -0,0 +1,121 @@
package com.android.launcher3.model;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.util.TestLauncherProvider;
import org.junit.Before;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowLog;
public abstract class BaseGridChangesTestCase {
public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
public static final int NO__ICON = -1;
public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
public Context mContext;
public TestLauncherProvider mProvider;
public SQLiteDatabase mDb;
@Before
public void setUpBaseCase() {
ShadowLog.stream = System.out;
mContext = RuntimeEnvironment.application;
mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
mDb = mProvider.getDb();
}
/**
* Adds a dummy item in the DB.
* @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
* folder (where the type represents the number of items in the folder).
*/
public int addItem(int type, int screen, int container, int x, int y) {
int id = LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites._ID, id);
values.put(LauncherSettings.Favorites.CONTAINER, container);
values.put(LauncherSettings.Favorites.SCREEN, screen);
values.put(LauncherSettings.Favorites.CELLX, x);
values.put(LauncherSettings.Favorites.CELLY, y);
values.put(LauncherSettings.Favorites.SPANX, 1);
values.put(LauncherSettings.Favorites.SPANY, 1);
if (type == APP_ICON || type == SHORTCUT) {
values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
values.put(LauncherSettings.Favorites.INTENT,
new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
} else {
values.put(LauncherSettings.Favorites.ITEM_TYPE,
LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
// Add folder items.
for (int i = 0; i < type; i++) {
addItem(APP_ICON, 0, id, 0, 0);
}
}
mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
return id;
}
public int[][][] createGrid(int[][][] typeArray) {
return createGrid(typeArray, 1);
}
/**
* Initializes the DB with dummy elements to represent the provided grid structure.
* @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
* type definitions. The first dimension represents the screens and the next
* two represent the workspace grid.
* @param startScreen First screen id from where the icons will be added.
* @return the same grid representation where each entry is the corresponding item id.
*/
public int[][][] createGrid(int[][][] typeArray, int startScreen) {
LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
int[][][] ids = new int[typeArray.length][][];
for (int i = 0; i < typeArray.length; i++) {
// Add screen to DB
int screenId = startScreen + i;
// Keep the screen id counter up to date
LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
ids[i] = new int[typeArray[i].length][];
for (int y = 0; y < typeArray[i].length; y++) {
ids[i][y] = new int[typeArray[i][y].length];
for (int x = 0; x < typeArray[i][y].length; x++) {
if (typeArray[i][y][x] < 0) {
// Empty cell
ids[i][y][x] = -1;
} else {
ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
}
}
}
}
return ids;
}
}

View File

@ -0,0 +1,115 @@
package com.android.launcher3.model;
import static android.database.DatabaseUtils.queryNumEntries;
import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.ContentValues;
import android.graphics.Point;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/**
* Unit tests for {@link GridBackupTable}
*/
@RunWith(RobolectricTestRunner.class)
public class GridBackupTableTest extends BaseGridChangesTestCase {
private static final int BACKUP_ITEM_COUNT = 12;
@Before
public void setupGridData() {
createGrid(new int[][][]{{
{ APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
{ SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
{ NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
{ APP_ICON, SHORTCUT, SHORTCUT, APP_ICON},
}});
assertEquals(BACKUP_ITEM_COUNT, queryNumEntries(mDb, TABLE_NAME));
}
@Test
public void backupTableCreated() {
GridBackupTable backupTable = new GridBackupTable(mContext, mDb, 4, 4, 4);
assertFalse(backupTable.backupOrRestoreAsNeeded());
Settings.call(mContext.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
// One extra entry for properties
assertEquals(BACKUP_ITEM_COUNT + 1, queryNumEntries(mDb, BACKUP_TABLE_NAME));
}
@Test
public void backupTableRestored() {
assertFalse(new GridBackupTable(mContext, mDb, 4, 4, 4).backupOrRestoreAsNeeded());
Settings.call(mContext.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
// Delete entries
mDb.delete(TABLE_NAME, null, null);
assertEquals(0, queryNumEntries(mDb, TABLE_NAME));
GridBackupTable backupTable = new GridBackupTable(mContext, mDb, 3, 3, 3);
assertTrue(backupTable.backupOrRestoreAsNeeded());
// Items have been restored
assertEquals(BACKUP_ITEM_COUNT, queryNumEntries(mDb, TABLE_NAME));
Point outSize = new Point();
assertEquals(4, backupTable.getRestoreHotseatAndGridSize(outSize));
assertEquals(4, outSize.x);
assertEquals(4, outSize.y);
}
@Test
public void backupTableRemovedOnAdd() {
assertFalse(new GridBackupTable(mContext, mDb, 4, 4, 4).backupOrRestoreAsNeeded());
Settings.call(mContext.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
addItem(1, 2, DESKTOP, 1, 1);
assertFalse(tableExists(mDb, BACKUP_TABLE_NAME));
}
@Test
public void backupTableRemovedOnDelete() {
assertFalse(new GridBackupTable(mContext, mDb, 4, 4, 4).backupOrRestoreAsNeeded());
Settings.call(mContext.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
mContext.getContentResolver().delete(Favorites.CONTENT_URI, null, null);
assertFalse(tableExists(mDb, BACKUP_TABLE_NAME));
}
@Test
public void backupTableRetainedOnUpdate() {
assertFalse(new GridBackupTable(mContext, mDb, 4, 4, 4).backupOrRestoreAsNeeded());
Settings.call(mContext.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
ContentValues values = new ContentValues();
values.put(Favorites.RANK, 4);
// Something was updated
assertTrue(mContext.getContentResolver()
.update(Favorites.CONTENT_URI, values, null, null) > 0);
// Backup table remains
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
}
}

View File

@ -5,29 +5,21 @@ import static com.android.launcher3.model.GridSizeMigrationTask.getWorkspaceScre
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Point;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.config.FlagOverrideRule;
import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.TestLauncherProvider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowContentResolver;
import java.util.HashSet;
import java.util.LinkedList;
@ -36,47 +28,33 @@ import java.util.LinkedList;
* Unit tests for {@link GridSizeMigrationTask}
*/
@RunWith(RobolectricTestRunner.class)
public class GridSizeMigrationTaskTest {
private static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
private static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
private static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
public class GridSizeMigrationTaskTest extends BaseGridChangesTestCase {
@Rule
public final FlagOverrideRule flags = new FlagOverrideRule();
private HashSet<String> mValidPackages;
private InvariantDeviceProfile mIdp;
private Context mContext;
private TestLauncherProvider mProvider;
@Before
public void setUp() {
mValidPackages = new HashSet<>();
mValidPackages.add(TEST_PACKAGE);
mIdp = new InvariantDeviceProfile();
mContext = RuntimeEnvironment.application;
mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
}
@Test
public void testHotseatMigration_apps_dropped() throws Exception {
int[] hotseatItems = {
addItem(APPLICATION, 0, HOTSEAT, 0, 0),
addItem(APP_ICON, 0, HOTSEAT, 0, 0),
addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
-1,
addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
addItem(APPLICATION, 4, HOTSEAT, 0, 0),
addItem(APP_ICON, 4, HOTSEAT, 0, 0),
};
mIdp.numHotseatIcons = 3;
new GridSizeMigrationTask(mContext, mIdp, mValidPackages, 5, 3)
new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
.migrateHotseat();
// First item is dropped as it has the least weight.
verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@ -85,7 +63,7 @@ public class GridSizeMigrationTaskTest {
@Test
public void testHotseatMigration_shortcuts_dropped() throws Exception {
int[] hotseatItems = {
addItem(APPLICATION, 0, HOTSEAT, 0, 0),
addItem(APP_ICON, 0, HOTSEAT, 0, 0),
addItem(30, 1, HOTSEAT, 0, 0),
-1,
addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
@ -93,7 +71,7 @@ public class GridSizeMigrationTaskTest {
};
mIdp.numHotseatIcons = 3;
new GridSizeMigrationTask(mContext, mIdp, mValidPackages, 5, 3)
new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
.migrateHotseat();
// First item is dropped as it has the least weight.
verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@ -138,7 +116,7 @@ public class GridSizeMigrationTaskTest {
{ 5, 2, -1, 6},
}});
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 3)).migrateWorkspace();
// Column 2 and row 2 got removed.
@ -158,7 +136,7 @@ public class GridSizeMigrationTaskTest {
{ 5, 2, -1, 6},
}});
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 3)).migrateWorkspace();
// Items in the second column get moved to new screen
@ -183,7 +161,7 @@ public class GridSizeMigrationTaskTest {
{ 3, 1, -1, 4},
}});
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 3)).migrateWorkspace();
// Items in the second column of the first screen should get placed on the 3rd
@ -215,7 +193,7 @@ public class GridSizeMigrationTaskTest {
{ 5, 2, -1, 6},
}});
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 3)).migrateWorkspace();
// Items in the second column of the first screen should get placed on a new screen.
@ -244,7 +222,7 @@ public class GridSizeMigrationTaskTest {
{ 5, 2, 7, -1},
}}, 0);
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 4)).migrateWorkspace();
// Items in the second column of the first screen should get placed on a new screen.
@ -269,7 +247,7 @@ public class GridSizeMigrationTaskTest {
{ 5, 6, 7, -1},
}}, 0);
new GridSizeMigrationTask(mContext, mIdp, mValidPackages,
new GridSizeMigrationTask(mContext, mDb, mValidPackages,
new Point(4, 4), new Point(3, 3)).migrateWorkspace();
// Items in the second column of the first screen should get placed on a new screen.
@ -283,54 +261,13 @@ public class GridSizeMigrationTaskTest {
}});
}
private int[][][] createGrid(int[][][] typeArray) throws Exception {
return createGrid(typeArray, 1);
}
/**
* Initializes the DB with dummy elements to represent the provided grid structure.
* @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
* type definitions. The first dimension represents the screens and the next
* two represent the workspace grid.
* @return the same grid representation where each entry is the corresponding item id.
*/
private int[][][] createGrid(int[][][] typeArray, int startScreen) throws Exception {
LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
int[][][] ids = new int[typeArray.length][][];
for (int i = 0; i < typeArray.length; i++) {
// Add screen to DB
int screenId = startScreen + i;
// Keep the screen id counter up to date
LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
ids[i] = new int[typeArray[i].length][];
for (int y = 0; y < typeArray[i].length; y++) {
ids[i][y] = new int[typeArray[i][y].length];
for (int x = 0; x < typeArray[i][y].length; x++) {
if (typeArray[i][y][x] < 0) {
// Empty cell
ids[i][y][x] = -1;
} else {
ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
}
}
}
}
return ids;
}
/**
* Verifies that the workspace items are arranged in the provided order.
* @param ids A 3d array where the first dimension represents the screen, and the rest two
* represent the workspace grid.
*/
private void verifyWorkspace(int[][][] ids) {
IntArray allScreens = getWorkspaceScreenIds(mContext);
IntArray allScreens = getWorkspaceScreenIds(mDb);
assertEquals(ids.length, allScreens.size());
int total = 0;
@ -367,42 +304,6 @@ public class GridSizeMigrationTaskTest {
c.close();
}
/**
* Adds a dummy item in the DB.
* @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for
* folder (where the type represents the number of items in the folder).
*/
private int addItem(int type, int screen, int container, int x, int y) throws Exception {
int id = LauncherSettings.Settings.call(mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites._ID, id);
values.put(LauncherSettings.Favorites.CONTAINER, container);
values.put(LauncherSettings.Favorites.SCREEN, screen);
values.put(LauncherSettings.Favorites.CELLX, x);
values.put(LauncherSettings.Favorites.CELLY, y);
values.put(LauncherSettings.Favorites.SPANX, 1);
values.put(LauncherSettings.Favorites.SPANY, 1);
if (type == APPLICATION || type == SHORTCUT) {
values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
values.put(LauncherSettings.Favorites.INTENT,
new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
} else {
values.put(LauncherSettings.Favorites.ITEM_TYPE,
LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
// Add folder items.
for (int i = 0; i < type; i++) {
addItem(APPLICATION, 0, id, 0, 0);
}
}
mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
return id;
}
@Test
public void testMultiStepMigration_small_to_large() throws Exception {
MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier();
@ -435,7 +336,7 @@ public class GridSizeMigrationTaskTest {
private final LinkedList<Point> mPoints;
public MultiStepMigrationTaskVerifier(int... points) {
super(null, null);
super(null, null, null);
mPoints = new LinkedList<>();
for (int i = 0; i < points.length; i += 2) {

View File

@ -23,6 +23,11 @@ public class TestLauncherProvider extends LauncherProvider {
}
}
public SQLiteDatabase getDb() {
createDbIfNotExists();
return mOpenHelper.getWritableDatabase();
}
@Override
protected void notifyListeners() { }

View File

@ -16,6 +16,9 @@
package com.android.launcher3;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
import android.annotation.TargetApi;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetManager;
@ -210,6 +213,7 @@ public class LauncherProvider extends ContentProvider {
addModifiedTime(initialValues);
final int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
if (rowId < 0) return null;
mOpenHelper.onAddOrDeleteOp(db);
uri = ContentUris.withAppendedId(uri, rowId);
notifyListeners();
@ -282,6 +286,7 @@ public class LauncherProvider extends ContentProvider {
return 0;
}
}
mOpenHelper.onAddOrDeleteOp(db);
t.commit();
}
@ -290,15 +295,30 @@ public class LauncherProvider extends ContentProvider {
return values.length;
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
createDbIfNotExists();
try (SQLiteTransaction t = new SQLiteTransaction(mOpenHelper.getWritableDatabase())) {
ContentProviderResult[] result = super.applyBatch(operations);
boolean isAddOrDelete = !Utilities.ATLEAST_MARSHMALLOW;
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
ContentProviderOperation op = operations.get(i);
results[i] = op.apply(this, results, i);
isAddOrDelete |= (op.isInsert() || op.isDelete()) &&
results[i].count != null && results[i].count > 0;
}
if (isAddOrDelete) {
mOpenHelper.onAddOrDeleteOp(t.getDb());
}
t.commit();
reloadLauncherIfExternal();
return result;
return results;
}
}
@ -315,6 +335,7 @@ public class LauncherProvider extends ContentProvider {
}
int count = db.delete(args.table, args.where, args.args);
if (count > 0) {
mOpenHelper.onAddOrDeleteOp(db);
notifyListeners();
reloadLauncherIfExternal();
}
@ -381,6 +402,17 @@ public class LauncherProvider extends ContentProvider {
mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
return null;
}
case LauncherSettings.Settings.METHOD_NEW_TRANSACTION: {
Bundle result = new Bundle();
result.putBinder(LauncherSettings.Settings.EXTRA_VALUE,
new SQLiteTransaction(mOpenHelper.getWritableDatabase()));
return result;
}
case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
mOpenHelper.mBackupTableExists =
tableExists(mOpenHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
return null;
}
}
return null;
}
@ -528,17 +560,19 @@ public class LauncherProvider extends ContentProvider {
private final Context mContext;
private int mMaxItemId = -1;
private int mMaxScreenId = -1;
private boolean mBackupTableExists;
DatabaseHelper(Context context, Handler widgetHostResetHandler) {
this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
// 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(Favorites.TABLE_NAME)) {
if (!tableExists(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.
addFavoritesTable(getWritableDatabase(), true);
}
mBackupTableExists = tableExists(getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
initIds();
}
@ -564,18 +598,6 @@ public class LauncherProvider extends ContentProvider {
}
}
private boolean tableExists(String tableName) {
Cursor c = getReadableDatabase().query(
true, "sqlite_master", new String[] {"tbl_name"},
"tbl_name = ?", new String[] {tableName},
null, null, null, null, null);
try {
return c.getCount() > 0;
} finally {
c.close();
}
}
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
@ -590,6 +612,13 @@ public class LauncherProvider extends ContentProvider {
onEmptyDbCreated();
}
protected void onAddOrDeleteOp(SQLiteDatabase db) {
if (mBackupTableExists) {
dropTable(db, Favorites.BACKUP_TABLE_NAME);
mBackupTableExists = false;
}
}
/**
* Overriden in tests.
*/
@ -733,7 +762,7 @@ public class LauncherProvider extends ContentProvider {
Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP);
db.execSQL(query);
}
db.execSQL("DROP TABLE IF EXISTS workspaceScreens");
dropTable(db, "workspaceScreens");
}
case 28:
// DB Upgraded successfully
@ -762,8 +791,8 @@ public class LauncherProvider extends ContentProvider {
*/
public void createEmptyDB(SQLiteDatabase db) {
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS workspaceScreens");
dropTable(db, Favorites.TABLE_NAME);
dropTable(db, "workspaceScreens");
onCreate(db);
t.commit();
}

View File

@ -88,6 +88,11 @@ public class LauncherSettings {
public static final String TABLE_NAME = "favorites";
/**
* Backup table created when when the favorites table is modified during grid migration
*/
public static final String BACKUP_TABLE_NAME = "favorites_bakup";
/**
* The content:// style URL for this table
*/
@ -231,8 +236,13 @@ public class LauncherSettings {
public static final String OPTIONS = "options";
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
addTableToDb(db, myProfileId, optional, TABLE_NAME);
}
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
String tableName) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + TABLE_NAME + " (" +
db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
"_id INTEGER PRIMARY KEY," +
"title TEXT," +
"intent TEXT," +
@ -279,6 +289,10 @@ public class LauncherSettings {
public static final String METHOD_REMOVE_GHOST_WIDGETS = "remove_ghost_widgets";
public static final String METHOD_NEW_TRANSACTION = "new_db_transaction";
public static final String METHOD_REFRESH_BACKUP_TABLE = "refresh_backup_table";
public static final String EXTRA_VALUE = "value";
public static Bundle call(ContentResolver cr, String method) {

View File

@ -0,0 +1,135 @@
/*
* Copyright (C) 2018 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.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
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.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point;
import android.os.Process;
import android.util.Log;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
import com.android.launcher3.compat.UserManagerCompat;
/**
* Helper class to backup and restore Favorites table into a separate table
* within the same data base.
*/
public class GridBackupTable {
private static final String TAG = "GridBackupTable";
private static final int ID_PROPERTY = -1;
private static final String KEY_HOTSEAT_SIZE = Favorites.SCREEN;
private static final String KEY_GRID_X_SIZE = Favorites.SPANX;
private static final String KEY_GRID_Y_SIZE = Favorites.SPANY;
private static final String KEY_DB_VERSION = Favorites.RANK;
private final Context mContext;
private final SQLiteDatabase mDb;
private final int mOldHotseatSize;
private final int mOldGridX;
private final int mOldGridY;
private int mRestoredHotseatSize;
private int mRestoredGridX;
private int mRestoredGridY;
public GridBackupTable(Context context, SQLiteDatabase db,
int hotseatSize, int gridX, int gridY) {
mContext = context;
mDb = db;
mOldHotseatSize = hotseatSize;
mOldGridX = gridX;
mOldGridY = gridY;
}
public boolean backupOrRestoreAsNeeded() {
// Check if backup table exists
if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
if (Settings.call(mContext.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED)
.getBoolean(Settings.EXTRA_VALUE, false)) {
// No need to copy if empty DB was created.
return false;
}
copyTable(Favorites.TABLE_NAME, BACKUP_TABLE_NAME);
encodeDBProperties();
return false;
}
if (!loadDbProperties()) {
return false;
}
copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME);
Log.d(TAG, "Backup table found");
return true;
}
public int getRestoreHotseatAndGridSize(Point outGridSize) {
outGridSize.set(mRestoredGridX, mRestoredGridY);
return mRestoredHotseatSize;
}
private void copyTable(String from, String to) {
long userSerial = UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
Process.myUserHandle());
dropTable(mDb, to);
Favorites.addTableToDb(mDb, userSerial, false, to);
mDb.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY);
}
private void encodeDBProperties() {
ContentValues values = new ContentValues();
values.put(Favorites._ID, ID_PROPERTY);
values.put(KEY_DB_VERSION, mDb.getVersion());
values.put(KEY_GRID_X_SIZE, mOldGridX);
values.put(KEY_GRID_Y_SIZE, mOldGridY);
values.put(KEY_HOTSEAT_SIZE, mOldHotseatSize);
mDb.insert(BACKUP_TABLE_NAME, null, values);
}
private boolean loadDbProperties() {
try (Cursor c = mDb.query(BACKUP_TABLE_NAME, new String[] {
KEY_DB_VERSION, // 0
KEY_GRID_X_SIZE, // 1
KEY_GRID_Y_SIZE, // 2
KEY_HOTSEAT_SIZE}, // 3
"_id=" + ID_PROPERTY, null, null, null, null)) {
if (!c.moveToNext()) {
Log.e(TAG, "Meta data not found in backup table");
return false;
}
if (mDb.getVersion() != c.getInt(0)) {
return false;
}
mRestoredGridX = c.getInt(1);
mRestoredGridY = c.getInt(2);
mRestoredHotseatSize = c.getInt(3);
return true;
}
}
}

View File

@ -1,10 +1,10 @@
package com.android.launcher3.model;
import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
import static com.android.launcher3.Utilities.getPointString;
import static com.android.launcher3.Utilities.parsePoint;
import android.content.ComponentName;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@ -12,24 +12,27 @@ import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point;
import android.util.Log;
import android.util.SparseArray;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.PackageInstallerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.provider.LauncherDbUtils;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
import java.util.ArrayList;
@ -60,13 +63,13 @@ public class GridSizeMigrationTask {
private static final float WT_WIDGET_FACTOR = 0.6f;
private static final float WT_FOLDER_FACTOR = 0.5f;
private final Context mContext;
private final InvariantDeviceProfile mIdp;
protected final SQLiteDatabase mDb;
protected final Context mContext;
private final ContentValues mTempValues = new ContentValues();
protected final IntArray mEntryToRemove = new IntArray();
private final ArrayList<ContentProviderOperation> mUpdateOperations = new ArrayList<>();
protected final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>();
private final HashSet<String> mValidPackages;
private final int mSrcX, mSrcY;
@ -76,11 +79,11 @@ public class GridSizeMigrationTask {
private final int mSrcHotseatSize;
private final int mDestHotseatSize;
protected GridSizeMigrationTask(Context context, InvariantDeviceProfile idp,
protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
HashSet<String> validPackages, Point sourceSize, Point targetSize) {
mContext = context;
mDb = db;
mValidPackages = validPackages;
mIdp = idp;
mSrcX = sourceSize.x;
mSrcY = sourceSize.y;
@ -95,11 +98,10 @@ public class GridSizeMigrationTask {
mSrcHotseatSize = mDestHotseatSize = -1;
}
protected GridSizeMigrationTask(Context context,
InvariantDeviceProfile idp, HashSet<String> validPackages,
int srcHotseatSize, int destHotseatSize) {
protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
HashSet<String> validPackages, int srcHotseatSize, int destHotseatSize) {
mContext = context;
mIdp = idp;
mDb = db;
mValidPackages = validPackages;
mSrcHotseatSize = srcHotseatSize;
@ -118,20 +120,21 @@ public class GridSizeMigrationTask {
*/
private boolean applyOperations() throws Exception {
// Update items
if (!mUpdateOperations.isEmpty()) {
mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations);
int updateCount = mUpdateOperations.size();
for (int i = 0; i < updateCount; i++) {
mDb.update(Favorites.TABLE_NAME, mUpdateOperations.valueAt(i),
"_id=" + mUpdateOperations.keyAt(i), null);
}
if (!mEntryToRemove.isEmpty()) {
if (DEBUG) {
Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
}
mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
Utilities.createDbSelectionQuery(
LauncherSettings.Favorites._ID, mEntryToRemove), null);
mDb.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
Favorites._ID, mEntryToRemove), null);
}
return !mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty();
return updateCount > 0 || !mEntryToRemove.isEmpty();
}
/**
@ -181,24 +184,17 @@ public class GridSizeMigrationTask {
}
@VisibleForTesting
static IntArray getWorkspaceScreenIds(Context context) {
IntSet set = new IntSet();
try (Cursor c = context.getContentResolver().query(Favorites.CONTENT_URI,
new String[] {Favorites.SCREEN},
static IntArray getWorkspaceScreenIds(SQLiteDatabase db) {
return LauncherDbUtils.queryIntArray(db, Favorites.TABLE_NAME, Favorites.SCREEN,
Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP,
null, Favorites.SCREEN)) {
while (c.moveToNext()) {
set.add(c.getInt(0));
}
}
return set.getArray();
Favorites.SCREEN, Favorites.SCREEN);
}
/**
* @return true if any DB change was made
*/
protected boolean migrateWorkspace() throws Exception {
IntArray allScreens = getWorkspaceScreenIds(mContext);
IntArray allScreens = getWorkspaceScreenIds(mDb);
if (allScreens.isEmpty()) {
throw new Exception("Unable to get workspace screens");
}
@ -230,8 +226,7 @@ public class GridSizeMigrationTask {
int newScreenId = LauncherSettings.Settings.call(
mContext.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
.getInt(EXTRA_VALUE);
for (DbEntry item : placement.finalPlacedItems) {
if (!mCarryOver.remove(itemMap.get(item.id))) {
throw new Exception("Unable to find matching items");
@ -362,11 +357,9 @@ public class GridSizeMigrationTask {
* Updates an item in the DB.
*/
protected void update(DbEntry item) {
mTempValues.clear();
item.addToContentValues(mTempValues);
mUpdateOperations.add(ContentProviderOperation
.newUpdate(LauncherSettings.Favorites.getContentUri(item.id))
.withValues(mTempValues).build());
ContentValues values = new ContentValues();
item.addToContentValues(values);
mUpdateOperations.put(item.id, values);
}
/**
@ -612,13 +605,13 @@ public class GridSizeMigrationTask {
}
private ArrayList<DbEntry> loadHotseatEntries() {
Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
Cursor c = queryWorkspace(
new String[]{
Favorites._ID, // 0
Favorites.ITEM_TYPE, // 1
Favorites.INTENT, // 2
Favorites.SCREEN}, // 3
Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null);
Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT);
final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
@ -796,8 +789,7 @@ public class GridSizeMigrationTask {
}
protected Cursor queryWorkspace(String[] columns, String where) {
return mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
columns, where, null, null, null);
return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
}
/**
@ -864,11 +856,11 @@ public class GridSizeMigrationTask {
}
public void addToContentValues(ContentValues values) {
values.put(LauncherSettings.Favorites.SCREEN, screenId);
values.put(LauncherSettings.Favorites.CELLX, cellX);
values.put(LauncherSettings.Favorites.CELLY, cellY);
values.put(LauncherSettings.Favorites.SPANX, spanX);
values.put(LauncherSettings.Favorites.SPANY, spanY);
values.put(Favorites.SCREEN, screenId);
values.put(Favorites.CELLX, cellX);
values.put(Favorites.CELLY, cellY);
values.put(Favorites.SPANX, spanX);
values.put(Favorites.SPANY, spanY);
}
}
@ -907,34 +899,43 @@ public class GridSizeMigrationTask {
}
long migrationStartTime = System.currentTimeMillis();
try {
try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
.getBinder(Settings.EXTRA_VALUE)) {
int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
idp.numHotseatIcons);
Point sourceSize = parsePoint(prefs.getString(
KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
boolean dbChanged = false;
GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
srcHotseatCount, sourceSize.x, sourceSize.y);
if (backupTable.backupOrRestoreAsNeeded()) {
dbChanged = true;
srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
}
HashSet<String> validPackages = getValidPackages(context);
// Hotseat
int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
idp.numHotseatIcons);
if (srcHotseatCount != idp.numHotseatIcons) {
// Migrate hotseat.
dbChanged = new GridSizeMigrationTask(context, LauncherAppState.getIDP(context),
dbChanged = new GridSizeMigrationTask(context, transaction.getDb(),
validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
}
// Grid size
Point targetSize = new Point(idp.numColumns, idp.numRows);
Point sourceSize = parsePoint(prefs.getString(
KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize,
targetSize)) {
if (new MultiStepMigrationTask(validPackages, context, transaction.getDb())
.migrate(sourceSize, targetSize)) {
dbChanged = true;
}
if (dbChanged) {
// Make sure we haven't removed everything.
final Cursor c = context.getContentResolver().query(
LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
Favorites.CONTENT_URI, null, null, null, null);
boolean hasData = c.moveToNext();
c.close();
if (!hasData) {
@ -942,6 +943,8 @@ public class GridSizeMigrationTask {
}
}
transaction.commit();
Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
return true;
} catch (Exception e) {
Log.e(TAG, "Error during grid migration", e);
@ -982,8 +985,11 @@ public class GridSizeMigrationTask {
*/
public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
throws Exception {
try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
.getBinder(Settings.EXTRA_VALUE)) {
GridSizeMigrationTask task = new GridSizeMigrationTask(
context, LauncherAppState.getIDP(context), getValidPackages(context),
context, transaction.getDb(), getValidPackages(context),
Integer.MAX_VALUE, Integer.MAX_VALUE);
// Load all the valid entries
@ -994,8 +1000,10 @@ public class GridSizeMigrationTask {
for (DbEntry item : items) {
positions.put(item.screenId, item);
}
transaction.commit();
return positions;
}
}
/**
* Task to run grid migration in multiple steps when the size difference is more than 1.
@ -1003,10 +1011,13 @@ public class GridSizeMigrationTask {
protected static class MultiStepMigrationTask {
private final HashSet<String> mValidPackages;
private final Context mContext;
private final SQLiteDatabase mDb;
public MultiStepMigrationTask(HashSet<String> validPackages, Context context) {
public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
SQLiteDatabase db) {
mValidPackages = validPackages;
mContext = context;
mDb = db;
}
public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
@ -1042,7 +1053,7 @@ public class GridSizeMigrationTask {
}
protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
return new GridSizeMigrationTask(mContext, LauncherAppState.getIDP(mContext),
return new GridSizeMigrationTask(mContext, mDb,
mValidPackages, sourceSize, nextSize).migrateWorkspace();
}
}

View File

@ -21,6 +21,7 @@ import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.Binder;
import android.util.Log;
import com.android.launcher3.LauncherAppState;
@ -103,10 +104,22 @@ public class LauncherDbUtils {
return out;
}
public static boolean tableExists(SQLiteDatabase db, String tableName) {
try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
"tbl_name = ?", new String[] {tableName},
null, null, null, null, null)) {
return c.getCount() > 0;
}
}
public static void dropTable(SQLiteDatabase db, String tableName) {
db.execSQL("DROP TABLE IF EXISTS " + tableName);
}
/**
* Utility class to simplify managing sqlite transactions
*/
public static class SQLiteTransaction implements AutoCloseable {
public static class SQLiteTransaction extends Binder implements AutoCloseable {
private final SQLiteDatabase mDb;
public SQLiteTransaction(SQLiteDatabase db) {
@ -122,5 +135,9 @@ public class LauncherDbUtils {
public void close() {
mDb.endTransaction();
}
public SQLiteDatabase getDb() {
return mDb;
}
}
}

View File

@ -37,28 +37,20 @@ import java.util.ArrayList;
*/
public class LossyScreenMigrationTask extends GridSizeMigrationTask {
private final SQLiteDatabase mDb;
private final IntSparseArrayMap<DbEntry> mOriginalItems;
private final IntSparseArrayMap<DbEntry> mUpdates;
protected LossyScreenMigrationTask(
Context context, InvariantDeviceProfile idp, SQLiteDatabase db) {
// Decrease the rows count by 1
super(context, idp, getValidPackages(context),
super(context, db, getValidPackages(context),
new Point(idp.numColumns, idp.numRows + 1),
new Point(idp.numColumns, idp.numRows));
mDb = db;
mOriginalItems = new IntSparseArrayMap<>();
mUpdates = new IntSparseArrayMap<>();
}
@Override
protected Cursor queryWorkspace(String[] columns, String where) {
return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
}
@Override
protected void update(DbEntry item) {
mUpdates.put(item.id, item.copy());

View File

@ -16,6 +16,8 @@
package com.android.launcher3.provider;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@ -111,7 +113,7 @@ public class RestoreDbTask {
db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;");
Favorites.addTableToDb(db, newProfileId, false);
db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;");
db.execSQL("DROP TABLE favorites_old;");
dropTable(db, "favorites_old");
}
/**