1 /* 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 17 package com.android.launcher3; 18 19 import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 20 import com.google.protobuf.nano.MessageNano; 21 22 import com.android.launcher3.LauncherSettings.Favorites; 23 import com.android.launcher3.LauncherSettings.WorkspaceScreens; 24 import com.android.launcher3.backup.BackupProtos; 25 import com.android.launcher3.backup.BackupProtos.CheckedMessage; 26 import com.android.launcher3.backup.BackupProtos.Favorite; 27 import com.android.launcher3.backup.BackupProtos.Journal; 28 import com.android.launcher3.backup.BackupProtos.Key; 29 import com.android.launcher3.backup.BackupProtos.Resource; 30 import com.android.launcher3.backup.BackupProtos.Screen; 31 import com.android.launcher3.backup.BackupProtos.Widget; 32 33 import android.app.backup.BackupDataInputStream; 34 import android.app.backup.BackupHelper; 35 import android.app.backup.BackupDataInput; 36 import android.app.backup.BackupDataOutput; 37 import android.app.backup.BackupManager; 38 import android.appwidget.AppWidgetManager; 39 import android.appwidget.AppWidgetProviderInfo; 40 import android.content.ComponentName; 41 import android.content.ContentResolver; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.database.Cursor; 45 import android.graphics.Bitmap; 46 import android.graphics.BitmapFactory; 47 import android.graphics.drawable.Drawable; 48 import android.os.ParcelFileDescriptor; 49 import android.text.TextUtils; 50 import android.util.Base64; 51 import android.util.Log; 52 53 import java.io.ByteArrayOutputStream; 54 import java.io.FileInputStream; 55 import java.io.FileOutputStream; 56 import java.io.IOException; 57 import java.net.URISyntaxException; 58 import java.util.ArrayList; 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Set; 63 import java.util.zip.CRC32; 64 65 /** 66 * Persist the launcher home state across calamities. 67 */ 68 public class LauncherBackupHelper implements BackupHelper { 69 70 private static final String TAG = "LauncherBackupHelper"; 71 private static final boolean DEBUG = false; 72 private static final boolean DEBUG_PAYLOAD = false; 73 74 private static final int MAX_JOURNAL_SIZE = 1000000; 75 76 /** icons are large, dribble them out */ 77 private static final int MAX_ICONS_PER_PASS = 10; 78 79 /** 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 84 public static final String LAUNCHER_PREFIX = "L"; 85 86 private static final Bitmap.CompressFormat IMAGE_FORMAT = 87 android.graphics.Bitmap.CompressFormat.PNG; 88 89 private static BackupManager sBackupManager; 90 91 private static final String[] FAVORITE_PROJECTION = { 92 Favorites._ID, // 0 93 Favorites.MODIFIED, // 1 94 Favorites.INTENT, // 2 95 Favorites.APPWIDGET_PROVIDER, // 3 96 Favorites.APPWIDGET_ID, // 4 97 Favorites.CELLX, // 5 98 Favorites.CELLY, // 6 99 Favorites.CONTAINER, // 7 100 Favorites.ICON, // 8 101 Favorites.ICON_PACKAGE, // 9 102 Favorites.ICON_RESOURCE, // 10 103 Favorites.ICON_TYPE, // 11 104 Favorites.ITEM_TYPE, // 12 105 Favorites.SCREEN, // 13 106 Favorites.SPANX, // 14 107 Favorites.SPANY, // 15 108 Favorites.TITLE, // 16 109 }; 110 111 private static final int ID_INDEX = 0; 112 private static final int ID_MODIFIED = 1; 113 private static final int INTENT_INDEX = 2; 114 private static final int APPWIDGET_PROVIDER_INDEX = 3; 115 private static final int APPWIDGET_ID_INDEX = 4; 116 private static final int CELLX_INDEX = 5; 117 private static final int CELLY_INDEX = 6; 118 private static final int CONTAINER_INDEX = 7; 119 private static final int ICON_INDEX = 8; 120 private static final int ICON_PACKAGE_INDEX = 9; 121 private static final int ICON_RESOURCE_INDEX = 10; 122 private static final int ICON_TYPE_INDEX = 11; 123 private static final int ITEM_TYPE_INDEX = 12; 124 private static final int SCREEN_INDEX = 13; 125 private static final int SPANX_INDEX = 14; 126 private static final int SPANY_INDEX = 15; 127 private static final int TITLE_INDEX = 16; 128 129 private static final String[] SCREEN_PROJECTION = { 130 WorkspaceScreens._ID, // 0 131 WorkspaceScreens.MODIFIED, // 1 132 WorkspaceScreens.SCREEN_RANK // 2 133 }; 134 135 private static final int SCREEN_RANK_INDEX = 2; 136 137 private final Context mContext; 138 139 private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap; 140 141 private ArrayList<Key> mKeys; 142 143 public LauncherBackupHelper(Context context) { 144 mContext = context; 145 } 146 147 private void dataChanged() { 148 if (sBackupManager == null) { 149 sBackupManager = new BackupManager(mContext); 150 } 151 sBackupManager.dataChanged(); 152 } 153 154 /** 155 * Back up launcher data so we can restore the user's state on a new device. 156 * 157 * <P>The journal is a timestamp and a list of keys that were saved as of that time. 158 * 159 * <P>Keys may come back in any order, so each key/value is one complete row of the database. 160 * 161 * @param oldState notes from the last backup 162 * @param data incremental key/value pairs to persist off-device 163 * @param newState notes for the next backup 164 * @throws IOException 165 */ 166 @Override 167 public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 168 ParcelFileDescriptor newState) { 169 Log.v(TAG, "onBackup"); 170 171 Journal in = readJournal(oldState); 172 Journal out = new Journal(); 173 174 long lastBackupTime = in.t; 175 out.t = System.currentTimeMillis(); 176 out.rows = 0; 177 out.bytes = 0; 178 179 Log.v(TAG, "lastBackupTime=" + lastBackupTime); 180 181 ArrayList<Key> keys = new ArrayList<Key>(); 182 try { 183 backupFavorites(in, data, out, keys); 184 backupScreens(in, data, out, keys); 185 backupIcons(in, data, out, keys); 186 backupWidgets(in, data, out, keys); 187 } catch (IOException e) { 188 Log.e(TAG, "launcher backup has failed", e); 189 } 190 191 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY); 192 writeJournal(newState, out); 193 Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows."); 194 } 195 196 /** 197 * Restore launcher configuration from the restored data stream. 198 * 199 * <P>Keys may arrive in any order. 200 * 201 * @param data the key/value pair from the server 202 */ 203 @Override 204 public void restoreEntity(BackupDataInputStream data) { 205 Log.v(TAG, "restoreEntity"); 206 if (mKeys == null) { 207 mKeys = new ArrayList<Key>(); 208 } 209 byte[] buffer = new byte[512]; 210 String backupKey = data.getKey(); 211 int dataSize = data.size(); 212 if (buffer.length < dataSize) { 213 buffer = new byte[dataSize]; 214 } 215 Key key = null; 216 int bytesRead = 0; 217 try { 218 bytesRead = data.read(buffer, 0, dataSize); 219 if (DEBUG) Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available"); 220 } catch (IOException e) { 221 Log.d(TAG, "failed to read entity from restore data", e); 222 } 223 try { 224 key = backupKeyToKey(backupKey); 225 switch (key.type) { 226 case Key.FAVORITE: 227 restoreFavorite(key, buffer, dataSize, mKeys); 228 break; 229 230 case Key.SCREEN: 231 restoreScreen(key, buffer, dataSize, mKeys); 232 break; 233 234 case Key.ICON: 235 restoreIcon(key, buffer, dataSize, mKeys); 236 break; 237 238 case Key.WIDGET: 239 restoreWidget(key, buffer, dataSize, mKeys); 240 break; 241 242 default: 243 Log.w(TAG, "unknown restore entity type: " + key.type); 244 break; 245 } 246 } catch (KeyParsingException e) { 247 Log.w(TAG, "ignoring unparsable backup key: " + backupKey); 248 } 249 250 } 251 252 /** 253 * Record the restore state for the next backup. 254 * 255 * @param newState notes about the backup state after restore. 256 */ 257 @Override 258 public void writeNewStateDescription(ParcelFileDescriptor newState) { 259 // clear the output journal time, to force a full backup to 260 // will catch any changes the restore process might have made 261 Journal out = new Journal(); 262 out.t = 0; 263 out.key = mKeys.toArray(BackupProtos.Key.EMPTY_ARRAY); 264 writeJournal(newState, out); 265 Log.v(TAG, "onRestore: read " + mKeys.size() + " rows"); 266 mKeys.clear(); 267 } 268 269 /** 270 * Write all modified favorites to the data stream. 271 * 272 * 273 * @param in notes from last backup 274 * @param data output stream for key/value pairs 275 * @param out notes about this backup 276 * @param keys keys to mark as clean in the notes for next backup 277 * @throws IOException 278 */ 279 private void backupFavorites(Journal in, BackupDataOutput data, Journal out, 280 ArrayList<Key> keys) 281 throws IOException { 282 // read the old ID set 283 Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in); 284 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size()); 285 286 // persist things that have changed since the last backup 287 ContentResolver cr = mContext.getContentResolver(); 288 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, 289 null, null, null); 290 Set<String> currentIds = new HashSet<String>(cursor.getCount()); 291 try { 292 cursor.moveToPosition(-1); 293 while(cursor.moveToNext()) { 294 final long id = cursor.getLong(ID_INDEX); 295 final long updateTime = cursor.getLong(ID_MODIFIED); 296 Key key = getKey(Key.FAVORITE, id); 297 keys.add(key); 298 currentIds.add(keyToBackupKey(key)); 299 if (updateTime > in.t) { 300 byte[] blob = packFavorite(cursor); 301 writeRowToBackup(key, blob, out, data); 302 } 303 } 304 } finally { 305 cursor.close(); 306 } 307 if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size()); 308 309 // these IDs must have been deleted 310 savedIds.removeAll(currentIds); 311 out.rows += removeDeletedKeysFromBackup(savedIds, data); 312 } 313 314 /** 315 * Read a favorite from the stream. 316 * 317 * <P>Keys arrive in any order, so screens and containers may not exist yet. 318 * 319 * @param key identifier for the row 320 * @param buffer the serialized proto from the stream, may be larger than dataSize 321 * @param dataSize the size of the proto from the stream 322 * @param keys keys to mark as clean in the notes for next backup 323 */ 324 private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) { 325 Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)"); 326 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + 327 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); 328 329 try { 330 Favorite favorite = unpackFavorite(buffer, 0, dataSize); 331 if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType); 332 } catch (InvalidProtocolBufferNanoException e) { 333 Log.w(TAG, "failed to decode proto", e); 334 } 335 } 336 337 /** 338 * Write all modified screens to the data stream. 339 * 340 * 341 * @param in notes from last backup 342 * @param data output stream for key/value pairs 343 * @param out notes about this backup 344 * @param keys keys to mark as clean in the notes for next backup 345 * @throws IOException 346 */ 347 private void backupScreens(Journal in, BackupDataOutput data, Journal out, 348 ArrayList<Key> keys) 349 throws IOException { 350 // read the old ID set 351 Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in); 352 if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size()); 353 354 // persist things that have changed since the last backup 355 ContentResolver cr = mContext.getContentResolver(); 356 Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION, 357 null, null, null); 358 Set<String> currentIds = new HashSet<String>(cursor.getCount()); 359 try { 360 cursor.moveToPosition(-1); 361 while(cursor.moveToNext()) { 362 final long id = cursor.getLong(ID_INDEX); 363 final long updateTime = cursor.getLong(ID_MODIFIED); 364 Key key = getKey(Key.SCREEN, id); 365 keys.add(key); 366 currentIds.add(keyToBackupKey(key)); 367 if (updateTime > in.t) { 368 byte[] blob = packScreen(cursor); 369 writeRowToBackup(key, blob, out, data); 370 } 371 } 372 } finally { 373 cursor.close(); 374 } 375 if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size()); 376 377 // these IDs must have been deleted 378 savedIds.removeAll(currentIds); 379 out.rows += removeDeletedKeysFromBackup(savedIds, data); 380 } 381 382 /** 383 * Read a screen from the stream. 384 * 385 * <P>Keys arrive in any order, so children of this screen may already exist. 386 * 387 * @param key identifier for the row 388 * @param buffer the serialized proto from the stream, may be larger than dataSize 389 * @param dataSize the size of the proto from the stream 390 * @param keys keys to mark as clean in the notes for next backup 391 */ 392 private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) { 393 Log.v(TAG, "unpacking screen " + key.id); 394 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + 395 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); 396 try { 397 Screen screen = unpackScreen(buffer, 0, dataSize); 398 if (DEBUG) Log.d(TAG, "unpacked " + screen.rank); 399 } catch (InvalidProtocolBufferNanoException e) { 400 Log.w(TAG, "failed to decode proto", e); 401 } 402 } 403 404 /** 405 * Write all the static icon resources we need to render placeholders 406 * for a package that is not installed. 407 * 408 * @param in notes from last backup 409 * @param data output stream for key/value pairs 410 * @param out notes about this backup 411 * @param keys keys to mark as clean in the notes for next backup 412 * @throws IOException 413 */ 414 private void backupIcons(Journal in, BackupDataOutput data, Journal out, 415 ArrayList<Key> keys) throws IOException { 416 // persist icons that haven't been persisted yet 417 final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); 418 if (appState == null) { 419 dataChanged(); // try again later 420 if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying icon backup"); 421 return; 422 } 423 final ContentResolver cr = mContext.getContentResolver(); 424 final IconCache iconCache = appState.getIconCache(); 425 final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; 426 427 // read the old ID set 428 Set<String> savedIds = getSavedIdsByType(Key.ICON, in); 429 if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size()); 430 431 int startRows = out.rows; 432 if (DEBUG) Log.d(TAG, "starting here: " + startRows); 433 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION; 434 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, 435 where, null, null); 436 Set<String> currentIds = new HashSet<String>(cursor.getCount()); 437 try { 438 cursor.moveToPosition(-1); 439 while(cursor.moveToNext()) { 440 final long id = cursor.getLong(ID_INDEX); 441 final String intentDescription = cursor.getString(INTENT_INDEX); 442 try { 443 Intent intent = Intent.parseUri(intentDescription, 0); 444 ComponentName cn = intent.getComponent(); 445 Key key = null; 446 String backupKey = null; 447 if (cn != null) { 448 key = getKey(Key.ICON, cn.flattenToShortString()); 449 backupKey = keyToBackupKey(key); 450 currentIds.add(backupKey); 451 } else { 452 Log.w(TAG, "empty intent on application favorite: " + id); 453 } 454 if (savedIds.contains(backupKey)) { 455 if (DEBUG) Log.d(TAG, "already saved icon " + backupKey); 456 457 // remember that we already backed this up previously 458 keys.add(key); 459 } else if (backupKey != null) { 460 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows); 461 if ((out.rows - startRows) < MAX_ICONS_PER_PASS) { 462 if (DEBUG) Log.d(TAG, "saving icon " + backupKey); 463 Bitmap icon = iconCache.getIcon(intent); 464 keys.add(key); 465 if (icon != null && !iconCache.isDefaultIcon(icon)) { 466 byte[] blob = packIcon(dpi, icon); 467 writeRowToBackup(key, blob, out, data); 468 } 469 } else { 470 if (DEBUG) Log.d(TAG, "scheduling another run for icon " + backupKey); 471 // too many icons for this pass, request another. 472 dataChanged(); 473 } 474 } 475 } catch (URISyntaxException e) { 476 Log.w(TAG, "invalid URI on application favorite: " + id); 477 } catch (IOException e) { 478 Log.w(TAG, "unable to save application icon for favorite: " + id); 479 } 480 481 } 482 } finally { 483 cursor.close(); 484 } 485 if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size()); 486 487 // these IDs must have been deleted 488 savedIds.removeAll(currentIds); 489 out.rows += removeDeletedKeysFromBackup(savedIds, data); 490 } 491 492 /** 493 * Read an icon from the stream. 494 * 495 * <P>Keys arrive in any order, so shortcuts that use this icon may already exist. 496 * 497 * @param key identifier for the row 498 * @param buffer the serialized proto from the stream, may be larger than dataSize 499 * @param dataSize the size of the proto from the stream 500 * @param keys keys to mark as clean in the notes for next backup 501 */ 502 private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) { 503 Log.v(TAG, "unpacking icon " + key.id); 504 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + 505 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); 506 try { 507 Resource res = unpackIcon(buffer, 0, dataSize); 508 if (DEBUG) Log.d(TAG, "unpacked " + res.dpi); 509 if (DEBUG) Log.d(TAG, "read " + 510 Base64.encodeToString(res.data, 0, res.data.length, 511 Base64.NO_WRAP)); 512 Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length); 513 if (icon == null) { 514 Log.w(TAG, "failed to unpack icon for " + key.name); 515 } 516 } catch (InvalidProtocolBufferNanoException e) { 517 Log.w(TAG, "failed to decode proto", e); 518 } 519 } 520 521 /** 522 * Write all the static widget resources we need to render placeholders 523 * for a package that is not installed. 524 * 525 * @param in notes from last backup 526 * @param data output stream for key/value pairs 527 * @param out notes about this backup 528 * @param keys keys to mark as clean in the notes for next backup 529 * @throws IOException 530 */ 531 private void backupWidgets(Journal in, BackupDataOutput data, Journal out, 532 ArrayList<Key> keys) throws IOException { 533 // persist static widget info that hasn't been persisted yet 534 final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); 535 if (appState == null) { 536 dataChanged(); // try again later 537 if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying widget backup"); 538 return; 539 } 540 final ContentResolver cr = mContext.getContentResolver(); 541 final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext); 542 final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext); 543 final IconCache iconCache = appState.getIconCache(); 544 final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; 545 final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile(); 546 if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx); 547 548 // read the old ID set 549 Set<String> savedIds = getSavedIdsByType(Key.WIDGET, in); 550 if (DEBUG) Log.d(TAG, "widgets savedIds.size()=" + savedIds.size()); 551 552 int startRows = out.rows; 553 if (DEBUG) Log.d(TAG, "starting here: " + startRows); 554 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET; 555 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, 556 where, null, null); 557 Set<String> currentIds = new HashSet<String>(cursor.getCount()); 558 try { 559 cursor.moveToPosition(-1); 560 while(cursor.moveToNext()) { 561 final long id = cursor.getLong(ID_INDEX); 562 final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); 563 final int spanX = cursor.getInt(SPANX_INDEX); 564 final int spanY = cursor.getInt(SPANY_INDEX); 565 final ComponentName provider = ComponentName.unflattenFromString(providerName); 566 Key key = null; 567 String backupKey = null; 568 if (provider != null) { 569 key = getKey(Key.WIDGET, providerName); 570 backupKey = keyToBackupKey(key); 571 currentIds.add(backupKey); 572 } else { 573 Log.w(TAG, "empty intent on appwidget: " + id); 574 } 575 if (savedIds.contains(backupKey)) { 576 if (DEBUG) Log.d(TAG, "already saved widget " + backupKey); 577 578 // remember that we already backed this up previously 579 keys.add(key); 580 } else if (backupKey != null) { 581 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows); 582 if ((out.rows - startRows) < MAX_WIDGETS_PER_PASS) { 583 if (DEBUG) Log.d(TAG, "saving widget " + backupKey); 584 previewLoader.setPreviewSize(spanX * profile.cellWidthPx, 585 spanY * profile.cellHeightPx, widgetSpacingLayout); 586 byte[] blob = packWidget(dpi, previewLoader, iconCache, provider); 587 keys.add(key); 588 writeRowToBackup(key, blob, out, data); 589 590 } else { 591 if (DEBUG) Log.d(TAG, "scheduling another run for widget " + backupKey); 592 // too many widgets for this pass, request another. 593 dataChanged(); 594 } 595 } 596 } 597 } finally { 598 cursor.close(); 599 } 600 if (DEBUG) Log.d(TAG, "widget currentIds.size()=" + currentIds.size()); 601 602 // these IDs must have been deleted 603 savedIds.removeAll(currentIds); 604 out.rows += removeDeletedKeysFromBackup(savedIds, data); 605 } 606 607 /** 608 * Read a widget from the stream. 609 * 610 * <P>Keys arrive in any order, so widgets that use this data may already exist. 611 * 612 * @param key identifier for the row 613 * @param buffer the serialized proto from the stream, may be larger than dataSize 614 * @param dataSize the size of the proto from the stream 615 * @param keys keys to mark as clean in the notes for next backup 616 */ 617 private void restoreWidget(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) { 618 Log.v(TAG, "unpacking widget " + key.id); 619 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + 620 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); 621 try { 622 Widget widget = unpackWidget(buffer, 0, dataSize); 623 if (DEBUG) Log.d(TAG, "unpacked " + widget.provider); 624 if (widget.icon.data != null) { 625 Bitmap icon = BitmapFactory 626 .decodeByteArray(widget.icon.data, 0, widget.icon.data.length); 627 if (icon == null) { 628 Log.w(TAG, "failed to unpack widget icon for " + key.name); 629 } 630 } 631 } catch (InvalidProtocolBufferNanoException e) { 632 Log.w(TAG, "failed to decode proto", e); 633 } 634 } 635 636 /** create a new key, with an integer ID. 637 * 638 * <P> Keys contain their own checksum instead of using 639 * the heavy-weight CheckedMessage wrapper. 640 */ 641 private Key getKey(int type, long id) { 642 Key key = new Key(); 643 key.type = type; 644 key.id = id; 645 key.checksum = checkKey(key); 646 return key; 647 } 648 649 /** create a new key for a named object. 650 * 651 * <P> Keys contain their own checksum instead of using 652 * the heavy-weight CheckedMessage wrapper. 653 */ 654 private Key getKey(int type, String name) { 655 Key key = new Key(); 656 key.type = type; 657 key.name = name; 658 key.checksum = checkKey(key); 659 return key; 660 } 661 662 /** keys need to be strings, serialize and encode. */ 663 private String keyToBackupKey(Key key) { 664 return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP); 665 } 666 667 /** keys need to be strings, decode and parse. */ 668 private Key backupKeyToKey(String backupKey) throws KeyParsingException { 669 try { 670 Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT)); 671 if (key.checksum != checkKey(key)) { 672 key = null; 673 throw new KeyParsingException("invalid key read from stream" + backupKey); 674 } 675 return key; 676 } catch (InvalidProtocolBufferNanoException e) { 677 throw new KeyParsingException(e); 678 } catch (IllegalArgumentException e) { 679 throw new KeyParsingException(e); 680 } 681 } 682 683 private String getKeyName(Key key) { 684 if (TextUtils.isEmpty(key.name)) { 685 return Long.toString(key.id); 686 } else { 687 return key.name; 688 } 689 690 } 691 692 private String geKeyType(Key key) { 693 switch (key.type) { 694 case Key.FAVORITE: 695 return "favorite"; 696 case Key.SCREEN: 697 return "screen"; 698 case Key.ICON: 699 return "icon"; 700 case Key.WIDGET: 701 return "widget"; 702 default: 703 return "anonymous"; 704 } 705 } 706 707 /** Compute the checksum over the important bits of a key. */ 708 private long checkKey(Key key) { 709 CRC32 checksum = new CRC32(); 710 checksum.update(key.type); 711 checksum.update((int) (key.id & 0xffff)); 712 checksum.update((int) ((key.id >> 32) & 0xffff)); 713 if (!TextUtils.isEmpty(key.name)) { 714 checksum.update(key.name.getBytes()); 715 } 716 return checksum.getValue(); 717 } 718 719 /** Serialize a Favorite for persistence, including a checksum wrapper. */ 720 private byte[] packFavorite(Cursor c) { 721 Favorite favorite = new Favorite(); 722 favorite.id = c.getLong(ID_INDEX); 723 favorite.screen = c.getInt(SCREEN_INDEX); 724 favorite.container = c.getInt(CONTAINER_INDEX); 725 favorite.cellX = c.getInt(CELLX_INDEX); 726 favorite.cellY = c.getInt(CELLY_INDEX); 727 favorite.spanX = c.getInt(SPANX_INDEX); 728 favorite.spanY = c.getInt(SPANY_INDEX); 729 favorite.iconType = c.getInt(ICON_TYPE_INDEX); 730 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { 731 String iconPackage = c.getString(ICON_PACKAGE_INDEX); 732 if (!TextUtils.isEmpty(iconPackage)) { 733 favorite.iconPackage = iconPackage; 734 } 735 String iconResource = c.getString(ICON_RESOURCE_INDEX); 736 if (!TextUtils.isEmpty(iconResource)) { 737 favorite.iconResource = iconResource; 738 } 739 } 740 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) { 741 byte[] blob = c.getBlob(ICON_INDEX); 742 if (blob != null && blob.length > 0) { 743 favorite.icon = blob; 744 } 745 } 746 String title = c.getString(TITLE_INDEX); 747 if (!TextUtils.isEmpty(title)) { 748 favorite.title = title; 749 } 750 String intent = c.getString(INTENT_INDEX); 751 if (!TextUtils.isEmpty(intent)) { 752 favorite.intent = intent; 753 } 754 favorite.itemType = c.getInt(ITEM_TYPE_INDEX); 755 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { 756 favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX); 757 String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX); 758 if (!TextUtils.isEmpty(appWidgetProvider)) { 759 favorite.appWidgetProvider = appWidgetProvider; 760 } 761 } 762 763 return writeCheckedBytes(favorite); 764 } 765 766 /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */ 767 private Favorite unpackFavorite(byte[] buffer, int offset, int dataSize) 768 throws InvalidProtocolBufferNanoException { 769 Favorite favorite = new Favorite(); 770 MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize)); 771 return favorite; 772 } 773 774 /** Serialize a Screen for persistence, including a checksum wrapper. */ 775 private byte[] packScreen(Cursor c) { 776 Screen screen = new Screen(); 777 screen.id = c.getLong(ID_INDEX); 778 screen.rank = c.getInt(SCREEN_RANK_INDEX); 779 780 return writeCheckedBytes(screen); 781 } 782 783 /** Deserialize a Screen from persistence, after verifying checksum wrapper. */ 784 private Screen unpackScreen(byte[] buffer, int offset, int dataSize) 785 throws InvalidProtocolBufferNanoException { 786 Screen screen = new Screen(); 787 MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize)); 788 return screen; 789 } 790 791 /** Serialize an icon Resource for persistence, including a checksum wrapper. */ 792 private byte[] packIcon(int dpi, Bitmap icon) { 793 Resource res = new Resource(); 794 res.dpi = dpi; 795 ByteArrayOutputStream os = new ByteArrayOutputStream(); 796 if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { 797 res.data = os.toByteArray(); 798 } 799 return writeCheckedBytes(res); 800 } 801 802 /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */ 803 private Resource unpackIcon(byte[] buffer, int offset, int dataSize) 804 throws InvalidProtocolBufferNanoException { 805 Resource res = new Resource(); 806 MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize)); 807 return res; 808 } 809 810 /** Serialize a widget for persistence, including a checksum wrapper. */ 811 private byte[] packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache, 812 ComponentName provider) { 813 final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider); 814 Widget widget = new Widget(); 815 widget.provider = provider.flattenToShortString(); 816 widget.label = info.label; 817 widget.configure = info.configure != null; 818 if (info.icon != 0) { 819 widget.icon = new Resource(); 820 Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon); 821 Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext); 822 ByteArrayOutputStream os = new ByteArrayOutputStream(); 823 if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { 824 widget.icon.data = os.toByteArray(); 825 widget.icon.dpi = dpi; 826 } 827 } 828 if (info.previewImage != 0) { 829 widget.preview = new Resource(); 830 Bitmap preview = previewLoader.generateWidgetPreview(info, null); 831 ByteArrayOutputStream os = new ByteArrayOutputStream(); 832 if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { 833 widget.preview.data = os.toByteArray(); 834 widget.preview.dpi = dpi; 835 } 836 } 837 return writeCheckedBytes(widget); 838 } 839 840 /** Deserialize a widget from persistence, after verifying checksum wrapper. */ 841 private Widget unpackWidget(byte[] buffer, int offset, int dataSize) 842 throws InvalidProtocolBufferNanoException { 843 Widget widget = new Widget(); 844 MessageNano.mergeFrom(widget, readCheckedBytes(buffer, offset, dataSize)); 845 return widget; 846 } 847 848 /** 849 * Read the old journal from the input file. 850 * 851 * In the event of any error, just pretend we didn't have a journal, 852 * in that case, do a full backup. 853 * 854 * @param oldState the read-0only file descriptor pointing to the old journal 855 * @return a Journal protocol bugffer 856 */ 857 private Journal readJournal(ParcelFileDescriptor oldState) { 858 Journal journal = new Journal(); 859 if (oldState == null) { 860 return journal; 861 } 862 FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor()); 863 try { 864 int remaining = inStream.available(); 865 if (DEBUG) Log.d(TAG, "available " + remaining); 866 if (remaining < MAX_JOURNAL_SIZE) { 867 byte[] buffer = new byte[remaining]; 868 int bytesRead = 0; 869 while (remaining > 0) { 870 try { 871 int result = inStream.read(buffer, bytesRead, remaining); 872 if (result > 0) { 873 if (DEBUG) Log.d(TAG, "read some bytes: " + result); 874 remaining -= result; 875 bytesRead += result; 876 } else { 877 // stop reading ands see what there is to parse 878 Log.w(TAG, "read error: " + result); 879 remaining = 0; 880 } 881 } catch (IOException e) { 882 Log.w(TAG, "failed to read the journal", e); 883 buffer = null; 884 remaining = 0; 885 } 886 } 887 if (DEBUG) Log.d(TAG, "journal bytes read: " + bytesRead); 888 889 if (buffer != null) { 890 try { 891 MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, bytesRead)); 892 } catch (InvalidProtocolBufferNanoException e) { 893 Log.d(TAG, "failed to read the journal", e); 894 journal.clear(); 895 } 896 } 897 } 898 } catch (IOException e) { 899 Log.d(TAG, "failed to close the journal", e); 900 } finally { 901 try { 902 inStream.close(); 903 } catch (IOException e) { 904 Log.d(TAG, "failed to close the journal", e); 905 } 906 } 907 return journal; 908 } 909 910 private void writeRowToBackup(Key key, byte[] blob, Journal out, 911 BackupDataOutput data) throws IOException { 912 String backupKey = keyToBackupKey(key); 913 data.writeEntityHeader(backupKey, blob.length); 914 data.writeEntityData(blob, blob.length); 915 out.rows++; 916 out.bytes += blob.length; 917 Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " + 918 getKeyName(key) + "/" + blob.length); 919 if(DEBUG_PAYLOAD) { 920 String encoded = Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP); 921 final int chunkSize = 1024; 922 for (int offset = 0; offset < encoded.length(); offset += chunkSize) { 923 int end = offset + chunkSize; 924 end = Math.min(end, encoded.length()); 925 Log.d(TAG, "wrote " + encoded.substring(offset, end)); 926 } 927 } 928 } 929 930 private Set<String> getSavedIdsByType(int type, Journal in) { 931 Set<String> savedIds = new HashSet<String>(); 932 for(int i = 0; i < in.key.length; i++) { 933 Key key = in.key[i]; 934 if (key.type == type) { 935 savedIds.add(keyToBackupKey(key)); 936 } 937 } 938 return savedIds; 939 } 940 941 private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data) 942 throws IOException { 943 int rows = 0; 944 for(String deleted: deletedIds) { 945 Log.v(TAG, "dropping icon " + deleted); 946 data.writeEntityHeader(deleted, -1); 947 rows++; 948 } 949 return rows; 950 } 951 952 /** 953 * Write the new journal to the output file. 954 * 955 * In the event of any error, just pretend we didn't have a journal, 956 * in that case, do a full backup. 957 958 * @param newState the write-only file descriptor pointing to the new journal 959 * @param journal a Journal protocol buffer 960 */ 961 private void writeJournal(ParcelFileDescriptor newState, Journal journal) { 962 FileOutputStream outStream = null; 963 try { 964 outStream = new FileOutputStream(newState.getFileDescriptor()); 965 outStream.write(writeCheckedBytes(journal)); 966 outStream.close(); 967 } catch (IOException e) { 968 Log.d(TAG, "failed to write backup journal", e); 969 } 970 } 971 972 /** Wrap a proto in a CheckedMessage and compute the checksum. */ 973 private byte[] writeCheckedBytes(MessageNano proto) { 974 CheckedMessage wrapper = new CheckedMessage(); 975 wrapper.payload = MessageNano.toByteArray(proto); 976 CRC32 checksum = new CRC32(); 977 checksum.update(wrapper.payload); 978 wrapper.checksum = checksum.getValue(); 979 return MessageNano.toByteArray(wrapper); 980 } 981 982 /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */ 983 private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize) 984 throws InvalidProtocolBufferNanoException { 985 CheckedMessage wrapper = new CheckedMessage(); 986 MessageNano.mergeFrom(wrapper, buffer, offset, dataSize); 987 CRC32 checksum = new CRC32(); 988 checksum.update(wrapper.payload); 989 if (wrapper.checksum != checksum.getValue()) { 990 throw new InvalidProtocolBufferNanoException("checksum does not match"); 991 } 992 return wrapper.payload; 993 } 994 995 private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) { 996 if (mWidgetMap == null) { 997 List<AppWidgetProviderInfo> widgets = 998 AppWidgetManager.getInstance(mContext).getInstalledProviders(); 999 mWidgetMap = new HashMap<ComponentName, AppWidgetProviderInfo>(widgets.size()); 1000 for (AppWidgetProviderInfo info : widgets) { 1001 mWidgetMap.put(info.provider, info); 1002 } 1003 } 1004 return mWidgetMap.get(component); 1005 } 1006 1007 private class KeyParsingException extends Throwable { 1008 private KeyParsingException(Throwable cause) { 1009 super(cause); 1010 } 1011 1012 public KeyParsingException(String reason) { 1013 super(reason); 1014 } 1015 } 1016 } 1017