Home | History | Annotate | Download | only in launcher3
      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 
    152     public LauncherBackupHelper(Context context) {
    153         mContext = context;
    154         mExistingKeys = new HashSet<String>();
    155         mKeys = new ArrayList<Key>();
    156         restoreSuccessful = true;
    157     }
    158 
    159     private void dataChanged() {
    160         if (mBackupManager == null) {
    161             mBackupManager = new BackupManager(mContext);
    162         }
    163         mBackupManager.dataChanged();
    164     }
    165 
    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
    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      */
    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
    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
    344     public void writeNewStateDescription(ParcelFileDescriptor newState) {
    345         writeJournal(newState, getCurrentStateJournal());
    346     }
    347 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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. */
    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. */
    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. */
    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. */
    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. */
    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. */
    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. */
    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. */
    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. */
    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      */
    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      */
    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 
   1012     private void writeRowToBackup(Key key, MessageNano proto, BackupDataOutput data)
   1013             throws IOException {
   1014         writeRowToBackup(keyToBackupKey(key), proto, data);
   1015     }
   1016 
   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      */
   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. */
   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. */
   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 
   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 
   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      */
   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 
   1121     private String getUserSelectionArg() {
   1122         return Favorites.PROFILE_ID + '=' + UserManagerCompat.getInstance(mContext)
   1123                 .getSerialNumberForUser(UserHandleCompat.myUserHandle());
   1124     }
   1125 
   1126     private class InvalidBackupException extends IOException {
   1127         private InvalidBackupException(Throwable cause) {
   1128             super(cause);
   1129         }
   1130 
   1131         public InvalidBackupException(String reason) {
   1132             super(reason);
   1133         }
   1134     }
   1135 }
   1136