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