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