summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml5
-rw-r--r--res/xml/backupscheme.xml7
-rw-r--r--src/com/android/launcher3/LauncherBackupAgent.java133
-rw-r--r--src/com/android/launcher3/LauncherProvider.java29
-rw-r--r--src/com/android/launcher3/LauncherSettings.java28
-rw-r--r--tests/src/com/android/launcher3/LauncherBackupAgentTest.java74
6 files changed, 248 insertions, 28 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e7b703cab..9192764b5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,8 +53,9 @@
<uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
<application
- android:allowBackup="@bool/enable_backup"
- android:backupAgent="com.android.launcher3.LauncherBackupAgentHelper"
+ android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+ android:fullBackupOnly="true"
+ android:fullBackupContent="@xml/backupscheme"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher_home"
android:label="@string/app_name"
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
new file mode 100644
index 000000000..7e833a0ca
--- /dev/null
+++ b/res/xml/backupscheme.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <include domain="database" path="launcher.db" />
+ <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
+
+</full-backup-content> \ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
new file mode 100644
index 000000000..b2f5c5717
--- /dev/null
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -0,0 +1,133 @@
+package com.android.launcher3;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.ParcelFileDescriptor;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.logging.FileLog;
+
+import java.io.InvalidObjectException;
+
+public class LauncherBackupAgent extends BackupAgent {
+
+ private static final String TAG = "LauncherBackupAgent";
+
+ private static final String INFO_COLUMN_NAME = "name";
+ private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
+
+ @Override
+ public void onRestore(
+ BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
+ // Doesn't do incremental backup/restore
+ }
+
+ @Override
+ public void onBackup(
+ ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
+ // Doesn't do incremental backup/restore
+ }
+
+ @Override
+ public void onRestoreFinished() {
+ DatabaseHelper helper = new DatabaseHelper(this, null, LauncherFiles.LAUNCHER_DB);
+
+ if (!sanitizeDBSafely(helper)) {
+ helper.createEmptyDB(helper.getWritableDatabase());
+ }
+
+ try {
+ // Flush all logs before the process is killed.
+ FileLog.flushAll(null);
+ } catch (Exception e) { }
+ }
+
+ private boolean sanitizeDBSafely(DatabaseHelper helper) {
+ SQLiteDatabase db = helper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ sanitizeDB(helper, db);
+ db.setTransactionSuccessful();
+ return true;
+ } catch (Exception e) {
+ FileLog.e(TAG, "Failed to verify db", e);
+ return false;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Makes the following changes in the provider DB.
+ * 1. Removes all entries belonging to a managed profile as managed profiles
+ * cannot be restored.
+ * 2. Marks all entries as restored. The flags are updated during first load or as
+ * the restored apps get installed.
+ * 3. If the user serial for primary profile is different than that of the previous device,
+ * update the entries to the new profile id.
+ */
+ private void sanitizeDB(DatabaseHelper helper, SQLiteDatabase db) throws Exception {
+ long oldProfileId = getDefaultProfileId(db);
+ // Delete all entries which do not belong to the main user
+ int itemsDeleted = db.delete(
+ Favorites.TABLE_NAME, "profileId != ?", new String[]{Long.toString(oldProfileId)});
+ if (itemsDeleted > 0) {
+ FileLog.d(TAG, itemsDeleted + " items belonging to a managed profile, were deleted");
+ }
+
+ // Mark all items as restored.
+ ContentValues values = new ContentValues();
+ values.put(Favorites.RESTORED, 1);
+ db.update(Favorites.TABLE_NAME, values, null, null);
+
+ // Mark widgets with appropriate restore flag
+ values.put(Favorites.RESTORED,
+ LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
+ LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
+ LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
+ db.update(Favorites.TABLE_NAME, values, "itemType = ?",
+ new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
+
+ long myProfileId = helper.getDefaultUserSerial();
+ if (Utilities.longCompare(oldProfileId, myProfileId) != 0) {
+ FileLog.d(TAG, "Changing primary user id from " + oldProfileId + " to " + myProfileId);
+ migrateProfileId(db, myProfileId);
+ }
+ }
+
+ /**
+ * Updates profile id of all entries and changes the default value for the column.
+ */
+ protected void migrateProfileId(SQLiteDatabase db, long newProfileId) {
+ // Update existing entries.
+ ContentValues values = new ContentValues();
+ values.put(Favorites.PROFILE_ID, newProfileId);
+ db.update(Favorites.TABLE_NAME, values, null, null);
+
+ // Change default value of the column.
+ 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;");
+ }
+
+ /**
+ * Returns the profile id for used in the favorites table of the provided db.
+ */
+ protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
+ try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+ int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
+ while (c.moveToNext()) {
+ if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
+ return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE));
+ }
+ }
+ throw new InvalidObjectException("Table does not have a profile id column");
+ }
+ }
+}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 8d74e6086..f10099e0c 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -564,7 +564,7 @@ public class LauncherProvider extends ContentProvider {
/**
* The class is subclassed in tests to create an in-memory db.
*/
- protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
+ public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
private final Handler mWidgetHostResetHandler;
private final Context mContext;
private long mMaxItemId = -1;
@@ -586,7 +586,7 @@ public class LauncherProvider extends ContentProvider {
}
/**
- * Constructor used only in tests.
+ * Constructor used in tests and for restore.
*/
public DatabaseHelper(
Context context, Handler widgetHostResetHandler, String tableName) {
@@ -658,30 +658,7 @@ public class LauncherProvider extends ContentProvider {
}
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
- String ifNotExists = optional ? " IF NOT EXISTS " : "";
- db.execSQL("CREATE TABLE " + ifNotExists + Favorites.TABLE_NAME + " (" +
- "_id INTEGER PRIMARY KEY," +
- "title TEXT," +
- "intent TEXT," +
- "container INTEGER," +
- "screen INTEGER," +
- "cellX INTEGER," +
- "cellY INTEGER," +
- "spanX INTEGER," +
- "spanY INTEGER," +
- "itemType INTEGER," +
- "appWidgetId INTEGER NOT NULL DEFAULT -1," +
- "iconType INTEGER," +
- "iconPackage TEXT," +
- "iconResource TEXT," +
- "icon BLOB," +
- "appWidgetProvider TEXT," +
- "modified INTEGER NOT NULL DEFAULT 0," +
- "restored INTEGER NOT NULL DEFAULT 0," +
- "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," +
- "rank INTEGER NOT NULL DEFAULT 0," +
- "options INTEGER NOT NULL DEFAULT 0" +
- ");");
+ Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
}
private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 0e559a544..13e45476a 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -17,6 +17,7 @@
package com.android.launcher3;
import android.content.ContentResolver;
+import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
@@ -256,6 +257,33 @@ public class LauncherSettings {
* <p>Type: INTEGER</p>
*/
public static final String OPTIONS = "options";
+
+ public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
+ String ifNotExists = optional ? " IF NOT EXISTS " : "";
+ db.execSQL("CREATE TABLE " + ifNotExists + TABLE_NAME + " (" +
+ "_id INTEGER PRIMARY KEY," +
+ "title TEXT," +
+ "intent TEXT," +
+ "container INTEGER," +
+ "screen INTEGER," +
+ "cellX INTEGER," +
+ "cellY INTEGER," +
+ "spanX INTEGER," +
+ "spanY INTEGER," +
+ "itemType INTEGER," +
+ "appWidgetId INTEGER NOT NULL DEFAULT -1," +
+ "iconType INTEGER," +
+ "iconPackage TEXT," +
+ "iconResource TEXT," +
+ "icon BLOB," +
+ "appWidgetProvider TEXT," +
+ "modified INTEGER NOT NULL DEFAULT 0," +
+ "restored INTEGER NOT NULL DEFAULT 0," +
+ "profileId INTEGER DEFAULT " + myProfileId + "," +
+ "rank INTEGER NOT NULL DEFAULT 0," +
+ "options INTEGER NOT NULL DEFAULT 0" +
+ ");");
+ }
}
/**
diff --git a/tests/src/com/android/launcher3/LauncherBackupAgentTest.java b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
new file mode 100644
index 000000000..798f0c25d
--- /dev/null
+++ b/tests/src/com/android/launcher3/LauncherBackupAgentTest.java
@@ -0,0 +1,74 @@
+package com.android.launcher3;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.launcher3.LauncherProvider.DatabaseHelper;
+import com.android.launcher3.LauncherSettings.Favorites;
+
+/**
+ * Tests for {@link LauncherBackupAgent}
+ */
+@MediumTest
+public class LauncherBackupAgentTest extends AndroidTestCase {
+
+ public void testGetProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
+ assertEquals(23, new LauncherBackupAgent().getDefaultProfileId(db));
+ }
+
+ public void testMigrateProfileId() throws Exception {
+ SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+ // Add some dummy data
+ for (int i = 0; i < 5; i++) {
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, i);
+ values.put(Favorites.TITLE, "item " + i);
+ db.insert(Favorites.TABLE_NAME, null, values);
+ }
+ // Verify item add
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+
+ new LauncherBackupAgent().migrateProfileId(db, 33);
+
+ // verify data migrated
+ assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+
+ // Verify default value changed
+ ContentValues values = new ContentValues();
+ values.put(Favorites._ID, 100);
+ values.put(Favorites.TITLE, "item 100");
+ db.insert(Favorites.TABLE_NAME, null, values);
+ assertEquals(6, getCount(db, "select * from favorites where profileId = 33"));
+ }
+
+ private int getCount(SQLiteDatabase db, String sql) {
+ Cursor c = db.rawQuery(sql, null);
+ try {
+ return c.getCount();
+ } finally {
+ c.getCount();
+ }
+ }
+
+ private class MyDatabaseHelper extends DatabaseHelper {
+
+ private final long mProfileId;
+
+ public MyDatabaseHelper(long profileId) {
+ super(getContext(), null, null, null);
+ mProfileId = profileId;
+ }
+
+ @Override
+ public long getDefaultUserSerial() {
+ return mProfileId;
+ }
+
+ protected void onEmptyDbCreated() { }
+ }
+}