blob: cbef36b735de6bb826b14be3155ba97313781ba0 [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;
31
32import android.app.backup.BackupAgent;
33import android.app.backup.BackupDataInput;
34import android.app.backup.BackupDataOutput;
35import android.app.backup.BackupManager;
Chris Wren22e130d2013-09-23 18:25:57 -040036import android.appwidget.AppWidgetManager;
37import android.appwidget.AppWidgetProviderInfo;
38import android.content.ComponentName;
Chris Wren1ada10d2013-09-13 18:01:38 -040039import android.content.ContentResolver;
40import android.content.Context;
Chris Wren22e130d2013-09-23 18:25:57 -040041import android.content.Intent;
Chris Wren1ada10d2013-09-13 18:01:38 -040042import android.database.Cursor;
Chris Wren22e130d2013-09-23 18:25:57 -040043import android.graphics.Bitmap;
44import android.graphics.BitmapFactory;
Chris Wren1ada10d2013-09-13 18:01:38 -040045import android.os.ParcelFileDescriptor;
Chris Wren1ada10d2013-09-13 18:01:38 -040046import android.text.TextUtils;
47import android.util.Base64;
48import android.util.Log;
49
Chris Wren22e130d2013-09-23 18:25:57 -040050import java.io.ByteArrayOutputStream;
Chris Wren1ada10d2013-09-13 18:01:38 -040051import java.io.FileInputStream;
52import java.io.FileOutputStream;
53import java.io.IOException;
Chris Wren22e130d2013-09-23 18:25:57 -040054import java.net.URISyntaxException;
Chris Wren1ada10d2013-09-13 18:01:38 -040055import java.util.ArrayList;
Chris Wren22e130d2013-09-23 18:25:57 -040056import java.util.HashMap;
Chris Wren1ada10d2013-09-13 18:01:38 -040057import java.util.HashSet;
Chris Wren22e130d2013-09-23 18:25:57 -040058import java.util.List;
Chris Wren1ada10d2013-09-13 18:01:38 -040059import java.util.Set;
60import java.util.zip.CRC32;
61
Chris Wren22e130d2013-09-23 18:25:57 -040062import static android.graphics.Bitmap.CompressFormat.WEBP;
63
Chris Wren1ada10d2013-09-13 18:01:38 -040064/**
65 * Persist the launcher home state across calamities.
66 */
67public class LauncherBackupAgent extends BackupAgent {
68
69 private static final String TAG = "LauncherBackupAgent";
Chris Wren22e130d2013-09-23 18:25:57 -040070 private static final boolean DEBUG = true;
Chris Wren1ada10d2013-09-13 18:01:38 -040071
72 private static final int MAX_JOURNAL_SIZE = 1000000;
73
Chris Wren22e130d2013-09-23 18:25:57 -040074 private static final int MAX_ICONS_PER_PASS = 10;
75
Chris Wren1ada10d2013-09-13 18:01:38 -040076 private static BackupManager sBackupManager;
77
78 private static final String[] FAVORITE_PROJECTION = {
79 Favorites._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -040080 Favorites.MODIFIED, // 1
81 Favorites.INTENT, // 2
82 Favorites.APPWIDGET_PROVIDER, // 3
83 Favorites.APPWIDGET_ID, // 4
84 Favorites.CELLX, // 5
85 Favorites.CELLY, // 6
86 Favorites.CONTAINER, // 7
87 Favorites.ICON, // 8
88 Favorites.ICON_PACKAGE, // 9
89 Favorites.ICON_RESOURCE, // 10
90 Favorites.ICON_TYPE, // 11
91 Favorites.ITEM_TYPE, // 12
92 Favorites.SCREEN, // 13
93 Favorites.SPANX, // 14
94 Favorites.SPANY, // 15
95 Favorites.TITLE, // 16
Chris Wren1ada10d2013-09-13 18:01:38 -040096 };
97
98 private static final int ID_INDEX = 0;
Chris Wren22e130d2013-09-23 18:25:57 -040099 private static final int ID_MODIFIED = 1;
100 private static final int INTENT_INDEX = 2;
101 private static final int APPWIDGET_PROVIDER_INDEX = 3;
102 private static final int APPWIDGET_ID_INDEX = 4;
103 private static final int CELLX_INDEX = 5;
104 private static final int CELLY_INDEX = 6;
105 private static final int CONTAINER_INDEX = 7;
106 private static final int ICON_INDEX = 8;
107 private static final int ICON_PACKAGE_INDEX = 9;
108 private static final int ICON_RESOURCE_INDEX = 10;
109 private static final int ICON_TYPE_INDEX = 11;
110 private static final int ITEM_TYPE_INDEX = 12;
111 private static final int SCREEN_INDEX = 13;
112 private static final int SPANX_INDEX = 14;
113 private static final int SPANY_INDEX = 15;
114 private static final int TITLE_INDEX = 16;
Chris Wren1ada10d2013-09-13 18:01:38 -0400115
116 private static final String[] SCREEN_PROJECTION = {
117 WorkspaceScreens._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -0400118 WorkspaceScreens.MODIFIED, // 1
119 WorkspaceScreens.SCREEN_RANK // 2
Chris Wren1ada10d2013-09-13 18:01:38 -0400120 };
121
Chris Wren22e130d2013-09-23 18:25:57 -0400122 private static final int SCREEN_RANK_INDEX = 2;
Chris Wren1ada10d2013-09-13 18:01:38 -0400123
Chris Wren22e130d2013-09-23 18:25:57 -0400124
125 private static final String[] ICON_PROJECTION = {
126 Favorites._ID, // 0
127 Favorites.MODIFIED, // 1
128 Favorites.INTENT // 2
Chris Wren1ada10d2013-09-13 18:01:38 -0400129 };
130
Chris Wren22e130d2013-09-23 18:25:57 -0400131 private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap;
132
Chris Wren1ada10d2013-09-13 18:01:38 -0400133
134 /**
135 * Notify the backup manager that out database is dirty.
136 *
137 * <P>This does not force an immediate backup.
138 *
139 * @param context application context
140 */
141 public static void dataChanged(Context context) {
142 if (sBackupManager == null) {
143 sBackupManager = new BackupManager(context);
144 }
145 sBackupManager.dataChanged();
146 }
147
148 /**
149 * Back up launcher data so we can restore the user's state on a new device.
150 *
151 * <P>The journal is a timestamp and a list of keys that were saved as of that time.
152 *
153 * <P>Keys may come back in any order, so each key/value is one complete row of the database.
154 *
155 * @param oldState notes from the last backup
156 * @param data incremental key/value pairs to persist off-device
157 * @param newState notes for the next backup
158 * @throws IOException
159 */
160 @Override
161 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
162 ParcelFileDescriptor newState)
163 throws IOException {
164 Log.v(TAG, "onBackup");
165
166 Journal in = readJournal(oldState);
167 Journal out = new Journal();
168
169 long lastBackupTime = in.t;
170 out.t = System.currentTimeMillis();
171 out.rows = 0;
172 out.bytes = 0;
173
174 Log.v(TAG, "lastBackupTime=" + lastBackupTime);
175
176 ArrayList<Key> keys = new ArrayList<Key>();
177 backupFavorites(in, data, out, keys);
178 backupScreens(in, data, out, keys);
Chris Wren22e130d2013-09-23 18:25:57 -0400179 backupIcons(in, data, out, keys);
Chris Wren1ada10d2013-09-13 18:01:38 -0400180
181 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
182 writeJournal(newState, out);
183 Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
Chris Wren1ada10d2013-09-13 18:01:38 -0400184 }
185
186 /**
187 * Restore home screen from the restored data stream.
188 *
189 * <P>Keys may arrive in any order.
190 *
191 * @param data the key/value pairs from the server
192 * @param versionCode the version of the app that generated the data
193 * @param newState notes for the next backup
194 * @throws IOException
195 */
196 @Override
197 public void onRestore(BackupDataInput data, int versionCode, ParcelFileDescriptor newState)
198 throws IOException {
199 Log.v(TAG, "onRestore");
200 int numRows = 0;
201 Journal out = new Journal();
202
203 ArrayList<Key> keys = new ArrayList<Key>();
204 byte[] buffer = new byte[512];
205 while (data.readNextHeader()) {
206 numRows++;
207 String backupKey = data.getKey();
208 int dataSize = data.getDataSize();
209 if (buffer.length < dataSize) {
210 buffer = new byte[dataSize];
211 }
212 Key key = null;
213 int bytesRead = data.readEntityData(buffer, 0, dataSize);
214 if (DEBUG) {
215 Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available");
216 }
217 try {
218 key = backupKeyToKey(backupKey);
219 switch (key.type) {
220 case Key.FAVORITE:
221 restoreFavorite(key, buffer, dataSize, keys);
222 break;
223
224 case Key.SCREEN:
225 restoreScreen(key, buffer, dataSize, keys);
226 break;
227
Chris Wren22e130d2013-09-23 18:25:57 -0400228 case Key.ICON:
229 restoreIcon(key, buffer, dataSize, keys);
230 break;
231
Chris Wren1ada10d2013-09-13 18:01:38 -0400232 default:
233 Log.w(TAG, "unknown restore entity type: " + key.type);
234 break;
235 }
236 } catch (KeyParsingException e) {
237 Log.w(TAG, "ignoring unparsable backup key: " + backupKey);
238 }
239 }
240
241 // clear the output journal time, to force a full backup to
242 // will catch any changes the restore process might have made
243 out.t = 0;
244 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
245 writeJournal(newState, out);
246 Log.v(TAG, "onRestore: read " + numRows + " rows");
247 }
248
249 /**
250 * Write all modified favorites to the data stream.
251 *
252 *
253 * @param in notes from last backup
254 * @param data output stream for key/value pairs
255 * @param out notes about this backup
256 * @param keys keys to mark as clean in the notes for next backup
257 * @throws IOException
258 */
259 private void backupFavorites(Journal in, BackupDataOutput data, Journal out,
260 ArrayList<Key> keys)
261 throws IOException {
262 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400263 Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in);
Chris Wren1ada10d2013-09-13 18:01:38 -0400264 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
265
266 // persist things that have changed since the last backup
267 ContentResolver cr = getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400268 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
269 null, null, null);
270 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400271 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400272 cursor.moveToPosition(-1);
273 while(cursor.moveToNext()) {
274 final long id = cursor.getLong(ID_INDEX);
275 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400276 Key key = getKey(Key.FAVORITE, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400277 keys.add(key);
Chris Wren22e130d2013-09-23 18:25:57 -0400278 currentIds.add(keyToBackupKey(key));
279 if (updateTime > in.t) {
280 byte[] blob = packFavorite(cursor);
281 writeRowToBackup(key, blob, out, data);
282 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400283 }
284 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400285 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400286 }
287 if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
288
289 // these IDs must have been deleted
290 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400291 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400292 }
293
294 /**
295 * Read a favorite from the stream.
296 *
297 * <P>Keys arrive in any order, so screens and containers may not exist yet.
298 *
299 * @param key identifier for the row
300 * @param buffer the serialized proto from the stream, may be larger than dataSize
301 * @param dataSize the size of the proto from the stream
302 * @param keys keys to mark as clean in the notes for next backup
303 */
304 private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
305 Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)");
306 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
307 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
308
309 try {
310 Favorite favorite = unpackFavorite(buffer, 0, dataSize);
311 if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType);
312 } catch (InvalidProtocolBufferNanoException e) {
313 Log.w(TAG, "failed to decode proto", e);
314 }
315 }
316
317 /**
318 * Write all modified screens to the data stream.
319 *
320 *
321 * @param in notes from last backup
322 * @param data output stream for key/value pairs
323 * @param out notes about this backup
Chris Wren22e130d2013-09-23 18:25:57 -0400324 * @param keys keys to mark as clean in the notes for next backup
325 * @throws IOException
Chris Wren1ada10d2013-09-13 18:01:38 -0400326 */
327 private void backupScreens(Journal in, BackupDataOutput data, Journal out,
328 ArrayList<Key> keys)
329 throws IOException {
330 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400331 Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in);
332 if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
Chris Wren1ada10d2013-09-13 18:01:38 -0400333
334 // persist things that have changed since the last backup
335 ContentResolver cr = getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400336 Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
337 null, null, null);
338 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400339 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400340 cursor.moveToPosition(-1);
341 while(cursor.moveToNext()) {
342 final long id = cursor.getLong(ID_INDEX);
343 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400344 Key key = getKey(Key.SCREEN, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400345 keys.add(key);
Chris Wren22e130d2013-09-23 18:25:57 -0400346 currentIds.add(keyToBackupKey(key));
347 if (updateTime > in.t) {
348 byte[] blob = packScreen(cursor);
349 writeRowToBackup(key, blob, out, data);
350 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400351 }
352 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400353 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400354 }
355 if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
356
357 // these IDs must have been deleted
358 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400359 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400360 }
361
362 /**
363 * Read a screen from the stream.
364 *
365 * <P>Keys arrive in any order, so children of this screen may already exist.
366 *
367 * @param key identifier for the row
368 * @param buffer the serialized proto from the stream, may be larger than dataSize
369 * @param dataSize the size of the proto from the stream
370 * @param keys keys to mark as clean in the notes for next backup
371 */
372 private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
373 Log.v(TAG, "unpacking screen " + key.id);
374 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
375 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
376 try {
377 Screen screen = unpackScreen(buffer, 0, dataSize);
378 if (DEBUG) Log.d(TAG, "unpacked " + screen.rank);
379 } catch (InvalidProtocolBufferNanoException e) {
380 Log.w(TAG, "failed to decode proto", e);
381 }
382 }
383
Chris Wren22e130d2013-09-23 18:25:57 -0400384 /**
385 * Write all the static icon resources we need to render placeholders
386 * for a package that is not installed.
387 *
388 * @param in notes from last backup
389 * @param data output stream for key/value pairs
390 * @param out notes about this backup
391 * @param keys keys to mark as clean in the notes for next backup
392 * @throws IOException
393 */
394 private void backupIcons(Journal in, BackupDataOutput data, Journal out,
395 ArrayList<Key> keys) throws IOException {
396 // persist icons for new shortcuts since the last backup
397 final ContentResolver cr = getContentResolver();
398 final IconCache iconCache = new IconCache(this);
399 final int dpi = getResources().getDisplayMetrics().densityDpi;
400
401 // read the old ID set
402 Set<String> savedIds = getSavedIdsByType(Key.ICON, in);
403 if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size());
404
405 int startRows = out.rows;
406 if (DEBUG) Log.d(TAG, "starting here: " + startRows);
407 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION;
408 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
409 where, null, null);
410 Set<String> currentIds = new HashSet<String>(cursor.getCount());
411 try {
412 cursor.moveToPosition(-1);
413 while(cursor.moveToNext()) {
414 final long id = cursor.getLong(ID_INDEX);
415 final String intentDescription = cursor.getString(INTENT_INDEX);
416 try {
417 Intent intent = Intent.parseUri(intentDescription, 0);
418 ComponentName cn = intent.getComponent();
419 Key key = null;
420 String backupKey = null;
421 if (cn != null) {
422 key = getKey(Key.ICON, cn.flattenToShortString());
423 backupKey = keyToBackupKey(key);
424 currentIds.add(backupKey);
425 } else {
426 Log.w(TAG, "empty intent on application favorite: " + id);
427 }
428 if (savedIds.contains(backupKey)) {
429 if (DEBUG) Log.d(TAG, "already saved icon " + backupKey);
430
431 // remember that we already backed this up previously
432 keys.add(key);
433 } else if (backupKey != null) {
434 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
435 if ((out.rows - startRows) < MAX_ICONS_PER_PASS) {
436 if (DEBUG) Log.d(TAG, "saving icon " + backupKey);
437 Bitmap icon = iconCache.getIcon(intent);
438 keys.add(key);
439 if (icon != null && !iconCache.isDefaultIcon(icon)) {
440 byte[] blob = packIcon(dpi, icon);
441 writeRowToBackup(key, blob, out, data);
442 }
443 } else {
444 if (DEBUG) Log.d(TAG, "scheduling another rtun for icon " + backupKey);
445 // too many icons for this pass, request another.
446 dataChanged(this);
447 }
448 }
449 } catch (URISyntaxException e) {
450 Log.w(TAG, "invalid URI on application favorite: " + id);
451 } catch (IOException e) {
452 Log.w(TAG, "unable to save application icon for favorite: " + id);
453 }
454
455 }
456 } finally {
457 cursor.close();
458 }
459 if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size());
460
461 // these IDs must have been deleted
462 savedIds.removeAll(currentIds);
463 out.rows += removeDeletedKeysFromBackup(savedIds, data);
464 }
465
466 /**
467 * Read an icon from the stream.
468 *
469 * <P>Keys arrive in any order, so shortcuts that use this screen may already exist.
470 *
471 * @param key identifier for the row
472 * @param buffer the serialized proto from the stream, may be larger than dataSize
473 * @param dataSize the size of the proto from the stream
474 * @param keys keys to mark as clean in the notes for next backup
475 */
476 private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
477 Log.v(TAG, "unpacking icon " + key.id);
478 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
479 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
480 try {
481 Resource res = unpackIcon(buffer, 0, dataSize);
482 if (DEBUG) Log.d(TAG, "unpacked " + res.dpi);
483 if (DEBUG) Log.d(TAG, "read " +
484 Base64.encodeToString(res.data, 0, res.data.length,
485 Base64.NO_WRAP));
486 Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length);
487 if (icon == null) {
488 Log.w(TAG, "failed to unpack icon for " + key.name);
489 }
490 } catch (InvalidProtocolBufferNanoException e) {
491 Log.w(TAG, "failed to decode proto", e);
492 }
493 }
494
495 /** create a new key, with an integer ID.
Chris Wren1ada10d2013-09-13 18:01:38 -0400496 *
497 * <P> Keys contain their own checksum instead of using
498 * the heavy-weight CheckedMessage wrapper.
499 */
500 private Key getKey(int type, long id) {
501 Key key = new Key();
502 key.type = type;
503 key.id = id;
504 key.checksum = checkKey(key);
505 return key;
506 }
507
Chris Wren22e130d2013-09-23 18:25:57 -0400508 /** create a new key for a named object.
509 *
510 * <P> Keys contain their own checksum instead of using
511 * the heavy-weight CheckedMessage wrapper.
512 */
513 private Key getKey(int type, String name) {
514 Key key = new Key();
515 key.type = type;
516 key.name = name;
517 key.checksum = checkKey(key);
518 return key;
519 }
520
Chris Wren1ada10d2013-09-13 18:01:38 -0400521 /** keys need to be strings, serialize and encode. */
522 private String keyToBackupKey(Key key) {
523 return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP | Base64.NO_PADDING);
524 }
525
526 /** keys need to be strings, decode and parse. */
527 private Key backupKeyToKey(String backupKey) throws KeyParsingException {
528 try {
529 Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT));
530 if (key.checksum != checkKey(key)) {
531 key = null;
532 throw new KeyParsingException("invalid key read from stream" + backupKey);
533 }
534 return key;
535 } catch (InvalidProtocolBufferNanoException e) {
536 throw new KeyParsingException(e);
537 } catch (IllegalArgumentException e) {
538 throw new KeyParsingException(e);
539 }
540 }
541
Chris Wren22e130d2013-09-23 18:25:57 -0400542 private String getKeyName(Key key) {
543 if (TextUtils.isEmpty(key.name)) {
544 return Long.toString(key.id);
545 } else {
546 return key.name;
547 }
548
549 }
550
551 private String geKeyType(Key key) {
552 switch (key.type) {
553 case Key.FAVORITE:
554 return "favorite";
555 case Key.SCREEN:
556 return "screen";
557 case Key.ICON:
558 return "icon";
559 default:
560 return "anonymous";
561 }
562 }
563
Chris Wren1ada10d2013-09-13 18:01:38 -0400564 /** Compute the checksum over the important bits of a key. */
565 private long checkKey(Key key) {
566 CRC32 checksum = new CRC32();
567 checksum.update(key.type);
568 checksum.update((int) (key.id & 0xffff));
569 checksum.update((int) ((key.id >> 32) & 0xffff));
570 if (!TextUtils.isEmpty(key.name)) {
571 checksum.update(key.name.getBytes());
572 }
573 return checksum.getValue();
574 }
575
576 /** Serialize a Favorite for persistence, including a checksum wrapper. */
577 private byte[] packFavorite(Cursor c) {
578 Favorite favorite = new Favorite();
579 favorite.id = c.getLong(ID_INDEX);
580 favorite.screen = c.getInt(SCREEN_INDEX);
581 favorite.container = c.getInt(CONTAINER_INDEX);
582 favorite.cellX = c.getInt(CELLX_INDEX);
583 favorite.cellY = c.getInt(CELLY_INDEX);
584 favorite.spanX = c.getInt(SPANX_INDEX);
585 favorite.spanY = c.getInt(SPANY_INDEX);
586 favorite.iconType = c.getInt(ICON_TYPE_INDEX);
587 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
588 String iconPackage = c.getString(ICON_PACKAGE_INDEX);
589 if (!TextUtils.isEmpty(iconPackage)) {
590 favorite.iconPackage = iconPackage;
591 }
592 String iconResource = c.getString(ICON_RESOURCE_INDEX);
593 if (!TextUtils.isEmpty(iconResource)) {
594 favorite.iconResource = iconResource;
595 }
596 }
597 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
598 byte[] blob = c.getBlob(ICON_INDEX);
599 if (blob != null && blob.length > 0) {
600 favorite.icon = blob;
601 }
602 }
603 String title = c.getString(TITLE_INDEX);
604 if (!TextUtils.isEmpty(title)) {
605 favorite.title = title;
606 }
607 String intent = c.getString(INTENT_INDEX);
608 if (!TextUtils.isEmpty(intent)) {
609 favorite.intent = intent;
610 }
611 favorite.itemType = c.getInt(ITEM_TYPE_INDEX);
612 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
613 favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX);
614 String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX);
615 if (!TextUtils.isEmpty(appWidgetProvider)) {
616 favorite.appWidgetProvider = appWidgetProvider;
617 }
618 }
619
620 return writeCheckedBytes(favorite);
621 }
622
623 /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */
624 private Favorite unpackFavorite(byte[] buffer, int offset, int dataSize)
625 throws InvalidProtocolBufferNanoException {
626 Favorite favorite = new Favorite();
627 MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize));
628 return favorite;
629 }
630
631 /** Serialize a Screen for persistence, including a checksum wrapper. */
632 private byte[] packScreen(Cursor c) {
633 Screen screen = new Screen();
634 screen.id = c.getLong(ID_INDEX);
635 screen.rank = c.getInt(SCREEN_RANK_INDEX);
636
637 return writeCheckedBytes(screen);
638 }
639
640 /** Deserialize a Screen from persistence, after verifying checksum wrapper. */
641 private Screen unpackScreen(byte[] buffer, int offset, int dataSize)
642 throws InvalidProtocolBufferNanoException {
643 Screen screen = new Screen();
644 MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize));
645 return screen;
646 }
647
Chris Wren22e130d2013-09-23 18:25:57 -0400648 /** Serialize an icon Resource for persistence, including a checksum wrapper. */
649 private byte[] packIcon(int dpi, Bitmap icon) {
650 Resource res = new Resource();
651 res.dpi = dpi;
652 ByteArrayOutputStream os = new ByteArrayOutputStream();
653 if (icon.compress(WEBP, 100, os)) {
654 res.data = os.toByteArray();
655 }
656 return writeCheckedBytes(res);
657 }
658
659 /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */
660 private Resource unpackIcon(byte[] buffer, int offset, int dataSize)
661 throws InvalidProtocolBufferNanoException {
662 Resource res = new Resource();
663 MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize));
664 return res;
665 }
666
Chris Wren1ada10d2013-09-13 18:01:38 -0400667 /**
668 * Read the old journal from the input file.
669 *
670 * In the event of any error, just pretend we didn't have a journal,
671 * in that case, do a full backup.
672 *
673 * @param oldState the read-0only file descriptor pointing to the old journal
674 * @return a Journal protocol bugffer
675 */
676 private Journal readJournal(ParcelFileDescriptor oldState) {
677 int fileSize = (int) oldState.getStatSize();
678 int remaining = fileSize;
679 byte[] buffer = null;
680 Journal journal = new Journal();
681 if (remaining < MAX_JOURNAL_SIZE) {
682 FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor());
683 int offset = 0;
684
685 buffer = new byte[remaining];
686 while (remaining > 0) {
687 int bytesRead = 0;
688 try {
689 bytesRead = inStream.read(buffer, offset, remaining);
690 } catch (IOException e) {
691 Log.w(TAG, "failed to read the journal", e);
692 buffer = null;
693 remaining = 0;
694 }
695 if (bytesRead > 0) {
696 remaining -= bytesRead;
697 } else {
698 // act like there is not journal
699 Log.w(TAG, "failed to read the journal");
700 buffer = null;
701 remaining = 0;
702 }
703 }
704
705 if (buffer != null) {
706 try {
707 MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, fileSize));
708 } catch (InvalidProtocolBufferNanoException e) {
709 Log.d(TAG, "failed to read the journal", e);
710 journal.clear();
711 }
712 }
713
714 try {
715 inStream.close();
716 } catch (IOException e) {
717 Log.d(TAG, "failed to close the journal", e);
718 }
719 }
720 return journal;
721 }
722
Chris Wren22e130d2013-09-23 18:25:57 -0400723 private void writeRowToBackup(Key key, byte[] blob, Journal out,
724 BackupDataOutput data) throws IOException {
725 String backupKey = keyToBackupKey(key);
726 data.writeEntityHeader(backupKey, blob.length);
727 data.writeEntityData(blob, blob.length);
728 out.rows++;
729 out.bytes += blob.length;
730 Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " +
731 getKeyName(key) + "/" + blob.length);
732 if(DEBUG) Log.d(TAG, "wrote " +
733 Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
734 }
735
736 private Set<String> getSavedIdsByType(int type, Journal in) {
737 Set<String> savedIds = new HashSet<String>();
738 for(int i = 0; i < in.key.length; i++) {
739 Key key = in.key[i];
740 if (key.type == type) {
741 savedIds.add(keyToBackupKey(key));
742 }
743 }
744 return savedIds;
745 }
746
747 private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data)
748 throws IOException {
749 int rows = 0;
750 for(String deleted: deletedIds) {
751 Log.v(TAG, "dropping icon " + deleted);
752 data.writeEntityHeader(deleted, -1);
753 rows++;
754 }
755 return rows;
756 }
757
Chris Wren1ada10d2013-09-13 18:01:38 -0400758 /**
759 * Write the new journal to the output file.
760 *
761 * In the event of any error, just pretend we didn't have a journal,
762 * in that case, do a full backup.
763
764 * @param newState the write-only file descriptor pointing to the new journal
765 * @param journal a Journal protocol buffer
766 */
767 private void writeJournal(ParcelFileDescriptor newState, Journal journal) {
768 FileOutputStream outStream = null;
769 try {
770 outStream = new FileOutputStream(newState.getFileDescriptor());
771 outStream.write(writeCheckedBytes(journal));
772 outStream.close();
773 } catch (IOException e) {
774 Log.d(TAG, "failed to write backup journal", e);
775 }
776 }
777
778 /** Wrap a proto in a CheckedMessage and compute the checksum. */
779 private byte[] writeCheckedBytes(MessageNano proto) {
780 CheckedMessage wrapper = new CheckedMessage();
781 wrapper.payload = MessageNano.toByteArray(proto);
782 CRC32 checksum = new CRC32();
783 checksum.update(wrapper.payload);
784 wrapper.checksum = checksum.getValue();
785 return MessageNano.toByteArray(wrapper);
786 }
787
788 /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */
789 private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize)
790 throws InvalidProtocolBufferNanoException {
791 CheckedMessage wrapper = new CheckedMessage();
792 MessageNano.mergeFrom(wrapper, buffer, offset, dataSize);
793 CRC32 checksum = new CRC32();
794 checksum.update(wrapper.payload);
795 if (wrapper.checksum != checksum.getValue()) {
796 throw new InvalidProtocolBufferNanoException("checksum does not match");
797 }
798 return wrapper.payload;
799 }
800
801 private class KeyParsingException extends Throwable {
802 private KeyParsingException(Throwable cause) {
803 super(cause);
804 }
805
806 public KeyParsingException(String reason) {
807 super(reason);
808 }
809 }
810}