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