From 22e130d40148c28cd4b9ab0c25f1d846da76bce7 Mon Sep 17 00:00:00 2001 From: Chris Wren Date: Mon, 23 Sep 2013 18:25:57 -0400 Subject: [PATCH] backup launcher icons Only allow a small number of icons to be backed up in any given pass. Also refactor common code out of favorite and screen backups. Bug: 10778984 Change-Id: I54bc769c1d1c1c9087ea4bc58f258bd15c167aea --- protos/backup.proto | 2 +- .../launcher3/LauncherBackupAgent.java | 439 ++++++++++++------ 2 files changed, 298 insertions(+), 143 deletions(-) diff --git a/protos/backup.proto b/protos/backup.proto index 3780bc5833..f43f338e5a 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 bb15ca1848..cbef36b735 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 mWidgetMap; + /** * Notify the backup manager that out database is dirty. @@ -155,12 +176,11 @@ public class LauncherBackupAgent extends BackupAgent { ArrayList keys = new ArrayList(); 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 keys) throws IOException { // read the old ID set - Set savedIds = new HashSet(); - for(int i = 0; i < in.key.length; i++) { - Key key = in.key[i]; - if (key.type == Key.FAVORITE) { - savedIds.add(keyToBackupKey(key)); - } - } + Set 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 currentIds = new HashSet(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 currentIds = new HashSet(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 keys) throws IOException { // read the old ID set - Set savedIds = new HashSet(); - 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 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 currentIds = new HashSet(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 currentIds = new HashSet(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 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 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 currentIds = new HashSet(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. + * + *

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 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. * *

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. + * + *

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 getSavedIdsByType(int type, Journal in) { + Set savedIds = new HashSet(); + 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 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. *