/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; import android.app.backup.BackupManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.WorkspaceScreens; import com.android.launcher3.backup.BackupProtos; import com.android.launcher3.backup.BackupProtos.CheckedMessage; import com.android.launcher3.backup.BackupProtos.DeviceProfieData; import com.android.launcher3.backup.BackupProtos.Favorite; import com.android.launcher3.backup.BackupProtos.Journal; import com.android.launcher3.backup.BackupProtos.Key; import com.android.launcher3.backup.BackupProtos.Resource; import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.model.MigrateFromRestoreTask; import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.zip.CRC32; /** * Persist the launcher home state across calamities. */ public class LauncherBackupHelper implements BackupHelper { private static final String TAG = "LauncherBackupHelper"; private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; private static final boolean DEBUG = LauncherBackupAgentHelper.DEBUG; private static final int BACKUP_VERSION = 4; private static final int MAX_JOURNAL_SIZE = 1000000; // Journal key is such that it is always smaller than any dynamically generated // key (any Base64 encoded string). private static final String JOURNAL_KEY = "#"; /** icons are large, dribble them out */ private static final int MAX_ICONS_PER_PASS = 10; /** widgets contain previews, which are very large, dribble them out */ private static final int MAX_WIDGETS_PER_PASS = 5; private static final String[] FAVORITE_PROJECTION = { Favorites._ID, // 0 Favorites.MODIFIED, // 1 Favorites.INTENT, // 2 Favorites.APPWIDGET_PROVIDER, // 3 Favorites.APPWIDGET_ID, // 4 Favorites.CELLX, // 5 Favorites.CELLY, // 6 Favorites.CONTAINER, // 7 Favorites.ICON, // 8 Favorites.ICON_PACKAGE, // 9 Favorites.ICON_RESOURCE, // 10 Favorites.ICON_TYPE, // 11 Favorites.ITEM_TYPE, // 12 Favorites.SCREEN, // 13 Favorites.SPANX, // 14 Favorites.SPANY, // 15 Favorites.TITLE, // 16 Favorites.PROFILE_ID, // 17 Favorites.RANK, // 18 }; private static final int ID_INDEX = 0; private static final int ID_MODIFIED = 1; private static final int INTENT_INDEX = 2; private static final int APPWIDGET_PROVIDER_INDEX = 3; private static final int APPWIDGET_ID_INDEX = 4; private static final int CELLX_INDEX = 5; private static final int CELLY_INDEX = 6; private static final int CONTAINER_INDEX = 7; private static final int ICON_INDEX = 8; private static final int ICON_PACKAGE_INDEX = 9; private static final int ICON_RESOURCE_INDEX = 10; private static final int ICON_TYPE_INDEX = 11; private static final int ITEM_TYPE_INDEX = 12; private static final int SCREEN_INDEX = 13; private static final int SPANX_INDEX = 14; private static final int SPANY_INDEX = 15; private static final int TITLE_INDEX = 16; private static final int RANK_INDEX = 18; private static final String[] SCREEN_PROJECTION = { WorkspaceScreens._ID, // 0 WorkspaceScreens.MODIFIED, // 1 WorkspaceScreens.SCREEN_RANK // 2 }; private static final int SCREEN_RANK_INDEX = 2; @Thunk final Context mContext; private final HashSet mExistingKeys; private final ArrayList mKeys; private final ItemTypeMatcher[] mItemTypeMatchers; private final long mUserSerial; private BackupManager mBackupManager; private byte[] mBuffer = new byte[512]; private long mLastBackupRestoreTime; private boolean mBackupDataWasUpdated; private IconCache mIconCache; private DeviceProfieData mDeviceProfileData; private InvariantDeviceProfile mIdp; DeviceProfieData migrationCompatibleProfileData; HashSet widgetSizes = new HashSet<>(); boolean restoreSuccessful; int restoredBackupVersion = 1; // When migrating from a device which different hotseat configuration, the icons are shifted // to center along the new all-apps icon. private int mHotseatShift = 0; public LauncherBackupHelper(Context context) { mContext = context; mExistingKeys = new HashSet(); mKeys = new ArrayList(); restoreSuccessful = true; mItemTypeMatchers = new ItemTypeMatcher[CommonAppTypeParser.SUPPORTED_TYPE_COUNT]; UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); mUserSerial = userManager.getSerialNumberForUser(UserHandleCompat.myUserHandle()); } private void dataChanged() { if (mBackupManager == null) { mBackupManager = new BackupManager(mContext); } mBackupManager.dataChanged(); } private void applyJournal(Journal journal) { mLastBackupRestoreTime = journal.t; mExistingKeys.clear(); if (journal.key != null) { for (Key key : journal.key) { mExistingKeys.add(keyToBackupKey(key)); } } restoredBackupVersion = journal.backupVersion; } /** * Back up launcher data so we can restore the user's state on a new device. * *

The journal is a timestamp and a list of keys that were saved as of that time. * *

Keys may come back in any order, so each key/value is one complete row of the database. * * @param oldState notes from the last backup * @param data incremental key/value pairs to persist off-device * @param newState notes for the next backup */ @Override public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { if (VERBOSE) Log.v(TAG, "onBackup"); Journal in = readJournal(oldState); if (!launcherIsReady()) { dataChanged(); // Perform backup later. writeJournal(newState, in); return; } if (mDeviceProfileData == null) { LauncherAppState app = LauncherAppState.getInstance(); mIdp = app.getInvariantDeviceProfile(); mDeviceProfileData = initDeviceProfileData(mIdp); mIconCache = app.getIconCache(); } Log.v(TAG, "lastBackupTime = " + in.t); mKeys.clear(); applyJournal(in); // Record the time before performing backup so that entries edited while the backup // was going on, do not get missed in next backup. long newBackupTime = System.currentTimeMillis(); mBackupDataWasUpdated = false; try { backupFavorites(data); backupScreens(data); backupIcons(data); backupWidgets(data); // Delete any key which still exist in the old backup, but is not valid anymore. HashSet validKeys = new HashSet(); for (Key key : mKeys) { validKeys.add(keyToBackupKey(key)); } mExistingKeys.removeAll(validKeys); // Delete anything left in the existing keys. for (String deleted: mExistingKeys) { if (VERBOSE) Log.v(TAG, "dropping deleted item " + deleted); data.writeEntityHeader(deleted, -1); mBackupDataWasUpdated = true; } mExistingKeys.clear(); if (!mBackupDataWasUpdated) { // Check if any metadata has changed mBackupDataWasUpdated = (in.profile == null) || !Arrays.equals(DeviceProfieData.toByteArray(in.profile), DeviceProfieData.toByteArray(mDeviceProfileData)) || (in.backupVersion != BACKUP_VERSION) || (in.appVersion != getAppVersion()); } if (mBackupDataWasUpdated) { mLastBackupRestoreTime = newBackupTime; // We store the journal at two places. // 1) Storing it in newState allows us to do partial backups by comparing old state // 2) Storing it in backup data allows us to validate keys during restore Journal state = getCurrentStateJournal(); writeRowToBackup(JOURNAL_KEY, state, data); } else { if (DEBUG) Log.d(TAG, "Nothing was written during backup"); } } catch (IOException e) { Log.e(TAG, "launcher backup has failed", e); } writeNewStateDescription(newState); } /** * @return true if the backup corresponding to oldstate can be successfully applied * to this device. */ private boolean isBackupCompatible(Journal oldState) { DeviceProfieData currentProfile = mDeviceProfileData; DeviceProfieData oldProfile = oldState.profile; if (oldProfile == null || oldProfile.desktopCols == 0) { // Profile info is not valid, ignore the check. return true; } boolean isHotseatCompatible = false; if (currentProfile.allappsRank >= oldProfile.hotseatCount) { isHotseatCompatible = true; mHotseatShift = 0; } if ((currentProfile.allappsRank >= oldProfile.allappsRank) && ((currentProfile.hotseatCount - currentProfile.allappsRank) >= (oldProfile.hotseatCount - oldProfile.allappsRank))) { // There is enough space on both sides of the hotseat. isHotseatCompatible = true; mHotseatShift = currentProfile.allappsRank - oldProfile.allappsRank; } if (!isHotseatCompatible) { return false; } if ((currentProfile.desktopCols >= oldProfile.desktopCols) && (currentProfile.desktopRows >= oldProfile.desktopRows)) { return true; } if (MigrateFromRestoreTask.ENABLED && (oldProfile.desktopCols - currentProfile.desktopCols <= 1) && (oldProfile.desktopRows - currentProfile.desktopRows <= 1)) { // Allow desktop migration when row and/or column count contracts by 1. migrationCompatibleProfileData = initDeviceProfileData(mIdp); migrationCompatibleProfileData.desktopCols = oldProfile.desktopCols; migrationCompatibleProfileData.desktopRows = oldProfile.desktopRows; return true; } return false; } /** * Restore launcher configuration from the restored data stream. * It assumes that the keys will arrive in lexical order. So if the journal was present in the * backup, it should arrive first. * * @param data the key/value pair from the server */ @Override public void restoreEntity(BackupDataInputStream data) { if (!restoreSuccessful) { return; } if (mDeviceProfileData == null) { // This call does not happen on a looper thread. So LauncherAppState // can't be created . Instead initialize required dependencies directly. mIdp = new InvariantDeviceProfile(mContext); mDeviceProfileData = initDeviceProfileData(mIdp); mIconCache = new IconCache(mContext, mIdp); } int dataSize = data.size(); if (mBuffer.length < dataSize) { mBuffer = new byte[dataSize]; } try { int bytesRead = data.read(mBuffer, 0, dataSize); if (DEBUG) Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available"); String backupKey = data.getKey(); if (JOURNAL_KEY.equals(backupKey)) { if (VERBOSE) Log.v(TAG, "Journal entry restored"); if (!mKeys.isEmpty()) { // We received the journal key after a restore key. Log.wtf(TAG, keyToBackupKey(mKeys.get(0)) + " received after " + JOURNAL_KEY); restoreSuccessful = false; return; } Journal journal = new Journal(); MessageNano.mergeFrom(journal, readCheckedBytes(mBuffer, dataSize)); applyJournal(journal); restoreSuccessful = isBackupCompatible(journal); return; } if (!mExistingKeys.isEmpty() && !mExistingKeys.contains(backupKey)) { if (DEBUG) Log.e(TAG, "Ignoring key not present in the backup state " + backupKey); return; } Key key = backupKeyToKey(backupKey); mKeys.add(key); switch (key.type) { case Key.FAVORITE: restoreFavorite(key, mBuffer, dataSize); break; case Key.SCREEN: restoreScreen(key, mBuffer, dataSize); break; case Key.ICON: restoreIcon(key, mBuffer, dataSize); break; case Key.WIDGET: restoreWidget(key, mBuffer, dataSize); break; default: Log.w(TAG, "unknown restore entity type: " + key.type); mKeys.remove(key); break; } } catch (IOException e) { Log.w(TAG, "ignoring unparsable backup entry", e); } } /** * Record the restore state for the next backup. * * @param newState notes about the backup state after restore. */ @Override public void writeNewStateDescription(ParcelFileDescriptor newState) { writeJournal(newState, getCurrentStateJournal()); } private Journal getCurrentStateJournal() { Journal journal = new Journal(); journal.t = mLastBackupRestoreTime; journal.key = mKeys.toArray(new BackupProtos.Key[mKeys.size()]); journal.appVersion = getAppVersion(); journal.backupVersion = BACKUP_VERSION; journal.profile = mDeviceProfileData; return journal; } private int getAppVersion() { try { return mContext.getPackageManager() .getPackageInfo(mContext.getPackageName(), 0).versionCode; } catch (NameNotFoundException e) { return 0; } } private DeviceProfieData initDeviceProfileData(InvariantDeviceProfile profile) { DeviceProfieData data = new DeviceProfieData(); data.desktopRows = profile.numRows; data.desktopCols = profile.numColumns; data.hotseatCount = profile.numHotseatIcons; data.allappsRank = profile.hotseatAllAppsRank; return data; } /** * Write all modified favorites to the data stream. * * @param data output stream for key/value pairs * @throws IOException */ private void backupFavorites(BackupDataOutput data) throws IOException { // persist things that have changed since the last backup ContentResolver cr = mContext.getContentResolver(); // Don't backup apps in other profiles for now. Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, getUserSelectionArg(), null, null); try { cursor.moveToPosition(-1); while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final long updateTime = cursor.getLong(ID_MODIFIED); Key key = getKey(Key.FAVORITE, id); mKeys.add(key); final String backupKey = keyToBackupKey(key); // Favorite proto changed in v4. Backup again if the version is old. if (!mExistingKeys.contains(backupKey) || updateTime >= mLastBackupRestoreTime || restoredBackupVersion < 4) { writeRowToBackup(key, packFavorite(cursor), data); } else { if (DEBUG) Log.d(TAG, "favorite already backup up: " + id); } } } finally { cursor.close(); } } /** * Read a favorite from the stream. * *

Keys arrive in any order, so screens and containers may not exist yet. * * @param key identifier for the row * @param buffer the serialized proto from the stream, may be larger than dataSize * @param dataSize the size of the proto from the stream */ private void restoreFavorite(Key key, byte[] buffer, int dataSize) throws IOException { if (VERBOSE) Log.v(TAG, "unpacking favorite " + key.id); if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackFavorite(buffer, dataSize); cr.insert(Favorites.CONTENT_URI, values); } /** * Write all modified screens to the data stream. * * @param data output stream for key/value pairs * @throws IOException */ private void backupScreens(BackupDataOutput data) throws IOException { // persist things that have changed since the last backup ContentResolver cr = mContext.getContentResolver(); Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION, null, null, null); try { cursor.moveToPosition(-1); if (DEBUG) Log.d(TAG, "dumping screens after: " + mLastBackupRestoreTime); while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final long updateTime = cursor.getLong(ID_MODIFIED); Key key = getKey(Key.SCREEN, id); mKeys.add(key); final String backupKey = keyToBackupKey(key); if (!mExistingKeys.contains(backupKey) || updateTime >= mLastBackupRestoreTime) { writeRowToBackup(key, packScreen(cursor), data); } else { if (VERBOSE) Log.v(TAG, "screen already backup up " + id); } } } finally { cursor.close(); } } /** * Read a screen from the stream. * *

Keys arrive in any order, so children of this screen may already exist. * * @param key identifier for the row * @param buffer the serialized proto from the stream, may be larger than dataSize * @param dataSize the size of the proto from the stream */ private void restoreScreen(Key key, byte[] buffer, int dataSize) throws IOException { if (VERBOSE) Log.v(TAG, "unpacking screen " + key.id); if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackScreen(buffer, dataSize); cr.insert(WorkspaceScreens.CONTENT_URI, values); } /** * Write all the static icon resources we need to render placeholders * for a package that is not installed. * * @param data output stream for key/value pairs */ private void backupIcons(BackupDataOutput data) throws IOException { // persist icons that haven't been persisted yet final ContentResolver cr = mContext.getContentResolver(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; final UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); int backupUpIconCount = 0; // Don't backup apps in other profiles for now. String where = "(" + Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION + " OR " + Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_SHORTCUT + ") AND " + getUserSelectionArg(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, where, null, null); try { cursor.moveToPosition(-1); while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final String intentDescription = cursor.getString(INTENT_INDEX); try { Intent intent = Intent.parseUri(intentDescription, 0); ComponentName cn = intent.getComponent(); Key key = null; String backupKey = null; if (cn != null) { key = getKey(Key.ICON, cn.flattenToShortString()); backupKey = keyToBackupKey(key); } else { Log.w(TAG, "empty intent on application favorite: " + id); } if (mExistingKeys.contains(backupKey)) { if (DEBUG) Log.d(TAG, "already saved icon " + backupKey); // remember that we already backed this up previously mKeys.add(key); } else if (backupKey != null) { if (DEBUG) Log.d(TAG, "I can count this high: " + backupUpIconCount); if (backupUpIconCount < MAX_ICONS_PER_PASS) { if (DEBUG) Log.d(TAG, "saving icon " + backupKey); Bitmap icon = mIconCache.getIcon(intent, myUserHandle); if (icon != null && !mIconCache.isDefaultIcon(icon, myUserHandle)) { writeRowToBackup(key, packIcon(dpi, icon), data); mKeys.add(key); backupUpIconCount ++; } } else { if (VERBOSE) Log.v(TAG, "deferring icon backup " + backupKey); // too many icons for this pass, request another. dataChanged(); } } } catch (URISyntaxException e) { Log.e(TAG, "invalid URI on application favorite: " + id); } catch (IOException e) { Log.e(TAG, "unable to save application icon for favorite: " + id); } } } finally { cursor.close(); } } /** * Read an icon from the stream. * *

Keys arrive in any order, so shortcuts that use this icon may already exist. * * @param key identifier for the row * @param buffer the serialized proto from the stream, may be larger than dataSize * @param dataSize the size of the proto from the stream */ private void restoreIcon(Key key, byte[] buffer, int dataSize) throws IOException { if (VERBOSE) Log.v(TAG, "unpacking icon " + key.id); if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); Resource res = unpackProto(new Resource(), buffer, dataSize); if (DEBUG) { Log.d(TAG, "unpacked " + res.dpi + " dpi icon"); } Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length); if (icon == null) { Log.w(TAG, "failed to unpack icon for " + key.name); } else { if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, "" /* label */, mUserSerial, mIdp); } } /** * Write all the static widget resources we need to render placeholders * for a package that is not installed. * * @param data output stream for key/value pairs * @throws IOException */ private void backupWidgets(BackupDataOutput data) throws IOException { // persist static widget info that hasn't been persisted yet final ContentResolver cr = mContext.getContentResolver(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; int backupWidgetCount = 0; String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET + " AND " + getUserSelectionArg(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, where, null, null); try { cursor.moveToPosition(-1); while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); final ComponentName provider = ComponentName.unflattenFromString(providerName); Key key = null; String backupKey = null; if (provider != null) { key = getKey(Key.WIDGET, providerName); backupKey = keyToBackupKey(key); } else { Log.w(TAG, "empty intent on appwidget: " + id); } // Widget backup proto changed in v3. So add it again if the original backup is old. if (mExistingKeys.contains(backupKey) && restoredBackupVersion >= 3) { if (DEBUG) Log.d(TAG, "already saved widget " + backupKey); // remember that we already backed this up previously mKeys.add(key); } else if (backupKey != null) { if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount); if (backupWidgetCount < MAX_WIDGETS_PER_PASS) { if (DEBUG) Log.d(TAG, "saving widget " + backupKey); UserHandleCompat user = UserHandleCompat.myUserHandle(); writeRowToBackup(key, packWidget(dpi, provider, user), data); mKeys.add(key); backupWidgetCount ++; } else { if (VERBOSE) Log.v(TAG, "deferring widget backup " + backupKey); // too many widgets for this pass, request another. dataChanged(); } } } } finally { cursor.close(); } } /** * Read a widget from the stream. * *

Keys arrive in any order, so widgets that use this data may already exist. * * @param key identifier for the row * @param buffer the serialized proto from the stream, may be larger than dataSize * @param dataSize the size of the proto from the stream */ private void restoreWidget(Key key, byte[] buffer, int dataSize) throws IOException { if (VERBOSE) Log.v(TAG, "unpacking widget " + key.id); if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " + Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP)); Widget widget = unpackProto(new Widget(), buffer, dataSize); if (DEBUG) Log.d(TAG, "unpacked " + widget.provider); if (widget.icon.data != null) { Bitmap icon = BitmapFactory .decodeByteArray(widget.icon.data, 0, widget.icon.data.length); if (icon == null) { Log.w(TAG, "failed to unpack widget icon for " + key.name); } else { mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider), icon, widget.icon.dpi, widget.label, mUserSerial, mIdp); } } // Cache widget min sizes incase migration is required. widgetSizes.add(widget.provider + "#" + widget.minSpanX + "," + widget.minSpanY); } /** create a new key, with an integer ID. * *

Keys contain their own checksum instead of using * the heavy-weight CheckedMessage wrapper. */ private Key getKey(int type, long id) { Key key = new Key(); key.type = type; key.id = id; key.checksum = checkKey(key); return key; } /** create a new key for a named object. * *

Keys contain their own checksum instead of using * the heavy-weight CheckedMessage wrapper. */ private Key getKey(int type, String name) { Key key = new Key(); key.type = type; key.name = name; key.checksum = checkKey(key); return key; } /** keys need to be strings, serialize and encode. */ private String keyToBackupKey(Key key) { return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP); } /** keys need to be strings, decode and parse. */ private Key backupKeyToKey(String backupKey) throws InvalidBackupException { try { Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT)); if (key.checksum != checkKey(key)) { key = null; throw new InvalidBackupException("invalid key read from stream" + backupKey); } return key; } catch (InvalidProtocolBufferNanoException e) { throw new InvalidBackupException(e); } catch (IllegalArgumentException e) { throw new InvalidBackupException(e); } } /** Compute the checksum over the important bits of a key. */ private long checkKey(Key key) { CRC32 checksum = new CRC32(); checksum.update(key.type); checksum.update((int) (key.id & 0xffff)); checksum.update((int) ((key.id >> 32) & 0xffff)); if (!TextUtils.isEmpty(key.name)) { checksum.update(key.name.getBytes()); } return checksum.getValue(); } /** * @return true if its an hotseat item, that can be replaced during restore. * TODO: Extend check for folders in hotseat. */ private boolean isReplaceableHotseatItem(Favorite favorite) { return favorite.container == Favorites.CONTAINER_HOTSEAT && favorite.intent != null && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT); } /** Serialize a Favorite for persistence, including a checksum wrapper. */ private Favorite packFavorite(Cursor c) { Favorite favorite = new Favorite(); favorite.id = c.getLong(ID_INDEX); favorite.screen = c.getInt(SCREEN_INDEX); favorite.container = c.getInt(CONTAINER_INDEX); favorite.cellX = c.getInt(CELLX_INDEX); favorite.cellY = c.getInt(CELLY_INDEX); favorite.spanX = c.getInt(SPANX_INDEX); favorite.spanY = c.getInt(SPANY_INDEX); favorite.iconType = c.getInt(ICON_TYPE_INDEX); favorite.rank = c.getInt(RANK_INDEX); String title = c.getString(TITLE_INDEX); if (!TextUtils.isEmpty(title)) { favorite.title = title; } String intentDescription = c.getString(INTENT_INDEX); Intent intent = null; if (!TextUtils.isEmpty(intentDescription)) { try { intent = Intent.parseUri(intentDescription, 0); intent.removeExtra(ItemInfo.EXTRA_PROFILE); favorite.intent = intent.toUri(0); } catch (URISyntaxException e) { Log.e(TAG, "Invalid intent", e); } } favorite.itemType = c.getInt(ITEM_TYPE_INDEX); if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX); String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX); if (!TextUtils.isEmpty(appWidgetProvider)) { favorite.appWidgetProvider = appWidgetProvider; } } else if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { String iconPackage = c.getString(ICON_PACKAGE_INDEX); if (!TextUtils.isEmpty(iconPackage)) { favorite.iconPackage = iconPackage; } String iconResource = c.getString(ICON_RESOURCE_INDEX); if (!TextUtils.isEmpty(iconResource)) { favorite.iconResource = iconResource; } } byte[] blob = c.getBlob(ICON_INDEX); if (blob != null && blob.length > 0) { favorite.icon = blob; } } if (isReplaceableHotseatItem(favorite)) { if (intent != null && intent.getComponent() != null) { PackageManager pm = mContext.getPackageManager(); ActivityInfo activity = null;; try { activity = pm.getActivityInfo(intent.getComponent(), 0); } catch (NameNotFoundException e) { Log.e(TAG, "Target not found", e); } if (activity == null) { return favorite; } for (int i = 0; i < mItemTypeMatchers.length; i++) { if (mItemTypeMatchers[i] == null) { mItemTypeMatchers[i] = new ItemTypeMatcher( CommonAppTypeParser.getResourceForItemType(i)); } if (mItemTypeMatchers[i].matches(activity, pm)) { favorite.targetType = i; break; } } } } return favorite; } /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */ private ContentValues unpackFavorite(byte[] buffer, int dataSize) throws IOException { Favorite favorite = unpackProto(new Favorite(), buffer, dataSize); // If it is a hotseat item, move it accordingly. if (favorite.container == Favorites.CONTAINER_HOTSEAT) { favorite.screen += mHotseatShift; } ContentValues values = new ContentValues(); values.put(Favorites._ID, favorite.id); values.put(Favorites.SCREEN, favorite.screen); values.put(Favorites.CONTAINER, favorite.container); values.put(Favorites.CELLX, favorite.cellX); values.put(Favorites.CELLY, favorite.cellY); values.put(Favorites.SPANX, favorite.spanX); values.put(Favorites.SPANY, favorite.spanY); values.put(Favorites.RANK, favorite.rank); if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { values.put(Favorites.ICON_TYPE, favorite.iconType); if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { values.put(Favorites.ICON_PACKAGE, favorite.iconPackage); values.put(Favorites.ICON_RESOURCE, favorite.iconResource); } values.put(Favorites.ICON, favorite.icon); } if (!TextUtils.isEmpty(favorite.title)) { values.put(Favorites.TITLE, favorite.title); } else { values.put(Favorites.TITLE, ""); } if (!TextUtils.isEmpty(favorite.intent)) { values.put(Favorites.INTENT, favorite.intent); } values.put(Favorites.ITEM_TYPE, favorite.itemType); UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); long userSerialNumber = UserManagerCompat.getInstance(mContext).getSerialNumberForUser(myUserHandle); values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); // If we will attempt grid resize, use the original profile to validate grid size, as // anything which fits in the original grid should fit in the current grid after // grid migration. DeviceProfieData currentProfile = migrationCompatibleProfileData == null ? mDeviceProfileData : migrationCompatibleProfileData; if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { if (!TextUtils.isEmpty(favorite.appWidgetProvider)) { values.put(Favorites.APPWIDGET_PROVIDER, favorite.appWidgetProvider); } values.put(Favorites.APPWIDGET_ID, favorite.appWidgetId); values.put(LauncherSettings.Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY | LauncherAppWidgetInfo.FLAG_UI_NOT_READY); // Verify placement if (((favorite.cellX + favorite.spanX) > currentProfile.desktopCols) || ((favorite.cellY + favorite.spanY) > currentProfile.desktopRows)) { restoreSuccessful = false; throw new InvalidBackupException("Widget not in screen bounds, aborting restore"); } } else { // Check if it is an hotseat item, that can be replaced. if (isReplaceableHotseatItem(favorite) && favorite.targetType != Favorite.TARGET_NONE && favorite.targetType < CommonAppTypeParser.SUPPORTED_TYPE_COUNT) { Log.e(TAG, "Added item type flag"); values.put(LauncherSettings.Favorites.RESTORED, 1 | CommonAppTypeParser.encodeItemTypeToFlag(favorite.targetType)); } else { // Let LauncherModel know we've been here. values.put(LauncherSettings.Favorites.RESTORED, 1); } // Verify placement if (favorite.container == Favorites.CONTAINER_HOTSEAT) { if ((favorite.screen >= currentProfile.hotseatCount) || (favorite.screen == currentProfile.allappsRank)) { restoreSuccessful = false; throw new InvalidBackupException("Item not in hotseat bounds, aborting restore"); } } else { if ((favorite.cellX >= currentProfile.desktopCols) || (favorite.cellY >= currentProfile.desktopRows)) { restoreSuccessful = false; throw new InvalidBackupException("Item not in desktop bounds, aborting restore"); } } } return values; } /** Serialize a Screen for persistence, including a checksum wrapper. */ private Screen packScreen(Cursor c) { Screen screen = new Screen(); screen.id = c.getLong(ID_INDEX); screen.rank = c.getInt(SCREEN_RANK_INDEX); return screen; } /** Deserialize a Screen from persistence, after verifying checksum wrapper. */ private ContentValues unpackScreen(byte[] buffer, int dataSize) throws InvalidProtocolBufferNanoException { Screen screen = unpackProto(new Screen(), buffer, dataSize); ContentValues values = new ContentValues(); values.put(WorkspaceScreens._ID, screen.id); values.put(WorkspaceScreens.SCREEN_RANK, screen.rank); return values; } /** Serialize an icon Resource for persistence, including a checksum wrapper. */ private Resource packIcon(int dpi, Bitmap icon) { Resource res = new Resource(); res.dpi = dpi; res.data = Utilities.flattenBitmap(icon); return res; } /** Serialize a widget for persistence, including a checksum wrapper. */ private Widget packWidget(int dpi, ComponentName provider, UserHandleCompat user) { final LauncherAppWidgetProviderInfo info = LauncherModel.getProviderInfo(mContext, provider, user); Widget widget = new Widget(); widget.provider = provider.flattenToShortString(); widget.label = info.label; widget.configure = info.configure != null; if (info.icon != 0) { widget.icon = new Resource(); Drawable fullResIcon = mIconCache.getFullResIcon(provider.getPackageName(), info.icon); Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext); widget.icon.data = Utilities.flattenBitmap(icon); widget.icon.dpi = dpi; } Point spans = info.getMinSpans(mIdp, mContext); widget.minSpanX = spans.x; widget.minSpanY = spans.y; return widget; } /** * Deserialize a proto after verifying checksum wrapper. */ private T unpackProto(T proto, byte[] buffer, int dataSize) throws InvalidProtocolBufferNanoException { MessageNano.mergeFrom(proto, readCheckedBytes(buffer, dataSize)); if (DEBUG) Log.d(TAG, "unpacked proto " + proto); return proto; } /** * Read the old journal from the input file. * * In the event of any error, just pretend we didn't have a journal, * in that case, do a full backup. * * @param oldState the read-0only file descriptor pointing to the old journal * @return a Journal protocol buffer */ private Journal readJournal(ParcelFileDescriptor oldState) { Journal journal = new Journal(); if (oldState == null) { return journal; } FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor()); try { int availableBytes = inStream.available(); if (DEBUG) Log.d(TAG, "available " + availableBytes); if (availableBytes < MAX_JOURNAL_SIZE) { byte[] buffer = new byte[availableBytes]; int bytesRead = 0; boolean valid = false; InvalidProtocolBufferNanoException lastProtoException = null; while (availableBytes > 0) { try { // OMG what are you doing? This is crazy inefficient! // If we read a byte that is not ours, we will cause trouble: b/12491813 // However, we don't know how many bytes to expect (oops). // So we have to step through *slowly*, watching for the end. int result = inStream.read(buffer, bytesRead, 1); if (result > 0) { availableBytes -= result; bytesRead += result; } else { Log.w(TAG, "unexpected end of file while reading journal."); // stop reading and see what there is to parse availableBytes = 0; } } catch (IOException e) { buffer = null; availableBytes = 0; } // check the buffer to see if we have a valid journal try { MessageNano.mergeFrom(journal, readCheckedBytes(buffer, bytesRead)); // if we are here, then we have read a valid, checksum-verified journal valid = true; availableBytes = 0; if (VERBOSE) Log.v(TAG, "read " + bytesRead + " bytes of journal"); } catch (InvalidProtocolBufferNanoException e) { // if we don't have the whole journal yet, mergeFrom will throw. keep going. lastProtoException = e; journal.clear(); } } if (DEBUG) Log.d(TAG, "journal bytes read: " + bytesRead); if (!valid) { Log.w(TAG, "could not find a valid journal", lastProtoException); } } } catch (IOException e) { Log.w(TAG, "failed to close the journal", e); } finally { try { inStream.close(); } catch (IOException e) { Log.w(TAG, "failed to close the journal", e); } } return journal; } private void writeRowToBackup(Key key, MessageNano proto, BackupDataOutput data) throws IOException { writeRowToBackup(keyToBackupKey(key), proto, data); } private void writeRowToBackup(String backupKey, MessageNano proto, BackupDataOutput data) throws IOException { byte[] blob = writeCheckedBytes(proto); data.writeEntityHeader(backupKey, blob.length); data.writeEntityData(blob, blob.length); mBackupDataWasUpdated = true; if (VERBOSE) Log.v(TAG, "Writing New entry " + backupKey); } /** * Write the new journal to the output file. * * In the event of any error, just pretend we didn't have a journal, * in that case, do a full backup. * @param newState the write-only file descriptor pointing to the new journal * @param journal a Journal protocol buffer */ private void writeJournal(ParcelFileDescriptor newState, Journal journal) { FileOutputStream outStream = null; try { outStream = new FileOutputStream(newState.getFileDescriptor()); final byte[] journalBytes = writeCheckedBytes(journal); outStream.write(journalBytes); outStream.close(); if (VERBOSE) Log.v(TAG, "wrote " + journalBytes.length + " bytes of journal"); } catch (IOException e) { Log.w(TAG, "failed to write backup journal", e); } } /** Wrap a proto in a CheckedMessage and compute the checksum. */ private byte[] writeCheckedBytes(MessageNano proto) { CheckedMessage wrapper = new CheckedMessage(); wrapper.payload = MessageNano.toByteArray(proto); CRC32 checksum = new CRC32(); checksum.update(wrapper.payload); wrapper.checksum = checksum.getValue(); return MessageNano.toByteArray(wrapper); } /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */ private static byte[] readCheckedBytes(byte[] buffer, int dataSize) throws InvalidProtocolBufferNanoException { CheckedMessage wrapper = new CheckedMessage(); MessageNano.mergeFrom(wrapper, buffer, 0, dataSize); CRC32 checksum = new CRC32(); checksum.update(wrapper.payload); if (wrapper.checksum != checksum.getValue()) { throw new InvalidProtocolBufferNanoException("checksum does not match"); } return wrapper.payload; } /** * @return true if the launcher is in a state to support backup */ private boolean launcherIsReady() { ContentResolver cr = mContext.getContentResolver(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, null, null, null); if (cursor == null) { // launcher data has been wiped, do nothing return false; } cursor.close(); if (LauncherAppState.getInstanceNoCreate() == null) { // launcher services are unavailable, try again later return false; } return true; } private String getUserSelectionArg() { return Favorites.PROFILE_ID + '=' + UserManagerCompat.getInstance(mContext) .getSerialNumberForUser(UserHandleCompat.myUserHandle()); } @Thunk class InvalidBackupException extends IOException { private static final long serialVersionUID = 8931456637211665082L; @Thunk InvalidBackupException(Throwable cause) { super(cause); } @Thunk InvalidBackupException(String reason) { super(reason); } } public boolean shouldAttemptWorkspaceMigration() { return migrationCompatibleProfileData != null; } /** * A class to check if an activity can handle one of the intents from a list of * predefined intents. */ private class ItemTypeMatcher { private final ArrayList mIntents; ItemTypeMatcher(int xml_res) { mIntents = xml_res == 0 ? new ArrayList() : parseIntents(xml_res); } private ArrayList parseIntents(int xml_res) { ArrayList intents = new ArrayList(); XmlResourceParser parser = mContext.getResources().getXml(xml_res); try { DefaultLayoutParser.beginDocument(parser, DefaultLayoutParser.TAG_RESOLVE); final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } else if (DefaultLayoutParser.TAG_FAVORITE.equals(parser.getName())) { final String uri = DefaultLayoutParser.getAttributeValue( parser, DefaultLayoutParser.ATTR_URI); intents.add(Intent.parseUri(uri, 0)); } } } catch (URISyntaxException | XmlPullParserException | IOException e) { Log.e(TAG, "Unable to parse " + xml_res, e); } finally { parser.close(); } return intents; } public boolean matches(ActivityInfo activity, PackageManager pm) { for (Intent intent : mIntents) { intent.setPackage(activity.packageName); ResolveInfo info = pm.resolveActivity(intent, 0); if (info != null && (info.activityInfo.name.equals(activity.name) || info.activityInfo.name.equals(activity.targetActivity))) { return true; } } return false; } } }