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