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