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