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