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
17package com.android.launcher3;
18
19import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
20import com.google.protobuf.nano.MessageNano;
21
22import com.android.launcher3.LauncherSettings.Favorites;
23import com.android.launcher3.LauncherSettings.WorkspaceScreens;
24import com.android.launcher3.backup.BackupProtos;
25import com.android.launcher3.backup.BackupProtos.CheckedMessage;
26import com.android.launcher3.backup.BackupProtos.Favorite;
27import com.android.launcher3.backup.BackupProtos.Journal;
28import com.android.launcher3.backup.BackupProtos.Key;
29import com.android.launcher3.backup.BackupProtos.Resource;
30import com.android.launcher3.backup.BackupProtos.Screen;
31import com.android.launcher3.backup.BackupProtos.Widget;
32
33import android.app.backup.BackupDataInputStream;
34import android.app.backup.BackupHelper;
35import android.app.backup.BackupDataInput;
36import android.app.backup.BackupDataOutput;
37import android.app.backup.BackupManager;
38import android.appwidget.AppWidgetManager;
39import android.appwidget.AppWidgetProviderInfo;
40import android.content.ComponentName;
41import android.content.ContentResolver;
42import android.content.Context;
43import android.content.Intent;
44import android.database.Cursor;
45import android.graphics.Bitmap;
46import android.graphics.BitmapFactory;
47import android.graphics.drawable.Drawable;
48import android.os.ParcelFileDescriptor;
49import android.text.TextUtils;
50import android.util.Base64;
51import android.util.Log;
52
53import java.io.ByteArrayOutputStream;
54import java.io.FileInputStream;
55import java.io.FileOutputStream;
56import java.io.IOException;
57import java.net.URISyntaxException;
58import java.util.ArrayList;
59import java.util.HashMap;
60import java.util.HashSet;
61import java.util.List;
62import java.util.Set;
63import java.util.zip.CRC32;
64
65/**
66 * Persist the launcher home state across calamities.
67 */
68public class LauncherBackupHelper implements BackupHelper {
69
70    private static final String TAG = "LauncherBackupHelper";
71    private static final boolean DEBUG = false;
72    private static final boolean DEBUG_PAYLOAD = false;
73
74    private static final int MAX_JOURNAL_SIZE = 1000000;
75
76    /** icons are large, dribble them out */
77    private static final int MAX_ICONS_PER_PASS = 10;
78
79    /** widgets contain previews, which are very large, dribble them out */
80    private static final int MAX_WIDGETS_PER_PASS = 5;
81
82    public static final int IMAGE_COMPRESSION_QUALITY = 75;
83
84    public static final String LAUNCHER_PREFIX = "L";
85
86    private static final Bitmap.CompressFormat IMAGE_FORMAT =
87            android.graphics.Bitmap.CompressFormat.PNG;
88
89    private static BackupManager sBackupManager;
90
91    private static final String[] FAVORITE_PROJECTION = {
92            Favorites._ID,                     // 0
93            Favorites.MODIFIED,                // 1
94            Favorites.INTENT,                  // 2
95            Favorites.APPWIDGET_PROVIDER,      // 3
96            Favorites.APPWIDGET_ID,            // 4
97            Favorites.CELLX,                   // 5
98            Favorites.CELLY,                   // 6
99            Favorites.CONTAINER,               // 7
100            Favorites.ICON,                    // 8
101            Favorites.ICON_PACKAGE,            // 9
102            Favorites.ICON_RESOURCE,           // 10
103            Favorites.ICON_TYPE,               // 11
104            Favorites.ITEM_TYPE,               // 12
105            Favorites.SCREEN,                  // 13
106            Favorites.SPANX,                   // 14
107            Favorites.SPANY,                   // 15
108            Favorites.TITLE,                   // 16
109    };
110
111    private static final int ID_INDEX = 0;
112    private static final int ID_MODIFIED = 1;
113    private static final int INTENT_INDEX = 2;
114    private static final int APPWIDGET_PROVIDER_INDEX = 3;
115    private static final int APPWIDGET_ID_INDEX = 4;
116    private static final int CELLX_INDEX = 5;
117    private static final int CELLY_INDEX = 6;
118    private static final int CONTAINER_INDEX = 7;
119    private static final int ICON_INDEX = 8;
120    private static final int ICON_PACKAGE_INDEX = 9;
121    private static final int ICON_RESOURCE_INDEX = 10;
122    private static final int ICON_TYPE_INDEX = 11;
123    private static final int ITEM_TYPE_INDEX = 12;
124    private static final int SCREEN_INDEX = 13;
125    private static final int SPANX_INDEX = 14;
126    private static final int SPANY_INDEX = 15;
127    private static final int TITLE_INDEX = 16;
128
129    private static final String[] SCREEN_PROJECTION = {
130            WorkspaceScreens._ID,              // 0
131            WorkspaceScreens.MODIFIED,         // 1
132            WorkspaceScreens.SCREEN_RANK       // 2
133    };
134
135    private static final int SCREEN_RANK_INDEX = 2;
136
137    private final Context mContext;
138
139    private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap;
140
141    private ArrayList<Key> mKeys;
142
143    public LauncherBackupHelper(Context context) {
144        mContext = context;
145    }
146
147    private void dataChanged() {
148        if (sBackupManager == null) {
149            sBackupManager = new BackupManager(mContext);
150        }
151        sBackupManager.dataChanged();
152    }
153
154    /**
155     * Back up launcher data so we can restore the user's state on a new device.
156     *
157     * <P>The journal is a timestamp and a list of keys that were saved as of that time.
158     *
159     * <P>Keys may come back in any order, so each key/value is one complete row of the database.
160     *
161     * @param oldState notes from the last backup
162     * @param data incremental key/value pairs to persist off-device
163     * @param newState notes for the next backup
164     * @throws IOException
165     */
166    @Override
167    public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
168            ParcelFileDescriptor newState) {
169        Log.v(TAG, "onBackup");
170
171        Journal in = readJournal(oldState);
172        Journal out = new Journal();
173
174        long lastBackupTime = in.t;
175        out.t = System.currentTimeMillis();
176        out.rows = 0;
177        out.bytes = 0;
178
179        Log.v(TAG, "lastBackupTime=" + lastBackupTime);
180
181        ArrayList<Key> keys = new ArrayList<Key>();
182        try {
183            backupFavorites(in, data, out, keys);
184            backupScreens(in, data, out, keys);
185            backupIcons(in, data, out, keys);
186            backupWidgets(in, data, out, keys);
187        } catch (IOException e) {
188            Log.e(TAG, "launcher backup has failed", e);
189        }
190
191        out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
192        writeJournal(newState, out);
193        Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
194    }
195
196    /**
197     * Restore launcher configuration from the restored data stream.
198     *
199     * <P>Keys may arrive in any order.
200     *
201     * @param data the key/value pair from the server
202     */
203    @Override
204    public void restoreEntity(BackupDataInputStream data) {
205        Log.v(TAG, "restoreEntity");
206        if (mKeys == null) {
207            mKeys = new ArrayList<Key>();
208        }
209        byte[] buffer = new byte[512];
210            String backupKey = data.getKey();
211            int dataSize = data.size();
212            if (buffer.length < dataSize) {
213                buffer = new byte[dataSize];
214            }
215            Key key = null;
216        int bytesRead = 0;
217        try {
218            bytesRead = data.read(buffer, 0, dataSize);
219            if (DEBUG) Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available");
220        } catch (IOException e) {
221            Log.d(TAG, "failed to read entity from restore data", e);
222        }
223        try {
224            key = backupKeyToKey(backupKey);
225            switch (key.type) {
226                case Key.FAVORITE:
227                    restoreFavorite(key, buffer, dataSize, mKeys);
228                    break;
229
230                case Key.SCREEN:
231                    restoreScreen(key, buffer, dataSize, mKeys);
232                    break;
233
234                case Key.ICON:
235                    restoreIcon(key, buffer, dataSize, mKeys);
236                    break;
237
238                case Key.WIDGET:
239                    restoreWidget(key, buffer, dataSize, mKeys);
240                    break;
241
242                default:
243                    Log.w(TAG, "unknown restore entity type: " + key.type);
244                    break;
245            }
246        } catch (KeyParsingException e) {
247            Log.w(TAG, "ignoring unparsable backup key: " + backupKey);
248        }
249
250    }
251
252    /**
253     * Record the restore state for the next backup.
254     *
255     * @param newState notes about the backup state after restore.
256     */
257    @Override
258    public void writeNewStateDescription(ParcelFileDescriptor newState) {
259        // clear the output journal time, to force a full backup to
260        // will catch any changes the restore process might have made
261        Journal out = new Journal();
262        out.t = 0;
263        out.key = mKeys.toArray(BackupProtos.Key.EMPTY_ARRAY);
264        writeJournal(newState, out);
265        Log.v(TAG, "onRestore: read " + mKeys.size() + " rows");
266        mKeys.clear();
267    }
268
269    /**
270     * Write all modified favorites to the data stream.
271     *
272     *
273     * @param in notes from last backup
274     * @param data output stream for key/value pairs
275     * @param out notes about this backup
276     * @param keys keys to mark as clean in the notes for next backup
277     * @throws IOException
278     */
279    private void backupFavorites(Journal in, BackupDataOutput data, Journal out,
280            ArrayList<Key> keys)
281            throws IOException {
282        // read the old ID set
283        Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in);
284        if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
285
286        // persist things that have changed since the last backup
287        ContentResolver cr = mContext.getContentResolver();
288        Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
289                null, null, null);
290        Set<String> currentIds = new HashSet<String>(cursor.getCount());
291        try {
292            cursor.moveToPosition(-1);
293            while(cursor.moveToNext()) {
294                final long id = cursor.getLong(ID_INDEX);
295                final long updateTime = cursor.getLong(ID_MODIFIED);
296                Key key = getKey(Key.FAVORITE, id);
297                keys.add(key);
298                currentIds.add(keyToBackupKey(key));
299                if (updateTime > in.t) {
300                    byte[] blob = packFavorite(cursor);
301                    writeRowToBackup(key, blob, out, data);
302                }
303            }
304        } finally {
305            cursor.close();
306        }
307        if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
308
309        // these IDs must have been deleted
310        savedIds.removeAll(currentIds);
311        out.rows += removeDeletedKeysFromBackup(savedIds, data);
312    }
313
314    /**
315     * Read a favorite from the stream.
316     *
317     * <P>Keys arrive in any order, so screens and containers may not exist yet.
318     *
319     * @param key identifier for the row
320     * @param buffer the serialized proto from the stream, may be larger than dataSize
321     * @param dataSize the size of the proto from the stream
322     * @param keys keys to mark as clean in the notes for next backup
323     */
324    private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
325        Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)");
326        if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
327                Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
328
329        try {
330            Favorite favorite =  unpackFavorite(buffer, 0, dataSize);
331            if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType);
332        } catch (InvalidProtocolBufferNanoException e) {
333            Log.w(TAG, "failed to decode proto", e);
334        }
335    }
336
337    /**
338     * Write all modified screens to the data stream.
339     *
340     *
341     * @param in notes from last backup
342     * @param data output stream for key/value pairs
343     * @param out notes about this backup
344     * @param keys keys to mark as clean in the notes for next backup
345     * @throws IOException
346     */
347    private void backupScreens(Journal in, BackupDataOutput data, Journal out,
348            ArrayList<Key> keys)
349            throws IOException {
350        // read the old ID set
351        Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in);
352        if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
353
354        // persist things that have changed since the last backup
355        ContentResolver cr = mContext.getContentResolver();
356        Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
357                null, null, null);
358        Set<String> currentIds = new HashSet<String>(cursor.getCount());
359        try {
360            cursor.moveToPosition(-1);
361            while(cursor.moveToNext()) {
362                final long id = cursor.getLong(ID_INDEX);
363                final long updateTime = cursor.getLong(ID_MODIFIED);
364                Key key = getKey(Key.SCREEN, id);
365                keys.add(key);
366                currentIds.add(keyToBackupKey(key));
367                if (updateTime > in.t) {
368                    byte[] blob = packScreen(cursor);
369                    writeRowToBackup(key, blob, out, data);
370                }
371            }
372        } finally {
373            cursor.close();
374        }
375        if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
376
377        // these IDs must have been deleted
378        savedIds.removeAll(currentIds);
379        out.rows += removeDeletedKeysFromBackup(savedIds, data);
380    }
381
382    /**
383     * Read a screen from the stream.
384     *
385     * <P>Keys arrive in any order, so children of this screen may already exist.
386     *
387     * @param key identifier for the row
388     * @param buffer the serialized proto from the stream, may be larger than dataSize
389     * @param dataSize the size of the proto from the stream
390     * @param keys keys to mark as clean in the notes for next backup
391     */
392    private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
393        Log.v(TAG, "unpacking screen " + key.id);
394        if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
395                Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
396        try {
397            Screen screen = unpackScreen(buffer, 0, dataSize);
398            if (DEBUG) Log.d(TAG, "unpacked " + screen.rank);
399        } catch (InvalidProtocolBufferNanoException e) {
400            Log.w(TAG, "failed to decode proto", e);
401        }
402    }
403
404    /**
405     * Write all the static icon resources we need to render placeholders
406     * for a package that is not installed.
407     *
408     * @param in notes from last backup
409     * @param data output stream for key/value pairs
410     * @param out notes about this backup
411     * @param keys keys to mark as clean in the notes for next backup
412     * @throws IOException
413     */
414    private void backupIcons(Journal in, BackupDataOutput data, Journal out,
415            ArrayList<Key> keys) throws IOException {
416        // persist icons that haven't been persisted yet
417        final LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
418        if (appState == null) {
419            dataChanged(); // try again later
420            if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying icon backup");
421            return;
422        }
423        final ContentResolver cr = mContext.getContentResolver();
424        final IconCache iconCache = appState.getIconCache();
425        final int dpi = mContext.getResources().getDisplayMetrics().densityDpi;
426
427        // read the old ID set
428        Set<String> savedIds = getSavedIdsByType(Key.ICON, in);
429        if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size());
430
431        int startRows = out.rows;
432        if (DEBUG) Log.d(TAG, "starting here: " + startRows);
433        String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION;
434        Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
435                where, null, null);
436        Set<String> currentIds = new HashSet<String>(cursor.getCount());
437        try {
438            cursor.moveToPosition(-1);
439            while(cursor.moveToNext()) {
440                final long id = cursor.getLong(ID_INDEX);
441                final String intentDescription = cursor.getString(INTENT_INDEX);
442                try {
443                    Intent intent = Intent.parseUri(intentDescription, 0);
444                    ComponentName cn = intent.getComponent();
445                    Key key = null;
446                    String backupKey = null;
447                    if (cn != null) {
448                        key = getKey(Key.ICON, cn.flattenToShortString());
449                        backupKey = keyToBackupKey(key);
450                        currentIds.add(backupKey);
451                    } else {
452                        Log.w(TAG, "empty intent on application favorite: " + id);
453                    }
454                    if (savedIds.contains(backupKey)) {
455                        if (DEBUG) Log.d(TAG, "already saved icon " + backupKey);
456
457                        // remember that we already backed this up previously
458                        keys.add(key);
459                    } else if (backupKey != null) {
460                        if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
461                        if ((out.rows - startRows) < MAX_ICONS_PER_PASS) {
462                            if (DEBUG) Log.d(TAG, "saving icon " + backupKey);
463                            Bitmap icon = iconCache.getIcon(intent);
464                            keys.add(key);
465                            if (icon != null && !iconCache.isDefaultIcon(icon)) {
466                                byte[] blob = packIcon(dpi, icon);
467                                writeRowToBackup(key, blob, out, data);
468                            }
469                        } else {
470                            if (DEBUG) Log.d(TAG, "scheduling another run for icon " + backupKey);
471                            // too many icons for this pass, request another.
472                            dataChanged();
473                        }
474                    }
475                } catch (URISyntaxException e) {
476                    Log.w(TAG, "invalid URI on application favorite: " + id);
477                } catch (IOException e) {
478                    Log.w(TAG, "unable to save application icon for favorite: " + id);
479                }
480
481            }
482        } finally {
483            cursor.close();
484        }
485        if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size());
486
487        // these IDs must have been deleted
488        savedIds.removeAll(currentIds);
489        out.rows += removeDeletedKeysFromBackup(savedIds, data);
490    }
491
492    /**
493     * Read an icon from the stream.
494     *
495     * <P>Keys arrive in any order, so shortcuts that use this icon may already exist.
496     *
497     * @param key identifier for the row
498     * @param buffer the serialized proto from the stream, may be larger than dataSize
499     * @param dataSize the size of the proto from the stream
500     * @param keys keys to mark as clean in the notes for next backup
501     */
502    private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
503        Log.v(TAG, "unpacking icon " + key.id);
504        if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
505                Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
506        try {
507            Resource res = unpackIcon(buffer, 0, dataSize);
508            if (DEBUG) Log.d(TAG, "unpacked " + res.dpi);
509            if (DEBUG) Log.d(TAG, "read " +
510                    Base64.encodeToString(res.data, 0, res.data.length,
511                            Base64.NO_WRAP));
512            Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length);
513            if (icon == null) {
514                Log.w(TAG, "failed to unpack icon for " + key.name);
515            }
516        } catch (InvalidProtocolBufferNanoException e) {
517            Log.w(TAG, "failed to decode proto", e);
518        }
519    }
520
521    /**
522     * Write all the static widget resources we need to render placeholders
523     * for a package that is not installed.
524     *
525     * @param in notes from last backup
526     * @param data output stream for key/value pairs
527     * @param out notes about this backup
528     * @param keys keys to mark as clean in the notes for next backup
529     * @throws IOException
530     */
531    private void backupWidgets(Journal in, BackupDataOutput data, Journal out,
532            ArrayList<Key> keys) throws IOException {
533        // persist static widget info that hasn't been persisted yet
534        final LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
535        if (appState == null) {
536            dataChanged(); // try again later
537            if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying widget backup");
538            return;
539        }
540        final ContentResolver cr = mContext.getContentResolver();
541        final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext);
542        final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext);
543        final IconCache iconCache = appState.getIconCache();
544        final int dpi = mContext.getResources().getDisplayMetrics().densityDpi;
545        final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile();
546        if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx);
547
548        // read the old ID set
549        Set<String> savedIds = getSavedIdsByType(Key.WIDGET, in);
550        if (DEBUG) Log.d(TAG, "widgets savedIds.size()=" + savedIds.size());
551
552        int startRows = out.rows;
553        if (DEBUG) Log.d(TAG, "starting here: " + startRows);
554        String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET;
555        Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
556                where, null, null);
557        Set<String> currentIds = new HashSet<String>(cursor.getCount());
558        try {
559            cursor.moveToPosition(-1);
560            while(cursor.moveToNext()) {
561                final long id = cursor.getLong(ID_INDEX);
562                final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX);
563                final int spanX = cursor.getInt(SPANX_INDEX);
564                final int spanY = cursor.getInt(SPANY_INDEX);
565                final ComponentName provider = ComponentName.unflattenFromString(providerName);
566                Key key = null;
567                String backupKey = null;
568                if (provider != null) {
569                    key = getKey(Key.WIDGET, providerName);
570                    backupKey = keyToBackupKey(key);
571                    currentIds.add(backupKey);
572                } else {
573                    Log.w(TAG, "empty intent on appwidget: " + id);
574                }
575                if (savedIds.contains(backupKey)) {
576                    if (DEBUG) Log.d(TAG, "already saved widget " + backupKey);
577
578                    // remember that we already backed this up previously
579                    keys.add(key);
580                } else if (backupKey != null) {
581                    if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
582                    if ((out.rows - startRows) < MAX_WIDGETS_PER_PASS) {
583                        if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
584                        previewLoader.setPreviewSize(spanX * profile.cellWidthPx,
585                                spanY * profile.cellHeightPx, widgetSpacingLayout);
586                        byte[] blob = packWidget(dpi, previewLoader, iconCache, provider);
587                        keys.add(key);
588                        writeRowToBackup(key, blob, out, data);
589
590                    } else {
591                        if (DEBUG) Log.d(TAG, "scheduling another run for widget " + backupKey);
592                        // too many widgets for this pass, request another.
593                        dataChanged();
594                    }
595                }
596            }
597        } finally {
598            cursor.close();
599        }
600        if (DEBUG) Log.d(TAG, "widget currentIds.size()=" + currentIds.size());
601
602        // these IDs must have been deleted
603        savedIds.removeAll(currentIds);
604        out.rows += removeDeletedKeysFromBackup(savedIds, data);
605    }
606
607    /**
608     * Read a widget from the stream.
609     *
610     * <P>Keys arrive in any order, so widgets that use this data may already exist.
611     *
612     * @param key identifier for the row
613     * @param buffer the serialized proto from the stream, may be larger than dataSize
614     * @param dataSize the size of the proto from the stream
615     * @param keys keys to mark as clean in the notes for next backup
616     */
617    private void restoreWidget(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
618        Log.v(TAG, "unpacking widget " + key.id);
619        if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
620                Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
621        try {
622            Widget widget = unpackWidget(buffer, 0, dataSize);
623            if (DEBUG) Log.d(TAG, "unpacked " + widget.provider);
624            if (widget.icon.data != null)  {
625                Bitmap icon = BitmapFactory
626                        .decodeByteArray(widget.icon.data, 0, widget.icon.data.length);
627                if (icon == null) {
628                    Log.w(TAG, "failed to unpack widget icon for " + key.name);
629                }
630            }
631        } catch (InvalidProtocolBufferNanoException e) {
632            Log.w(TAG, "failed to decode proto", e);
633        }
634    }
635
636    /** create a new key, with an integer ID.
637     *
638     * <P> Keys contain their own checksum instead of using
639     * the heavy-weight CheckedMessage wrapper.
640     */
641    private Key getKey(int type, long id) {
642        Key key = new Key();
643        key.type = type;
644        key.id = id;
645        key.checksum = checkKey(key);
646        return key;
647    }
648
649    /** create a new key for a named object.
650     *
651     * <P> Keys contain their own checksum instead of using
652     * the heavy-weight CheckedMessage wrapper.
653     */
654    private Key getKey(int type, String name) {
655        Key key = new Key();
656        key.type = type;
657        key.name = name;
658        key.checksum = checkKey(key);
659        return key;
660    }
661
662    /** keys need to be strings, serialize and encode. */
663    private String keyToBackupKey(Key key) {
664        return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP);
665    }
666
667    /** keys need to be strings, decode and parse. */
668    private Key backupKeyToKey(String backupKey) throws KeyParsingException {
669        try {
670            Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT));
671            if (key.checksum != checkKey(key)) {
672                key = null;
673                throw new KeyParsingException("invalid key read from stream" + backupKey);
674            }
675            return key;
676        } catch (InvalidProtocolBufferNanoException e) {
677            throw new KeyParsingException(e);
678        } catch (IllegalArgumentException e) {
679            throw new KeyParsingException(e);
680        }
681    }
682
683    private String getKeyName(Key key) {
684        if (TextUtils.isEmpty(key.name)) {
685            return Long.toString(key.id);
686        } else {
687            return key.name;
688        }
689
690    }
691
692    private String geKeyType(Key key) {
693        switch (key.type) {
694            case Key.FAVORITE:
695                return "favorite";
696            case Key.SCREEN:
697                return "screen";
698            case Key.ICON:
699                return "icon";
700            case Key.WIDGET:
701                return "widget";
702            default:
703                return "anonymous";
704        }
705    }
706
707    /** Compute the checksum over the important bits of a key. */
708    private long checkKey(Key key) {
709        CRC32 checksum = new CRC32();
710        checksum.update(key.type);
711        checksum.update((int) (key.id & 0xffff));
712        checksum.update((int) ((key.id >> 32) & 0xffff));
713        if (!TextUtils.isEmpty(key.name)) {
714            checksum.update(key.name.getBytes());
715        }
716        return checksum.getValue();
717    }
718
719    /** Serialize a Favorite for persistence, including a checksum wrapper. */
720    private byte[] packFavorite(Cursor c) {
721        Favorite favorite = new Favorite();
722        favorite.id = c.getLong(ID_INDEX);
723        favorite.screen = c.getInt(SCREEN_INDEX);
724        favorite.container = c.getInt(CONTAINER_INDEX);
725        favorite.cellX = c.getInt(CELLX_INDEX);
726        favorite.cellY = c.getInt(CELLY_INDEX);
727        favorite.spanX = c.getInt(SPANX_INDEX);
728        favorite.spanY = c.getInt(SPANY_INDEX);
729        favorite.iconType = c.getInt(ICON_TYPE_INDEX);
730        if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
731            String iconPackage = c.getString(ICON_PACKAGE_INDEX);
732            if (!TextUtils.isEmpty(iconPackage)) {
733                favorite.iconPackage = iconPackage;
734            }
735            String iconResource = c.getString(ICON_RESOURCE_INDEX);
736            if (!TextUtils.isEmpty(iconResource)) {
737                favorite.iconResource = iconResource;
738            }
739        }
740        if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
741            byte[] blob = c.getBlob(ICON_INDEX);
742            if (blob != null && blob.length > 0) {
743                favorite.icon = blob;
744            }
745        }
746        String title = c.getString(TITLE_INDEX);
747        if (!TextUtils.isEmpty(title)) {
748            favorite.title = title;
749        }
750        String intent = c.getString(INTENT_INDEX);
751        if (!TextUtils.isEmpty(intent)) {
752            favorite.intent = intent;
753        }
754        favorite.itemType = c.getInt(ITEM_TYPE_INDEX);
755        if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
756            favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX);
757            String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX);
758            if (!TextUtils.isEmpty(appWidgetProvider)) {
759                favorite.appWidgetProvider = appWidgetProvider;
760            }
761        }
762
763        return writeCheckedBytes(favorite);
764    }
765
766    /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */
767    private Favorite unpackFavorite(byte[] buffer, int offset, int dataSize)
768            throws InvalidProtocolBufferNanoException {
769        Favorite favorite = new Favorite();
770        MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize));
771        return favorite;
772    }
773
774    /** Serialize a Screen for persistence, including a checksum wrapper. */
775    private byte[] packScreen(Cursor c) {
776        Screen screen = new Screen();
777        screen.id = c.getLong(ID_INDEX);
778        screen.rank = c.getInt(SCREEN_RANK_INDEX);
779
780        return writeCheckedBytes(screen);
781    }
782
783    /** Deserialize a Screen from persistence, after verifying checksum wrapper. */
784    private Screen unpackScreen(byte[] buffer, int offset, int dataSize)
785            throws InvalidProtocolBufferNanoException {
786        Screen screen = new Screen();
787        MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize));
788        return screen;
789    }
790
791    /** Serialize an icon Resource for persistence, including a checksum wrapper. */
792    private byte[] packIcon(int dpi, Bitmap icon) {
793        Resource res = new Resource();
794        res.dpi = dpi;
795        ByteArrayOutputStream os = new ByteArrayOutputStream();
796        if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
797            res.data = os.toByteArray();
798        }
799        return writeCheckedBytes(res);
800    }
801
802    /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */
803    private Resource unpackIcon(byte[] buffer, int offset, int dataSize)
804            throws InvalidProtocolBufferNanoException {
805        Resource res = new Resource();
806        MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize));
807        return res;
808    }
809
810    /** Serialize a widget for persistence, including a checksum wrapper. */
811    private byte[] packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache,
812            ComponentName provider) {
813        final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider);
814        Widget widget = new Widget();
815        widget.provider = provider.flattenToShortString();
816        widget.label = info.label;
817        widget.configure = info.configure != null;
818        if (info.icon != 0) {
819            widget.icon = new Resource();
820            Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon);
821            Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext);
822            ByteArrayOutputStream os = new ByteArrayOutputStream();
823            if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
824                widget.icon.data = os.toByteArray();
825                widget.icon.dpi = dpi;
826            }
827        }
828        if (info.previewImage != 0) {
829            widget.preview = new Resource();
830            Bitmap preview = previewLoader.generateWidgetPreview(info, null);
831            ByteArrayOutputStream os = new ByteArrayOutputStream();
832            if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
833                widget.preview.data = os.toByteArray();
834                widget.preview.dpi = dpi;
835            }
836        }
837        return writeCheckedBytes(widget);
838    }
839
840    /** Deserialize a widget from persistence, after verifying checksum wrapper. */
841    private Widget unpackWidget(byte[] buffer, int offset, int dataSize)
842            throws InvalidProtocolBufferNanoException {
843        Widget widget = new Widget();
844        MessageNano.mergeFrom(widget, readCheckedBytes(buffer, offset, dataSize));
845        return widget;
846    }
847
848    /**
849     * Read the old journal from the input file.
850     *
851     * In the event of any error, just pretend we didn't have a journal,
852     * in that case, do a full backup.
853     *
854     * @param oldState the read-0only file descriptor pointing to the old journal
855     * @return a Journal protocol bugffer
856     */
857    private Journal readJournal(ParcelFileDescriptor oldState) {
858        Journal journal = new Journal();
859        if (oldState == null) {
860            return journal;
861        }
862        FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor());
863        try {
864            int remaining = inStream.available();
865            if (DEBUG) Log.d(TAG, "available " + remaining);
866            if (remaining < MAX_JOURNAL_SIZE) {
867                byte[] buffer = new byte[remaining];
868                int bytesRead = 0;
869                while (remaining > 0) {
870                    try {
871                        int result = inStream.read(buffer, bytesRead, remaining);
872                        if (result > 0) {
873                            if (DEBUG) Log.d(TAG, "read some bytes: " + result);
874                            remaining -= result;
875                            bytesRead += result;
876                        } else {
877                            // stop reading ands see what there is to parse
878                            Log.w(TAG, "read error: " + result);
879                            remaining = 0;
880                        }
881                    } catch (IOException e) {
882                        Log.w(TAG, "failed to read the journal", e);
883                        buffer = null;
884                        remaining = 0;
885                    }
886                }
887                if (DEBUG) Log.d(TAG, "journal bytes read: " + bytesRead);
888
889                if (buffer != null) {
890                    try {
891                        MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, bytesRead));
892                    } catch (InvalidProtocolBufferNanoException e) {
893                        Log.d(TAG, "failed to read the journal", e);
894                        journal.clear();
895                    }
896                }
897            }
898        } catch (IOException e) {
899            Log.d(TAG, "failed to close the journal", e);
900        } finally {
901            try {
902                inStream.close();
903            } catch (IOException e) {
904                Log.d(TAG, "failed to close the journal", e);
905            }
906        }
907        return journal;
908    }
909
910    private void writeRowToBackup(Key key, byte[] blob, Journal out,
911            BackupDataOutput data) throws IOException {
912        String backupKey = keyToBackupKey(key);
913        data.writeEntityHeader(backupKey, blob.length);
914        data.writeEntityData(blob, blob.length);
915        out.rows++;
916        out.bytes += blob.length;
917        Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " +
918                getKeyName(key) + "/" + blob.length);
919        if(DEBUG_PAYLOAD) {
920            String encoded = Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP);
921            final int chunkSize = 1024;
922            for (int offset = 0; offset < encoded.length(); offset += chunkSize) {
923                int end = offset + chunkSize;
924                end = Math.min(end, encoded.length());
925                Log.d(TAG, "wrote " + encoded.substring(offset, end));
926            }
927        }
928    }
929
930    private Set<String> getSavedIdsByType(int type, Journal in) {
931        Set<String> savedIds = new HashSet<String>();
932        for(int i = 0; i < in.key.length; i++) {
933            Key key = in.key[i];
934            if (key.type == type) {
935                savedIds.add(keyToBackupKey(key));
936            }
937        }
938        return savedIds;
939    }
940
941    private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data)
942            throws IOException {
943        int rows = 0;
944        for(String deleted: deletedIds) {
945            Log.v(TAG, "dropping icon " + deleted);
946            data.writeEntityHeader(deleted, -1);
947            rows++;
948        }
949        return rows;
950    }
951
952    /**
953     * Write the new journal to the output file.
954     *
955     * In the event of any error, just pretend we didn't have a journal,
956     * in that case, do a full backup.
957
958     * @param newState the write-only file descriptor pointing to the new journal
959     * @param journal a Journal protocol buffer
960     */
961    private void writeJournal(ParcelFileDescriptor newState, Journal journal) {
962        FileOutputStream outStream = null;
963        try {
964            outStream = new FileOutputStream(newState.getFileDescriptor());
965            outStream.write(writeCheckedBytes(journal));
966            outStream.close();
967        } catch (IOException e) {
968            Log.d(TAG, "failed to write backup journal", e);
969        }
970    }
971
972    /** Wrap a proto in a CheckedMessage and compute the checksum. */
973    private byte[] writeCheckedBytes(MessageNano proto) {
974        CheckedMessage wrapper = new CheckedMessage();
975        wrapper.payload = MessageNano.toByteArray(proto);
976        CRC32 checksum = new CRC32();
977        checksum.update(wrapper.payload);
978        wrapper.checksum = checksum.getValue();
979        return MessageNano.toByteArray(wrapper);
980    }
981
982    /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */
983    private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize)
984            throws InvalidProtocolBufferNanoException {
985        CheckedMessage wrapper = new CheckedMessage();
986        MessageNano.mergeFrom(wrapper, buffer, offset, dataSize);
987        CRC32 checksum = new CRC32();
988        checksum.update(wrapper.payload);
989        if (wrapper.checksum != checksum.getValue()) {
990            throw new InvalidProtocolBufferNanoException("checksum does not match");
991        }
992        return wrapper.payload;
993    }
994
995    private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) {
996        if (mWidgetMap == null) {
997            List<AppWidgetProviderInfo> widgets =
998                    AppWidgetManager.getInstance(mContext).getInstalledProviders();
999            mWidgetMap = new HashMap<ComponentName, AppWidgetProviderInfo>(widgets.size());
1000            for (AppWidgetProviderInfo info : widgets) {
1001                mWidgetMap.put(info.provider, info);
1002            }
1003        }
1004        return mWidgetMap.get(component);
1005    }
1006
1007    private class KeyParsingException extends Throwable {
1008        private KeyParsingException(Throwable cause) {
1009            super(cause);
1010        }
1011
1012        public KeyParsingException(String reason) {
1013            super(reason);
1014        }
1015    }
1016}
1017