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