summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--protos/backup.proto2
-rw-r--r--src/com/android/launcher3/LauncherBackupAgent.java439
2 files changed, 298 insertions, 143 deletions
diff --git a/protos/backup.proto b/protos/backup.proto
index 3780bc5..f43f338 100644
--- a/protos/backup.proto
+++ b/protos/backup.proto
@@ -23,7 +23,7 @@ message Key {
enum Type {
FAVORITE = 1;
SCREEN = 2;
- IMAGE = 3;
+ ICON = 3;
}
required Type type = 1;
optional string name = 2; // keep this short
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index bb15ca1..cbef36b 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -19,7 +19,6 @@ package com.android.launcher3;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import com.google.protobuf.nano.MessageNano;
-import com.android.launcher3.LauncherSettings.ChangeLogColumns;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.WorkspaceScreens;
import com.android.launcher3.backup.BackupProtos;
@@ -27,88 +26,110 @@ import com.android.launcher3.backup.BackupProtos.CheckedMessage;
import com.android.launcher3.backup.BackupProtos.Favorite;
import com.android.launcher3.backup.BackupProtos.Journal;
import com.android.launcher3.backup.BackupProtos.Key;
+import com.android.launcher3.backup.BackupProtos.Resource;
import com.android.launcher3.backup.BackupProtos.Screen;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupManager;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.os.ParcelFileDescriptor;
-import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
+import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.zip.CRC32;
+import static android.graphics.Bitmap.CompressFormat.WEBP;
+
/**
* Persist the launcher home state across calamities.
*/
public class LauncherBackupAgent extends BackupAgent {
private static final String TAG = "LauncherBackupAgent";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final int MAX_JOURNAL_SIZE = 1000000;
+ private static final int MAX_ICONS_PER_PASS = 10;
+
private static BackupManager sBackupManager;
private static final String[] FAVORITE_PROJECTION = {
Favorites._ID, // 0
- Favorites.APPWIDGET_ID, // 1
- Favorites.APPWIDGET_PROVIDER, // 2
- Favorites.CELLX, // 3
- Favorites.CELLY, // 4
- Favorites.CONTAINER, // 5
- Favorites.ICON, // 6
- Favorites.ICON_PACKAGE, // 7
- Favorites.ICON_RESOURCE, // 8
- Favorites.ICON_TYPE, // 9
- Favorites.ITEM_TYPE, // 10
- Favorites.INTENT, // 11
- Favorites.SCREEN, // 12
- Favorites.SPANX, // 13
- Favorites.SPANY, // 14
- Favorites.TITLE, // 15
+ Favorites.MODIFIED, // 1
+ Favorites.INTENT, // 2
+ Favorites.APPWIDGET_PROVIDER, // 3
+ Favorites.APPWIDGET_ID, // 4
+ Favorites.CELLX, // 5
+ Favorites.CELLY, // 6
+ Favorites.CONTAINER, // 7
+ Favorites.ICON, // 8
+ Favorites.ICON_PACKAGE, // 9
+ Favorites.ICON_RESOURCE, // 10
+ Favorites.ICON_TYPE, // 11
+ Favorites.ITEM_TYPE, // 12
+ Favorites.SCREEN, // 13
+ Favorites.SPANX, // 14
+ Favorites.SPANY, // 15
+ Favorites.TITLE, // 16
};
private static final int ID_INDEX = 0;
- private static final int APPWIDGET_ID_INDEX = 1;
- private static final int APPWIDGET_PROVIDER_INDEX = 2;
- private static final int CELLX_INDEX = 3;
- private static final int CELLY_INDEX = 4;
- private static final int CONTAINER_INDEX = 5;
- private static final int ICON_INDEX = 6;
- private static final int ICON_PACKAGE_INDEX = 7;
- private static final int ICON_RESOURCE_INDEX = 8;
- private static final int ICON_TYPE_INDEX = 9;
- private static final int ITEM_TYPE_INDEX = 10;
- private static final int INTENT_INDEX = 11;
- private static final int SCREEN_INDEX = 12;
- private static final int SPANX_INDEX = 13 ;
- private static final int SPANY_INDEX = 14;
- private static final int TITLE_INDEX = 15;
+ private static final int ID_MODIFIED = 1;
+ private static final int INTENT_INDEX = 2;
+ private static final int APPWIDGET_PROVIDER_INDEX = 3;
+ private static final int APPWIDGET_ID_INDEX = 4;
+ private static final int CELLX_INDEX = 5;
+ private static final int CELLY_INDEX = 6;
+ private static final int CONTAINER_INDEX = 7;
+ private static final int ICON_INDEX = 8;
+ private static final int ICON_PACKAGE_INDEX = 9;
+ private static final int ICON_RESOURCE_INDEX = 10;
+ private static final int ICON_TYPE_INDEX = 11;
+ private static final int ITEM_TYPE_INDEX = 12;
+ private static final int SCREEN_INDEX = 13;
+ private static final int SPANX_INDEX = 14;
+ private static final int SPANY_INDEX = 15;
+ private static final int TITLE_INDEX = 16;
private static final String[] SCREEN_PROJECTION = {
WorkspaceScreens._ID, // 0
- WorkspaceScreens.SCREEN_RANK // 1
+ WorkspaceScreens.MODIFIED, // 1
+ WorkspaceScreens.SCREEN_RANK // 2
};
- private static final int SCREEN_RANK_INDEX = 1;
+ private static final int SCREEN_RANK_INDEX = 2;
- private static final String[] ID_ONLY_PROJECTION = {
- BaseColumns._ID
+
+ private static final String[] ICON_PROJECTION = {
+ Favorites._ID, // 0
+ Favorites.MODIFIED, // 1
+ Favorites.INTENT // 2
};
+ private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap;
+
/**
* Notify the backup manager that out database is dirty.
@@ -155,12 +176,11 @@ public class LauncherBackupAgent extends BackupAgent {
ArrayList<Key> keys = new ArrayList<Key>();
backupFavorites(in, data, out, keys);
backupScreens(in, data, out, keys);
+ backupIcons(in, data, out, keys);
out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
writeJournal(newState, out);
Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
-
- Log.v(TAG, "onBackup: finished");
}
/**
@@ -205,6 +225,10 @@ public class LauncherBackupAgent extends BackupAgent {
restoreScreen(key, buffer, dataSize, keys);
break;
+ case Key.ICON:
+ restoreIcon(key, buffer, dataSize, keys);
+ break;
+
default:
Log.w(TAG, "unknown restore entity type: " + key.type);
break;
@@ -236,70 +260,35 @@ public class LauncherBackupAgent extends BackupAgent {
ArrayList<Key> keys)
throws IOException {
// read the old ID set
- Set<String> savedIds = new HashSet<String>();
- for(int i = 0; i < in.key.length; i++) {
- Key key = in.key[i];
- if (key.type == Key.FAVORITE) {
- savedIds.add(keyToBackupKey(key));
- }
- }
+ Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in);
if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
// persist things that have changed since the last backup
ContentResolver cr = getContentResolver();
- String where = ChangeLogColumns.MODIFIED + " > ?";
- String[] args = {Long.toString(in.t)};
- String updateOrder = ChangeLogColumns.MODIFIED;
- Cursor updated = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
- where, args, updateOrder);
- if (DEBUG) Log.d(TAG, "favorite updated.getCount()=" + updated.getCount());
+ Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
+ null, null, null);
+ Set<String> currentIds = new HashSet<String>(cursor.getCount());
try {
- updated.moveToPosition(-1);
- while(updated.moveToNext()) {
- final long id = updated.getLong(ID_INDEX);
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ final long id = cursor.getLong(ID_INDEX);
+ final long updateTime = cursor.getLong(ID_MODIFIED);
Key key = getKey(Key.FAVORITE, id);
- byte[] blob = packFavorite(updated);
- String backupKey = keyToBackupKey(key);
- data.writeEntityHeader(backupKey, blob.length);
- data.writeEntityData(blob, blob.length);
- out.rows++;
- out.bytes += blob.length;
- Log.v(TAG, "saving favorite " + backupKey + ": " + id + "/" + blob.length);
- if(DEBUG) Log.d(TAG, "wrote " +
- Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
- // remember that is was a new column, so we don't delete it.
- savedIds.add(backupKey);
- }
- } finally {
- updated.close();
- }
- if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
-
- // build the current ID set
- String idOrder = BaseColumns._ID;
- Cursor idCursor = cr.query(Favorites.CONTENT_URI, ID_ONLY_PROJECTION,
- null, null, idOrder);
- Set<String> currentIds = new HashSet<String>(idCursor.getCount());
- try {
- idCursor.moveToPosition(-1);
- while(idCursor.moveToNext()) {
- Key key = getKey(Key.FAVORITE, idCursor.getLong(ID_INDEX));
- currentIds.add(keyToBackupKey(key));
- // save the IDs for next time
keys.add(key);
+ currentIds.add(keyToBackupKey(key));
+ if (updateTime > in.t) {
+ byte[] blob = packFavorite(cursor);
+ writeRowToBackup(key, blob, out, data);
+ }
}
} finally {
- idCursor.close();
+ cursor.close();
}
if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
// these IDs must have been deleted
savedIds.removeAll(currentIds);
- for (String deleted : savedIds) {
- Log.v(TAG, "dropping favorite " + deleted);
- data.writeEntityHeader(deleted, -1);
- out.rows++;
- }
+ out.rows += removeDeletedKeysFromBackup(savedIds, data);
}
/**
@@ -332,76 +321,42 @@ public class LauncherBackupAgent extends BackupAgent {
* @param in notes from last backup
* @param data output stream for key/value pairs
* @param out notes about this backup
- * @param keys keys to mark as clean in the notes for next backup @throws IOException
+ * @param keys keys to mark as clean in the notes for next backup
+ * @throws IOException
*/
private void backupScreens(Journal in, BackupDataOutput data, Journal out,
ArrayList<Key> keys)
throws IOException {
// read the old ID set
- Set<String> savedIds = new HashSet<String>();
- for(int i = 0; i < in.key.length; i++) {
- Key key = in.key[i];
- if (key.type == Key.SCREEN) {
- savedIds.add(keyToBackupKey(key));
- }
- }
- if (DEBUG) Log.d(TAG, "screens savedIds.size()=" + savedIds.size());
+ Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in);
+ if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
// persist things that have changed since the last backup
ContentResolver cr = getContentResolver();
- String where = ChangeLogColumns.MODIFIED + " > ?";
- String[] args = {Long.toString(in.t)};
- String updateOrder = ChangeLogColumns.MODIFIED;
- Cursor updated = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
- where, args, updateOrder);
- updated.moveToPosition(-1);
- if (DEBUG) Log.d(TAG, "screens updated.getCount()=" + updated.getCount());
+ Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
+ null, null, null);
+ Set<String> currentIds = new HashSet<String>(cursor.getCount());
try {
- while(updated.moveToNext()) {
- final long id = updated.getLong(ID_INDEX);
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ final long id = cursor.getLong(ID_INDEX);
+ final long updateTime = cursor.getLong(ID_MODIFIED);
Key key = getKey(Key.SCREEN, id);
- byte[] blob = packScreen(updated);
- String backupKey = keyToBackupKey(key);
- data.writeEntityHeader(backupKey, blob.length);
- data.writeEntityData(blob, blob.length);
- out.rows++;
- out.bytes += blob.length;
- Log.v(TAG, "saving screen " + backupKey + ": " + id + "/" + blob.length);
- if(DEBUG) Log.d(TAG, "wrote " +
- Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
- // remember that is was a new column, so we don't delete it.
- savedIds.add(backupKey);
- }
- } finally {
- updated.close();
- }
- if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
-
- // build the current ID set
- String idOrder = BaseColumns._ID;
- Cursor idCursor = cr.query(WorkspaceScreens.CONTENT_URI, ID_ONLY_PROJECTION,
- null, null, idOrder);
- idCursor.moveToPosition(-1);
- Set<String> currentIds = new HashSet<String>(idCursor.getCount());
- try {
- while(idCursor.moveToNext()) {
- Key key = getKey(Key.SCREEN, idCursor.getLong(ID_INDEX));
- currentIds.add(keyToBackupKey(key));
- // save the IDs for next time
keys.add(key);
+ currentIds.add(keyToBackupKey(key));
+ if (updateTime > in.t) {
+ byte[] blob = packScreen(cursor);
+ writeRowToBackup(key, blob, out, data);
+ }
}
} finally {
- idCursor.close();
+ cursor.close();
}
if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
// these IDs must have been deleted
savedIds.removeAll(currentIds);
- for(String deleted: savedIds) {
- Log.v(TAG, "dropping screen " + deleted);
- data.writeEntityHeader(deleted, -1);
- out.rows++;
- }
+ out.rows += removeDeletedKeysFromBackup(savedIds, data);
}
/**
@@ -426,7 +381,118 @@ public class LauncherBackupAgent extends BackupAgent {
}
}
- /** create a new key object.
+ /**
+ * Write all the static icon resources we need to render placeholders
+ * for a package that is not installed.
+ *
+ * @param in notes from last backup
+ * @param data output stream for key/value pairs
+ * @param out notes about this backup
+ * @param keys keys to mark as clean in the notes for next backup
+ * @throws IOException
+ */
+ private void backupIcons(Journal in, BackupDataOutput data, Journal out,
+ ArrayList<Key> keys) throws IOException {
+ // persist icons for new shortcuts since the last backup
+ final ContentResolver cr = getContentResolver();
+ final IconCache iconCache = new IconCache(this);
+ final int dpi = getResources().getDisplayMetrics().densityDpi;
+
+ // read the old ID set
+ Set<String> savedIds = getSavedIdsByType(Key.ICON, in);
+ if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size());
+
+ int startRows = out.rows;
+ if (DEBUG) Log.d(TAG, "starting here: " + startRows);
+ String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION;
+ Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
+ where, null, null);
+ Set<String> currentIds = new HashSet<String>(cursor.getCount());
+ try {
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ final long id = cursor.getLong(ID_INDEX);
+ final String intentDescription = cursor.getString(INTENT_INDEX);
+ try {
+ Intent intent = Intent.parseUri(intentDescription, 0);
+ ComponentName cn = intent.getComponent();
+ Key key = null;
+ String backupKey = null;
+ if (cn != null) {
+ key = getKey(Key.ICON, cn.flattenToShortString());
+ backupKey = keyToBackupKey(key);
+ currentIds.add(backupKey);
+ } else {
+ Log.w(TAG, "empty intent on application favorite: " + id);
+ }
+ if (savedIds.contains(backupKey)) {
+ if (DEBUG) Log.d(TAG, "already saved icon " + backupKey);
+
+ // remember that we already backed this up previously
+ keys.add(key);
+ } else if (backupKey != null) {
+ if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
+ if ((out.rows - startRows) < MAX_ICONS_PER_PASS) {
+ if (DEBUG) Log.d(TAG, "saving icon " + backupKey);
+ Bitmap icon = iconCache.getIcon(intent);
+ keys.add(key);
+ if (icon != null && !iconCache.isDefaultIcon(icon)) {
+ byte[] blob = packIcon(dpi, icon);
+ writeRowToBackup(key, blob, out, data);
+ }
+ } else {
+ if (DEBUG) Log.d(TAG, "scheduling another rtun for icon " + backupKey);
+ // too many icons for this pass, request another.
+ dataChanged(this);
+ }
+ }
+ } catch (URISyntaxException e) {
+ Log.w(TAG, "invalid URI on application favorite: " + id);
+ } catch (IOException e) {
+ Log.w(TAG, "unable to save application icon for favorite: " + id);
+ }
+
+ }
+ } finally {
+ cursor.close();
+ }
+ if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size());
+
+ // these IDs must have been deleted
+ savedIds.removeAll(currentIds);
+ out.rows += removeDeletedKeysFromBackup(savedIds, data);
+ }
+
+ /**
+ * Read an icon from the stream.
+ *
+ * <P>Keys arrive in any order, so shortcuts that use this screen may already exist.
+ *
+ * @param key identifier for the row
+ * @param buffer the serialized proto from the stream, may be larger than dataSize
+ * @param dataSize the size of the proto from the stream
+ * @param keys keys to mark as clean in the notes for next backup
+ */
+ private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
+ Log.v(TAG, "unpacking icon " + key.id);
+ if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
+ Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
+ try {
+ Resource res = unpackIcon(buffer, 0, dataSize);
+ if (DEBUG) Log.d(TAG, "unpacked " + res.dpi);
+ if (DEBUG) Log.d(TAG, "read " +
+ Base64.encodeToString(res.data, 0, res.data.length,
+ Base64.NO_WRAP));
+ Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length);
+ if (icon == null) {
+ Log.w(TAG, "failed to unpack icon for " + key.name);
+ }
+ } catch (InvalidProtocolBufferNanoException e) {
+ Log.w(TAG, "failed to decode proto", e);
+ }
+ }
+
+ /** create a new key, with an integer ID.
*
* <P> Keys contain their own checksum instead of using
* the heavy-weight CheckedMessage wrapper.
@@ -439,6 +505,19 @@ public class LauncherBackupAgent extends BackupAgent {
return key;
}
+ /** create a new key for a named object.
+ *
+ * <P> Keys contain their own checksum instead of using
+ * the heavy-weight CheckedMessage wrapper.
+ */
+ private Key getKey(int type, String name) {
+ Key key = new Key();
+ key.type = type;
+ key.name = name;
+ key.checksum = checkKey(key);
+ return key;
+ }
+
/** keys need to be strings, serialize and encode. */
private String keyToBackupKey(Key key) {
return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP | Base64.NO_PADDING);
@@ -460,6 +539,28 @@ public class LauncherBackupAgent extends BackupAgent {
}
}
+ private String getKeyName(Key key) {
+ if (TextUtils.isEmpty(key.name)) {
+ return Long.toString(key.id);
+ } else {
+ return key.name;
+ }
+
+ }
+
+ private String geKeyType(Key key) {
+ switch (key.type) {
+ case Key.FAVORITE:
+ return "favorite";
+ case Key.SCREEN:
+ return "screen";
+ case Key.ICON:
+ return "icon";
+ default:
+ return "anonymous";
+ }
+ }
+
/** Compute the checksum over the important bits of a key. */
private long checkKey(Key key) {
CRC32 checksum = new CRC32();
@@ -544,6 +645,25 @@ public class LauncherBackupAgent extends BackupAgent {
return screen;
}
+ /** Serialize an icon Resource for persistence, including a checksum wrapper. */
+ private byte[] packIcon(int dpi, Bitmap icon) {
+ Resource res = new Resource();
+ res.dpi = dpi;
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ if (icon.compress(WEBP, 100, os)) {
+ res.data = os.toByteArray();
+ }
+ return writeCheckedBytes(res);
+ }
+
+ /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */
+ private Resource unpackIcon(byte[] buffer, int offset, int dataSize)
+ throws InvalidProtocolBufferNanoException {
+ Resource res = new Resource();
+ MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize));
+ return res;
+ }
+
/**
* Read the old journal from the input file.
*
@@ -600,6 +720,41 @@ public class LauncherBackupAgent extends BackupAgent {
return journal;
}
+ private void writeRowToBackup(Key key, byte[] blob, Journal out,
+ BackupDataOutput data) throws IOException {
+ String backupKey = keyToBackupKey(key);
+ data.writeEntityHeader(backupKey, blob.length);
+ data.writeEntityData(blob, blob.length);
+ out.rows++;
+ out.bytes += blob.length;
+ Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " +
+ getKeyName(key) + "/" + blob.length);
+ if(DEBUG) Log.d(TAG, "wrote " +
+ Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
+ }
+
+ private Set<String> getSavedIdsByType(int type, Journal in) {
+ Set<String> savedIds = new HashSet<String>();
+ for(int i = 0; i < in.key.length; i++) {
+ Key key = in.key[i];
+ if (key.type == type) {
+ savedIds.add(keyToBackupKey(key));
+ }
+ }
+ return savedIds;
+ }
+
+ private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data)
+ throws IOException {
+ int rows = 0;
+ for(String deleted: deletedIds) {
+ Log.v(TAG, "dropping icon " + deleted);
+ data.writeEntityHeader(deleted, -1);
+ rows++;
+ }
+ return rows;
+ }
+
/**
* Write the new journal to the output file.
*