blob: 4d06f9cf3e46591dad3996fc9072788d91f076f5 [file] [log] [blame]
Chris Wren1ada10d2013-09-13 18:01:38 -04001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher3;
18
19import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
20import com.google.protobuf.nano.MessageNano;
21
Chris Wren1ada10d2013-09-13 18:01:38 -040022import com.android.launcher3.LauncherSettings.Favorites;
23import com.android.launcher3.LauncherSettings.WorkspaceScreens;
24import com.android.launcher3.backup.BackupProtos;
25import com.android.launcher3.backup.BackupProtos.CheckedMessage;
26import com.android.launcher3.backup.BackupProtos.Favorite;
27import com.android.launcher3.backup.BackupProtos.Journal;
28import com.android.launcher3.backup.BackupProtos.Key;
Chris Wren22e130d2013-09-23 18:25:57 -040029import com.android.launcher3.backup.BackupProtos.Resource;
Chris Wren1ada10d2013-09-13 18:01:38 -040030import com.android.launcher3.backup.BackupProtos.Screen;
Chris Wrenfd13c712013-09-27 15:45:19 -040031import com.android.launcher3.backup.BackupProtos.Widget;
Chris Wren1ada10d2013-09-13 18:01:38 -040032
33import android.app.backup.BackupAgent;
34import android.app.backup.BackupDataInput;
35import android.app.backup.BackupDataOutput;
36import android.app.backup.BackupManager;
Chris Wren22e130d2013-09-23 18:25:57 -040037import android.appwidget.AppWidgetManager;
38import android.appwidget.AppWidgetProviderInfo;
39import android.content.ComponentName;
Chris Wren1ada10d2013-09-13 18:01:38 -040040import android.content.ContentResolver;
41import android.content.Context;
Chris Wren22e130d2013-09-23 18:25:57 -040042import android.content.Intent;
Chris Wren1ada10d2013-09-13 18:01:38 -040043import android.database.Cursor;
Chris Wren22e130d2013-09-23 18:25:57 -040044import android.graphics.Bitmap;
45import android.graphics.BitmapFactory;
Chris Wrenfd13c712013-09-27 15:45:19 -040046import android.graphics.drawable.Drawable;
Chris Wren1ada10d2013-09-13 18:01:38 -040047import android.os.ParcelFileDescriptor;
Chris Wren1ada10d2013-09-13 18:01:38 -040048import android.text.TextUtils;
49import android.util.Base64;
50import android.util.Log;
51
Chris Wren22e130d2013-09-23 18:25:57 -040052import java.io.ByteArrayOutputStream;
Chris Wren1ada10d2013-09-13 18:01:38 -040053import java.io.FileInputStream;
54import java.io.FileOutputStream;
55import java.io.IOException;
Chris Wren22e130d2013-09-23 18:25:57 -040056import java.net.URISyntaxException;
Chris Wren1ada10d2013-09-13 18:01:38 -040057import java.util.ArrayList;
Chris Wren22e130d2013-09-23 18:25:57 -040058import java.util.HashMap;
Chris Wren1ada10d2013-09-13 18:01:38 -040059import java.util.HashSet;
Chris Wren22e130d2013-09-23 18:25:57 -040060import java.util.List;
Chris Wren1ada10d2013-09-13 18:01:38 -040061import java.util.Set;
62import java.util.zip.CRC32;
63
Chris Wren22e130d2013-09-23 18:25:57 -040064import static android.graphics.Bitmap.CompressFormat.WEBP;
65
Chris Wren1ada10d2013-09-13 18:01:38 -040066/**
67 * Persist the launcher home state across calamities.
68 */
69public class LauncherBackupAgent extends BackupAgent {
70
71 private static final String TAG = "LauncherBackupAgent";
Chris Wrenfd13c712013-09-27 15:45:19 -040072 private static final boolean DEBUG = false;
Chris Wren1ada10d2013-09-13 18:01:38 -040073
74 private static final int MAX_JOURNAL_SIZE = 1000000;
75
Chris Wrenfd13c712013-09-27 15:45:19 -040076 /** icons are large, dribble them out */
Chris Wren22e130d2013-09-23 18:25:57 -040077 private static final int MAX_ICONS_PER_PASS = 10;
78
Chris Wrenfd13c712013-09-27 15:45:19 -040079 /** widgets contain previews, which are very large, dribble them out */
80 private static final int MAX_WIDGETS_PER_PASS = 5;
81
82 public static final int IMAGE_COMPRESSION_QUALITY = 75;
83
Chris Wren1ada10d2013-09-13 18:01:38 -040084 private static BackupManager sBackupManager;
85
86 private static final String[] FAVORITE_PROJECTION = {
87 Favorites._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -040088 Favorites.MODIFIED, // 1
89 Favorites.INTENT, // 2
90 Favorites.APPWIDGET_PROVIDER, // 3
91 Favorites.APPWIDGET_ID, // 4
92 Favorites.CELLX, // 5
93 Favorites.CELLY, // 6
94 Favorites.CONTAINER, // 7
95 Favorites.ICON, // 8
96 Favorites.ICON_PACKAGE, // 9
97 Favorites.ICON_RESOURCE, // 10
98 Favorites.ICON_TYPE, // 11
99 Favorites.ITEM_TYPE, // 12
100 Favorites.SCREEN, // 13
101 Favorites.SPANX, // 14
102 Favorites.SPANY, // 15
103 Favorites.TITLE, // 16
Chris Wren1ada10d2013-09-13 18:01:38 -0400104 };
105
106 private static final int ID_INDEX = 0;
Chris Wren22e130d2013-09-23 18:25:57 -0400107 private static final int ID_MODIFIED = 1;
108 private static final int INTENT_INDEX = 2;
109 private static final int APPWIDGET_PROVIDER_INDEX = 3;
110 private static final int APPWIDGET_ID_INDEX = 4;
111 private static final int CELLX_INDEX = 5;
112 private static final int CELLY_INDEX = 6;
113 private static final int CONTAINER_INDEX = 7;
114 private static final int ICON_INDEX = 8;
115 private static final int ICON_PACKAGE_INDEX = 9;
116 private static final int ICON_RESOURCE_INDEX = 10;
117 private static final int ICON_TYPE_INDEX = 11;
118 private static final int ITEM_TYPE_INDEX = 12;
119 private static final int SCREEN_INDEX = 13;
120 private static final int SPANX_INDEX = 14;
121 private static final int SPANY_INDEX = 15;
122 private static final int TITLE_INDEX = 16;
Chris Wren1ada10d2013-09-13 18:01:38 -0400123
124 private static final String[] SCREEN_PROJECTION = {
125 WorkspaceScreens._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -0400126 WorkspaceScreens.MODIFIED, // 1
127 WorkspaceScreens.SCREEN_RANK // 2
Chris Wren1ada10d2013-09-13 18:01:38 -0400128 };
129
Chris Wren22e130d2013-09-23 18:25:57 -0400130 private static final int SCREEN_RANK_INDEX = 2;
Chris Wren1ada10d2013-09-13 18:01:38 -0400131
Chris Wren22e130d2013-09-23 18:25:57 -0400132
133 private static final String[] ICON_PROJECTION = {
134 Favorites._ID, // 0
135 Favorites.MODIFIED, // 1
136 Favorites.INTENT // 2
Chris Wren1ada10d2013-09-13 18:01:38 -0400137 };
138
Chris Wren22e130d2013-09-23 18:25:57 -0400139 private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap;
140
Chris Wren1ada10d2013-09-13 18:01:38 -0400141
142 /**
143 * Notify the backup manager that out database is dirty.
144 *
145 * <P>This does not force an immediate backup.
146 *
147 * @param context application context
148 */
149 public static void dataChanged(Context context) {
150 if (sBackupManager == null) {
151 sBackupManager = new BackupManager(context);
152 }
153 sBackupManager.dataChanged();
154 }
155
156 /**
157 * Back up launcher data so we can restore the user's state on a new device.
158 *
159 * <P>The journal is a timestamp and a list of keys that were saved as of that time.
160 *
161 * <P>Keys may come back in any order, so each key/value is one complete row of the database.
162 *
163 * @param oldState notes from the last backup
164 * @param data incremental key/value pairs to persist off-device
165 * @param newState notes for the next backup
166 * @throws IOException
167 */
168 @Override
169 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
170 ParcelFileDescriptor newState)
171 throws IOException {
172 Log.v(TAG, "onBackup");
173
174 Journal in = readJournal(oldState);
175 Journal out = new Journal();
176
177 long lastBackupTime = in.t;
178 out.t = System.currentTimeMillis();
179 out.rows = 0;
180 out.bytes = 0;
181
182 Log.v(TAG, "lastBackupTime=" + lastBackupTime);
183
184 ArrayList<Key> keys = new ArrayList<Key>();
185 backupFavorites(in, data, out, keys);
186 backupScreens(in, data, out, keys);
Chris Wren22e130d2013-09-23 18:25:57 -0400187 backupIcons(in, data, out, keys);
Chris Wrenfd13c712013-09-27 15:45:19 -0400188 backupWidgets(in, data, out, keys);
Chris Wren1ada10d2013-09-13 18:01:38 -0400189
190 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
191 writeJournal(newState, out);
192 Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
Chris Wren1ada10d2013-09-13 18:01:38 -0400193 }
194
195 /**
196 * Restore home screen from the restored data stream.
197 *
198 * <P>Keys may arrive in any order.
199 *
200 * @param data the key/value pairs from the server
201 * @param versionCode the version of the app that generated the data
202 * @param newState notes for the next backup
203 * @throws IOException
204 */
205 @Override
206 public void onRestore(BackupDataInput data, int versionCode, ParcelFileDescriptor newState)
207 throws IOException {
208 Log.v(TAG, "onRestore");
209 int numRows = 0;
210 Journal out = new Journal();
211
212 ArrayList<Key> keys = new ArrayList<Key>();
213 byte[] buffer = new byte[512];
214 while (data.readNextHeader()) {
215 numRows++;
216 String backupKey = data.getKey();
217 int dataSize = data.getDataSize();
218 if (buffer.length < dataSize) {
219 buffer = new byte[dataSize];
220 }
221 Key key = null;
222 int bytesRead = data.readEntityData(buffer, 0, dataSize);
223 if (DEBUG) {
224 Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available");
225 }
226 try {
227 key = backupKeyToKey(backupKey);
228 switch (key.type) {
229 case Key.FAVORITE:
230 restoreFavorite(key, buffer, dataSize, keys);
231 break;
232
233 case Key.SCREEN:
234 restoreScreen(key, buffer, dataSize, keys);
235 break;
236
Chris Wren22e130d2013-09-23 18:25:57 -0400237 case Key.ICON:
238 restoreIcon(key, buffer, dataSize, keys);
239 break;
240
Chris Wrenfd13c712013-09-27 15:45:19 -0400241 case Key.WIDGET:
242 restoreWidget(key, buffer, dataSize, keys);
243 break;
244
Chris Wren1ada10d2013-09-13 18:01:38 -0400245 default:
246 Log.w(TAG, "unknown restore entity type: " + key.type);
247 break;
248 }
249 } catch (KeyParsingException e) {
250 Log.w(TAG, "ignoring unparsable backup key: " + backupKey);
251 }
252 }
253
254 // clear the output journal time, to force a full backup to
255 // will catch any changes the restore process might have made
256 out.t = 0;
257 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
258 writeJournal(newState, out);
259 Log.v(TAG, "onRestore: read " + numRows + " rows");
260 }
261
262 /**
263 * Write all modified favorites to the data stream.
264 *
265 *
266 * @param in notes from last backup
267 * @param data output stream for key/value pairs
268 * @param out notes about this backup
269 * @param keys keys to mark as clean in the notes for next backup
270 * @throws IOException
271 */
272 private void backupFavorites(Journal in, BackupDataOutput data, Journal out,
273 ArrayList<Key> keys)
274 throws IOException {
275 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400276 Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in);
Chris Wren1ada10d2013-09-13 18:01:38 -0400277 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
278
279 // persist things that have changed since the last backup
280 ContentResolver cr = getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400281 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
282 null, null, null);
283 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400284 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400285 cursor.moveToPosition(-1);
286 while(cursor.moveToNext()) {
287 final long id = cursor.getLong(ID_INDEX);
288 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400289 Key key = getKey(Key.FAVORITE, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400290 keys.add(key);
Chris Wren22e130d2013-09-23 18:25:57 -0400291 currentIds.add(keyToBackupKey(key));
292 if (updateTime > in.t) {
293 byte[] blob = packFavorite(cursor);
294 writeRowToBackup(key, blob, out, data);
295 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400296 }
297 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400298 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400299 }
300 if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
301
302 // these IDs must have been deleted
303 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400304 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400305 }
306
307 /**
308 * Read a favorite from the stream.
309 *
310 * <P>Keys arrive in any order, so screens and containers may not exist yet.
311 *
312 * @param key identifier for the row
313 * @param buffer the serialized proto from the stream, may be larger than dataSize
314 * @param dataSize the size of the proto from the stream
315 * @param keys keys to mark as clean in the notes for next backup
316 */
317 private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
318 Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)");
319 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
320 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
321
322 try {
323 Favorite favorite = unpackFavorite(buffer, 0, dataSize);
324 if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType);
325 } catch (InvalidProtocolBufferNanoException e) {
326 Log.w(TAG, "failed to decode proto", e);
327 }
328 }
329
330 /**
331 * Write all modified screens to the data stream.
332 *
333 *
334 * @param in notes from last backup
335 * @param data output stream for key/value pairs
336 * @param out notes about this backup
Chris Wren22e130d2013-09-23 18:25:57 -0400337 * @param keys keys to mark as clean in the notes for next backup
338 * @throws IOException
Chris Wren1ada10d2013-09-13 18:01:38 -0400339 */
340 private void backupScreens(Journal in, BackupDataOutput data, Journal out,
341 ArrayList<Key> keys)
342 throws IOException {
343 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400344 Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in);
345 if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
Chris Wren1ada10d2013-09-13 18:01:38 -0400346
347 // persist things that have changed since the last backup
348 ContentResolver cr = getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400349 Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
350 null, null, null);
351 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400352 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400353 cursor.moveToPosition(-1);
354 while(cursor.moveToNext()) {
355 final long id = cursor.getLong(ID_INDEX);
356 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400357 Key key = getKey(Key.SCREEN, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400358 keys.add(key);
Chris Wren22e130d2013-09-23 18:25:57 -0400359 currentIds.add(keyToBackupKey(key));
360 if (updateTime > in.t) {
361 byte[] blob = packScreen(cursor);
362 writeRowToBackup(key, blob, out, data);
363 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400364 }
365 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400366 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400367 }
368 if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
369
370 // these IDs must have been deleted
371 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400372 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400373 }
374
375 /**
376 * Read a screen from the stream.
377 *
378 * <P>Keys arrive in any order, so children of this screen may already exist.
379 *
380 * @param key identifier for the row
381 * @param buffer the serialized proto from the stream, may be larger than dataSize
382 * @param dataSize the size of the proto from the stream
383 * @param keys keys to mark as clean in the notes for next backup
384 */
385 private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
386 Log.v(TAG, "unpacking screen " + key.id);
387 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
388 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
389 try {
390 Screen screen = unpackScreen(buffer, 0, dataSize);
391 if (DEBUG) Log.d(TAG, "unpacked " + screen.rank);
392 } catch (InvalidProtocolBufferNanoException e) {
393 Log.w(TAG, "failed to decode proto", e);
394 }
395 }
396
Chris Wren22e130d2013-09-23 18:25:57 -0400397 /**
398 * Write all the static icon resources we need to render placeholders
399 * for a package that is not installed.
400 *
401 * @param in notes from last backup
402 * @param data output stream for key/value pairs
403 * @param out notes about this backup
404 * @param keys keys to mark as clean in the notes for next backup
405 * @throws IOException
406 */
407 private void backupIcons(Journal in, BackupDataOutput data, Journal out,
408 ArrayList<Key> keys) throws IOException {
Chris Wrenfd13c712013-09-27 15:45:19 -0400409 // persist icons that haven't been persisted yet
Chris Wren22e130d2013-09-23 18:25:57 -0400410 final ContentResolver cr = getContentResolver();
Chris Wrenfd13c712013-09-27 15:45:19 -0400411 final LauncherAppState app = LauncherAppState.getInstance();
412 final IconCache iconCache = app.getIconCache();
Chris Wren22e130d2013-09-23 18:25:57 -0400413 final int dpi = getResources().getDisplayMetrics().densityDpi;
414
415 // read the old ID set
416 Set<String> savedIds = getSavedIdsByType(Key.ICON, in);
417 if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size());
418
419 int startRows = out.rows;
420 if (DEBUG) Log.d(TAG, "starting here: " + startRows);
421 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION;
422 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
423 where, null, null);
424 Set<String> currentIds = new HashSet<String>(cursor.getCount());
425 try {
426 cursor.moveToPosition(-1);
427 while(cursor.moveToNext()) {
428 final long id = cursor.getLong(ID_INDEX);
429 final String intentDescription = cursor.getString(INTENT_INDEX);
430 try {
431 Intent intent = Intent.parseUri(intentDescription, 0);
432 ComponentName cn = intent.getComponent();
433 Key key = null;
434 String backupKey = null;
435 if (cn != null) {
436 key = getKey(Key.ICON, cn.flattenToShortString());
437 backupKey = keyToBackupKey(key);
438 currentIds.add(backupKey);
439 } else {
440 Log.w(TAG, "empty intent on application favorite: " + id);
441 }
442 if (savedIds.contains(backupKey)) {
443 if (DEBUG) Log.d(TAG, "already saved icon " + backupKey);
444
445 // remember that we already backed this up previously
446 keys.add(key);
447 } else if (backupKey != null) {
448 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
449 if ((out.rows - startRows) < MAX_ICONS_PER_PASS) {
450 if (DEBUG) Log.d(TAG, "saving icon " + backupKey);
451 Bitmap icon = iconCache.getIcon(intent);
452 keys.add(key);
453 if (icon != null && !iconCache.isDefaultIcon(icon)) {
454 byte[] blob = packIcon(dpi, icon);
455 writeRowToBackup(key, blob, out, data);
456 }
457 } else {
Chris Wrenfd13c712013-09-27 15:45:19 -0400458 if (DEBUG) Log.d(TAG, "scheduling another run for icon " + backupKey);
Chris Wren22e130d2013-09-23 18:25:57 -0400459 // too many icons for this pass, request another.
460 dataChanged(this);
461 }
462 }
463 } catch (URISyntaxException e) {
464 Log.w(TAG, "invalid URI on application favorite: " + id);
465 } catch (IOException e) {
466 Log.w(TAG, "unable to save application icon for favorite: " + id);
467 }
468
469 }
470 } finally {
471 cursor.close();
472 }
473 if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size());
474
475 // these IDs must have been deleted
476 savedIds.removeAll(currentIds);
477 out.rows += removeDeletedKeysFromBackup(savedIds, data);
478 }
479
480 /**
481 * Read an icon from the stream.
482 *
Chris Wrenfd13c712013-09-27 15:45:19 -0400483 * <P>Keys arrive in any order, so shortcuts that use this icon may already exist.
Chris Wren22e130d2013-09-23 18:25:57 -0400484 *
485 * @param key identifier for the row
486 * @param buffer the serialized proto from the stream, may be larger than dataSize
487 * @param dataSize the size of the proto from the stream
488 * @param keys keys to mark as clean in the notes for next backup
489 */
490 private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
491 Log.v(TAG, "unpacking icon " + key.id);
492 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
493 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
494 try {
495 Resource res = unpackIcon(buffer, 0, dataSize);
496 if (DEBUG) Log.d(TAG, "unpacked " + res.dpi);
497 if (DEBUG) Log.d(TAG, "read " +
498 Base64.encodeToString(res.data, 0, res.data.length,
499 Base64.NO_WRAP));
500 Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length);
501 if (icon == null) {
502 Log.w(TAG, "failed to unpack icon for " + key.name);
503 }
504 } catch (InvalidProtocolBufferNanoException e) {
505 Log.w(TAG, "failed to decode proto", e);
506 }
507 }
508
Chris Wrenfd13c712013-09-27 15:45:19 -0400509 /**
510 * Write all the static widget resources we need to render placeholders
511 * for a package that is not installed.
512 *
513 * @param in notes from last backup
514 * @param data output stream for key/value pairs
515 * @param out notes about this backup
516 * @param keys keys to mark as clean in the notes for next backup
517 * @throws IOException
518 */
519 private void backupWidgets(Journal in, BackupDataOutput data, Journal out,
520 ArrayList<Key> keys) throws IOException {
521 // persist static widget info that hasn't been persisted yet
522 final ContentResolver cr = getContentResolver();
523 final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(this);
524 final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(this);
525 final LauncherAppState appState = LauncherAppState.getInstance();
526 final IconCache iconCache = appState.getIconCache();
527 final int dpi = getResources().getDisplayMetrics().densityDpi;
528 final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile();
529 if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx);
530
531 // read the old ID set
532 Set<String> savedIds = getSavedIdsByType(Key.WIDGET, in);
533 if (DEBUG) Log.d(TAG, "widgets savedIds.size()=" + savedIds.size());
534
535 int startRows = out.rows;
536 if (DEBUG) Log.d(TAG, "starting here: " + startRows);
537 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET;
538 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
539 where, null, null);
540 Set<String> currentIds = new HashSet<String>(cursor.getCount());
541 try {
542 cursor.moveToPosition(-1);
543 while(cursor.moveToNext()) {
544 final long id = cursor.getLong(ID_INDEX);
545 final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX);
546 final int spanX = cursor.getInt(SPANX_INDEX);
547 final int spanY = cursor.getInt(SPANY_INDEX);
548 final ComponentName provider = ComponentName.unflattenFromString(providerName);
549 Key key = null;
550 String backupKey = null;
551 if (provider != null) {
552 key = getKey(Key.WIDGET, providerName);
553 backupKey = keyToBackupKey(key);
554 currentIds.add(backupKey);
555 } else {
556 Log.w(TAG, "empty intent on appwidget: " + id);
557 }
558 if (savedIds.contains(backupKey)) {
559 if (DEBUG) Log.d(TAG, "already saved widget " + backupKey);
560
561 // remember that we already backed this up previously
562 keys.add(key);
563 } else if (backupKey != null) {
564 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
565 if ((out.rows - startRows) < MAX_WIDGETS_PER_PASS) {
566 if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
567 previewLoader.setPreviewSize(spanX * profile.cellWidthPx,
568 spanY * profile.cellHeightPx, widgetSpacingLayout);
569 byte[] blob = packWidget(dpi, previewLoader, iconCache, provider);
Chris Wrenb1fd63b2013-10-03 15:43:58 -0400570 keys.add(key);
Chris Wrenfd13c712013-09-27 15:45:19 -0400571 writeRowToBackup(key, blob, out, data);
572
573 } else {
574 if (DEBUG) Log.d(TAG, "scheduling another run for widget " + backupKey);
575 // too many widgets for this pass, request another.
576 dataChanged(this);
577 }
578 }
579 }
580 } finally {
581 cursor.close();
582 }
583 if (DEBUG) Log.d(TAG, "widget currentIds.size()=" + currentIds.size());
584
585 // these IDs must have been deleted
586 savedIds.removeAll(currentIds);
587 out.rows += removeDeletedKeysFromBackup(savedIds, data);
588 }
589
590 /**
591 * Read a widget from the stream.
592 *
593 * <P>Keys arrive in any order, so widgets that use this data may already exist.
594 *
595 * @param key identifier for the row
596 * @param buffer the serialized proto from the stream, may be larger than dataSize
597 * @param dataSize the size of the proto from the stream
598 * @param keys keys to mark as clean in the notes for next backup
599 */
600 private void restoreWidget(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
601 Log.v(TAG, "unpacking widget " + key.id);
602 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
603 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
604 try {
605 Widget widget = unpackWidget(buffer, 0, dataSize);
606 if (DEBUG) Log.d(TAG, "unpacked " + widget.provider);
607 if (widget.icon.data != null) {
608 Bitmap icon = BitmapFactory
609 .decodeByteArray(widget.icon.data, 0, widget.icon.data.length);
610 if (icon == null) {
611 Log.w(TAG, "failed to unpack widget icon for " + key.name);
612 }
613 }
614 } catch (InvalidProtocolBufferNanoException e) {
615 Log.w(TAG, "failed to decode proto", e);
616 }
617 }
618
Chris Wren22e130d2013-09-23 18:25:57 -0400619 /** create a new key, with an integer ID.
Chris Wren1ada10d2013-09-13 18:01:38 -0400620 *
621 * <P> Keys contain their own checksum instead of using
622 * the heavy-weight CheckedMessage wrapper.
623 */
624 private Key getKey(int type, long id) {
625 Key key = new Key();
626 key.type = type;
627 key.id = id;
628 key.checksum = checkKey(key);
629 return key;
630 }
631
Chris Wren22e130d2013-09-23 18:25:57 -0400632 /** create a new key for a named object.
633 *
634 * <P> Keys contain their own checksum instead of using
635 * the heavy-weight CheckedMessage wrapper.
636 */
637 private Key getKey(int type, String name) {
638 Key key = new Key();
639 key.type = type;
640 key.name = name;
641 key.checksum = checkKey(key);
642 return key;
643 }
644
Chris Wren1ada10d2013-09-13 18:01:38 -0400645 /** keys need to be strings, serialize and encode. */
646 private String keyToBackupKey(Key key) {
Chris Wren978194c2013-10-03 17:47:22 -0400647 return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP);
Chris Wren1ada10d2013-09-13 18:01:38 -0400648 }
649
650 /** keys need to be strings, decode and parse. */
651 private Key backupKeyToKey(String backupKey) throws KeyParsingException {
652 try {
653 Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT));
654 if (key.checksum != checkKey(key)) {
655 key = null;
656 throw new KeyParsingException("invalid key read from stream" + backupKey);
657 }
658 return key;
659 } catch (InvalidProtocolBufferNanoException e) {
660 throw new KeyParsingException(e);
661 } catch (IllegalArgumentException e) {
662 throw new KeyParsingException(e);
663 }
664 }
665
Chris Wren22e130d2013-09-23 18:25:57 -0400666 private String getKeyName(Key key) {
667 if (TextUtils.isEmpty(key.name)) {
668 return Long.toString(key.id);
669 } else {
670 return key.name;
671 }
672
673 }
674
675 private String geKeyType(Key key) {
676 switch (key.type) {
677 case Key.FAVORITE:
678 return "favorite";
679 case Key.SCREEN:
680 return "screen";
681 case Key.ICON:
682 return "icon";
Chris Wrenfd13c712013-09-27 15:45:19 -0400683 case Key.WIDGET:
684 return "widget";
Chris Wren22e130d2013-09-23 18:25:57 -0400685 default:
686 return "anonymous";
687 }
688 }
689
Chris Wren1ada10d2013-09-13 18:01:38 -0400690 /** Compute the checksum over the important bits of a key. */
691 private long checkKey(Key key) {
692 CRC32 checksum = new CRC32();
693 checksum.update(key.type);
694 checksum.update((int) (key.id & 0xffff));
695 checksum.update((int) ((key.id >> 32) & 0xffff));
696 if (!TextUtils.isEmpty(key.name)) {
697 checksum.update(key.name.getBytes());
698 }
699 return checksum.getValue();
700 }
701
702 /** Serialize a Favorite for persistence, including a checksum wrapper. */
703 private byte[] packFavorite(Cursor c) {
704 Favorite favorite = new Favorite();
705 favorite.id = c.getLong(ID_INDEX);
706 favorite.screen = c.getInt(SCREEN_INDEX);
707 favorite.container = c.getInt(CONTAINER_INDEX);
708 favorite.cellX = c.getInt(CELLX_INDEX);
709 favorite.cellY = c.getInt(CELLY_INDEX);
710 favorite.spanX = c.getInt(SPANX_INDEX);
711 favorite.spanY = c.getInt(SPANY_INDEX);
712 favorite.iconType = c.getInt(ICON_TYPE_INDEX);
713 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
714 String iconPackage = c.getString(ICON_PACKAGE_INDEX);
715 if (!TextUtils.isEmpty(iconPackage)) {
716 favorite.iconPackage = iconPackage;
717 }
718 String iconResource = c.getString(ICON_RESOURCE_INDEX);
719 if (!TextUtils.isEmpty(iconResource)) {
720 favorite.iconResource = iconResource;
721 }
722 }
723 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
724 byte[] blob = c.getBlob(ICON_INDEX);
725 if (blob != null && blob.length > 0) {
726 favorite.icon = blob;
727 }
728 }
729 String title = c.getString(TITLE_INDEX);
730 if (!TextUtils.isEmpty(title)) {
731 favorite.title = title;
732 }
733 String intent = c.getString(INTENT_INDEX);
734 if (!TextUtils.isEmpty(intent)) {
735 favorite.intent = intent;
736 }
737 favorite.itemType = c.getInt(ITEM_TYPE_INDEX);
738 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
739 favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX);
740 String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX);
741 if (!TextUtils.isEmpty(appWidgetProvider)) {
742 favorite.appWidgetProvider = appWidgetProvider;
743 }
744 }
745
746 return writeCheckedBytes(favorite);
747 }
748
749 /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */
750 private Favorite unpackFavorite(byte[] buffer, int offset, int dataSize)
751 throws InvalidProtocolBufferNanoException {
752 Favorite favorite = new Favorite();
753 MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize));
754 return favorite;
755 }
756
757 /** Serialize a Screen for persistence, including a checksum wrapper. */
758 private byte[] packScreen(Cursor c) {
759 Screen screen = new Screen();
760 screen.id = c.getLong(ID_INDEX);
761 screen.rank = c.getInt(SCREEN_RANK_INDEX);
762
763 return writeCheckedBytes(screen);
764 }
765
766 /** Deserialize a Screen from persistence, after verifying checksum wrapper. */
767 private Screen unpackScreen(byte[] buffer, int offset, int dataSize)
768 throws InvalidProtocolBufferNanoException {
769 Screen screen = new Screen();
770 MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize));
771 return screen;
772 }
773
Chris Wren22e130d2013-09-23 18:25:57 -0400774 /** Serialize an icon Resource for persistence, including a checksum wrapper. */
775 private byte[] packIcon(int dpi, Bitmap icon) {
776 Resource res = new Resource();
777 res.dpi = dpi;
778 ByteArrayOutputStream os = new ByteArrayOutputStream();
Chris Wrenfd13c712013-09-27 15:45:19 -0400779 if (icon.compress(WEBP, IMAGE_COMPRESSION_QUALITY, os)) {
Chris Wren22e130d2013-09-23 18:25:57 -0400780 res.data = os.toByteArray();
781 }
782 return writeCheckedBytes(res);
783 }
784
785 /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */
786 private Resource unpackIcon(byte[] buffer, int offset, int dataSize)
787 throws InvalidProtocolBufferNanoException {
788 Resource res = new Resource();
789 MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize));
790 return res;
791 }
792
Chris Wrenfd13c712013-09-27 15:45:19 -0400793 /** Serialize a widget for persistence, including a checksum wrapper. */
794 private byte[] packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache,
795 ComponentName provider) {
796 final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider);
797 Widget widget = new Widget();
798 widget.provider = provider.flattenToShortString();
799 widget.label = info.label;
800 widget.configure = info.configure != null;
801 if (info.icon != 0) {
802 widget.icon = new Resource();
803 Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon);
804 Bitmap icon = Utilities.createIconBitmap(fullResIcon, this);
805 ByteArrayOutputStream os = new ByteArrayOutputStream();
806 if (icon.compress(WEBP, IMAGE_COMPRESSION_QUALITY, os)) {
807 widget.icon.data = os.toByteArray();
808 widget.icon.dpi = dpi;
809 }
810 }
811 if (info.previewImage != 0) {
812 widget.preview = new Resource();
813 Bitmap preview = previewLoader.generateWidgetPreview(info, null);
814 ByteArrayOutputStream os = new ByteArrayOutputStream();
815 if (preview.compress(WEBP, IMAGE_COMPRESSION_QUALITY, os)) {
816 widget.preview.data = os.toByteArray();
817 widget.preview.dpi = dpi;
818 }
819 }
820 return writeCheckedBytes(widget);
821 }
822
823 /** Deserialize a widget from persistence, after verifying checksum wrapper. */
824 private Widget unpackWidget(byte[] buffer, int offset, int dataSize)
825 throws InvalidProtocolBufferNanoException {
826 Widget widget = new Widget();
827 MessageNano.mergeFrom(widget, readCheckedBytes(buffer, offset, dataSize));
828 return widget;
829 }
830
Chris Wren1ada10d2013-09-13 18:01:38 -0400831 /**
832 * Read the old journal from the input file.
833 *
834 * In the event of any error, just pretend we didn't have a journal,
835 * in that case, do a full backup.
836 *
837 * @param oldState the read-0only file descriptor pointing to the old journal
838 * @return a Journal protocol bugffer
839 */
840 private Journal readJournal(ParcelFileDescriptor oldState) {
841 int fileSize = (int) oldState.getStatSize();
842 int remaining = fileSize;
843 byte[] buffer = null;
844 Journal journal = new Journal();
845 if (remaining < MAX_JOURNAL_SIZE) {
846 FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor());
847 int offset = 0;
848
849 buffer = new byte[remaining];
850 while (remaining > 0) {
851 int bytesRead = 0;
852 try {
853 bytesRead = inStream.read(buffer, offset, remaining);
854 } catch (IOException e) {
855 Log.w(TAG, "failed to read the journal", e);
856 buffer = null;
857 remaining = 0;
858 }
859 if (bytesRead > 0) {
860 remaining -= bytesRead;
861 } else {
862 // act like there is not journal
863 Log.w(TAG, "failed to read the journal");
864 buffer = null;
865 remaining = 0;
866 }
867 }
868
869 if (buffer != null) {
870 try {
871 MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, fileSize));
872 } catch (InvalidProtocolBufferNanoException e) {
873 Log.d(TAG, "failed to read the journal", e);
874 journal.clear();
875 }
876 }
877
878 try {
879 inStream.close();
880 } catch (IOException e) {
881 Log.d(TAG, "failed to close the journal", e);
882 }
883 }
884 return journal;
885 }
886
Chris Wren22e130d2013-09-23 18:25:57 -0400887 private void writeRowToBackup(Key key, byte[] blob, Journal out,
888 BackupDataOutput data) throws IOException {
889 String backupKey = keyToBackupKey(key);
890 data.writeEntityHeader(backupKey, blob.length);
891 data.writeEntityData(blob, blob.length);
892 out.rows++;
893 out.bytes += blob.length;
894 Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " +
895 getKeyName(key) + "/" + blob.length);
Chris Wren2b6c21d2013-10-02 14:16:04 -0400896 if(DEBUG) {
897 String encoded = Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP);
898 final int chunkSize = 1024;
899 for (int offset = 0; offset < encoded.length(); offset += chunkSize) {
900 int end = offset + chunkSize;
901 end = Math.min(end, encoded.length());
902 Log.d(TAG, "wrote " + encoded.substring(offset, end));
903 }
904 }
Chris Wren22e130d2013-09-23 18:25:57 -0400905 }
906
907 private Set<String> getSavedIdsByType(int type, Journal in) {
908 Set<String> savedIds = new HashSet<String>();
909 for(int i = 0; i < in.key.length; i++) {
910 Key key = in.key[i];
911 if (key.type == type) {
912 savedIds.add(keyToBackupKey(key));
913 }
914 }
915 return savedIds;
916 }
917
918 private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data)
919 throws IOException {
920 int rows = 0;
921 for(String deleted: deletedIds) {
922 Log.v(TAG, "dropping icon " + deleted);
923 data.writeEntityHeader(deleted, -1);
924 rows++;
925 }
926 return rows;
927 }
928
Chris Wren1ada10d2013-09-13 18:01:38 -0400929 /**
930 * Write the new journal to the output file.
931 *
932 * In the event of any error, just pretend we didn't have a journal,
933 * in that case, do a full backup.
934
935 * @param newState the write-only file descriptor pointing to the new journal
936 * @param journal a Journal protocol buffer
937 */
938 private void writeJournal(ParcelFileDescriptor newState, Journal journal) {
939 FileOutputStream outStream = null;
940 try {
941 outStream = new FileOutputStream(newState.getFileDescriptor());
942 outStream.write(writeCheckedBytes(journal));
943 outStream.close();
944 } catch (IOException e) {
945 Log.d(TAG, "failed to write backup journal", e);
946 }
947 }
948
949 /** Wrap a proto in a CheckedMessage and compute the checksum. */
950 private byte[] writeCheckedBytes(MessageNano proto) {
951 CheckedMessage wrapper = new CheckedMessage();
952 wrapper.payload = MessageNano.toByteArray(proto);
953 CRC32 checksum = new CRC32();
954 checksum.update(wrapper.payload);
955 wrapper.checksum = checksum.getValue();
956 return MessageNano.toByteArray(wrapper);
957 }
958
959 /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */
960 private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize)
961 throws InvalidProtocolBufferNanoException {
962 CheckedMessage wrapper = new CheckedMessage();
963 MessageNano.mergeFrom(wrapper, buffer, offset, dataSize);
964 CRC32 checksum = new CRC32();
965 checksum.update(wrapper.payload);
966 if (wrapper.checksum != checksum.getValue()) {
967 throw new InvalidProtocolBufferNanoException("checksum does not match");
968 }
969 return wrapper.payload;
970 }
971
Chris Wrenfd13c712013-09-27 15:45:19 -0400972 private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) {
973 if (mWidgetMap == null) {
974 List<AppWidgetProviderInfo> widgets =
975 AppWidgetManager.getInstance(this).getInstalledProviders();
976 mWidgetMap = new HashMap<ComponentName, AppWidgetProviderInfo>(widgets.size());
977 for (AppWidgetProviderInfo info : widgets) {
978 mWidgetMap.put(info.provider, info);
979 }
980 }
981 return mWidgetMap.get(component);
982 }
983
Chris Wren1ada10d2013-09-13 18:01:38 -0400984 private class KeyParsingException extends Throwable {
985 private KeyParsingException(Throwable cause) {
986 super(cause);
987 }
988
989 public KeyParsingException(String reason) {
990 super(reason);
991 }
992 }
993}