blob: 69b0026ed54eef1ccb753998c9810173982d3e47 [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
Chris Wren92aa4232013-10-04 11:29:36 -040033import android.app.backup.BackupDataInputStream;
Chris Wren1ada10d2013-09-13 18:01:38 -040034import android.app.backup.BackupDataOutput;
Chris Wren4d89e2a2013-10-09 17:03:50 -040035import android.app.backup.BackupHelper;
Chris Wren1ada10d2013-09-13 18:01:38 -040036import 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;
Chris Wren5dee7af2013-12-20 17:22:11 -050041import android.content.ContentValues;
Chris Wren1ada10d2013-09-13 18:01:38 -040042import android.content.Context;
Chris Wren22e130d2013-09-23 18:25:57 -040043import android.content.Intent;
Chris Wren1ada10d2013-09-13 18:01:38 -040044import android.database.Cursor;
Chris Wren22e130d2013-09-23 18:25:57 -040045import android.graphics.Bitmap;
46import android.graphics.BitmapFactory;
Chris Wrenfd13c712013-09-27 15:45:19 -040047import android.graphics.drawable.Drawable;
Chris Wren1ada10d2013-09-13 18:01:38 -040048import android.os.ParcelFileDescriptor;
Chris Wren1ada10d2013-09-13 18:01:38 -040049import android.text.TextUtils;
50import android.util.Base64;
51import android.util.Log;
52
Chris Wren22e130d2013-09-23 18:25:57 -040053import java.io.ByteArrayOutputStream;
Chris Wren1ada10d2013-09-13 18:01:38 -040054import java.io.FileInputStream;
55import java.io.FileOutputStream;
56import java.io.IOException;
Chris Wren22e130d2013-09-23 18:25:57 -040057import java.net.URISyntaxException;
Chris Wren1ada10d2013-09-13 18:01:38 -040058import java.util.ArrayList;
Chris Wren22e130d2013-09-23 18:25:57 -040059import java.util.HashMap;
Chris Wren1ada10d2013-09-13 18:01:38 -040060import java.util.HashSet;
Chris Wren22e130d2013-09-23 18:25:57 -040061import java.util.List;
Chris Wren1ada10d2013-09-13 18:01:38 -040062import java.util.Set;
63import java.util.zip.CRC32;
64
65/**
66 * Persist the launcher home state across calamities.
67 */
Chris Wren92aa4232013-10-04 11:29:36 -040068public class LauncherBackupHelper implements BackupHelper {
Chris Wren1ada10d2013-09-13 18:01:38 -040069
Chris Wren92aa4232013-10-04 11:29:36 -040070 private static final String TAG = "LauncherBackupHelper";
Chris Wrenfd13c712013-09-27 15:45:19 -040071 private static final boolean DEBUG = false;
Chris Wren92aa4232013-10-04 11:29:36 -040072 private static final boolean DEBUG_PAYLOAD = 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 Wren92aa4232013-10-04 11:29:36 -040084 public static final String LAUNCHER_PREFIX = "L";
85
Chris Wren45297f82013-10-17 15:16:48 -040086 public static final String LAUNCHER_PREFS_PREFIX = "LP";
87
Chris Wrenb86f0762013-10-04 10:10:21 -040088 private static final Bitmap.CompressFormat IMAGE_FORMAT =
89 android.graphics.Bitmap.CompressFormat.PNG;
90
Chris Wren1ada10d2013-09-13 18:01:38 -040091 private static BackupManager sBackupManager;
92
93 private static final String[] FAVORITE_PROJECTION = {
94 Favorites._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -040095 Favorites.MODIFIED, // 1
96 Favorites.INTENT, // 2
97 Favorites.APPWIDGET_PROVIDER, // 3
98 Favorites.APPWIDGET_ID, // 4
99 Favorites.CELLX, // 5
100 Favorites.CELLY, // 6
101 Favorites.CONTAINER, // 7
102 Favorites.ICON, // 8
103 Favorites.ICON_PACKAGE, // 9
104 Favorites.ICON_RESOURCE, // 10
105 Favorites.ICON_TYPE, // 11
106 Favorites.ITEM_TYPE, // 12
107 Favorites.SCREEN, // 13
108 Favorites.SPANX, // 14
109 Favorites.SPANY, // 15
110 Favorites.TITLE, // 16
Chris Wren1ada10d2013-09-13 18:01:38 -0400111 };
112
113 private static final int ID_INDEX = 0;
Chris Wren22e130d2013-09-23 18:25:57 -0400114 private static final int ID_MODIFIED = 1;
115 private static final int INTENT_INDEX = 2;
116 private static final int APPWIDGET_PROVIDER_INDEX = 3;
117 private static final int APPWIDGET_ID_INDEX = 4;
118 private static final int CELLX_INDEX = 5;
119 private static final int CELLY_INDEX = 6;
120 private static final int CONTAINER_INDEX = 7;
121 private static final int ICON_INDEX = 8;
122 private static final int ICON_PACKAGE_INDEX = 9;
123 private static final int ICON_RESOURCE_INDEX = 10;
124 private static final int ICON_TYPE_INDEX = 11;
125 private static final int ITEM_TYPE_INDEX = 12;
126 private static final int SCREEN_INDEX = 13;
127 private static final int SPANX_INDEX = 14;
128 private static final int SPANY_INDEX = 15;
129 private static final int TITLE_INDEX = 16;
Chris Wren1ada10d2013-09-13 18:01:38 -0400130
131 private static final String[] SCREEN_PROJECTION = {
132 WorkspaceScreens._ID, // 0
Chris Wren22e130d2013-09-23 18:25:57 -0400133 WorkspaceScreens.MODIFIED, // 1
134 WorkspaceScreens.SCREEN_RANK // 2
Chris Wren1ada10d2013-09-13 18:01:38 -0400135 };
136
Chris Wren22e130d2013-09-23 18:25:57 -0400137 private static final int SCREEN_RANK_INDEX = 2;
Chris Wren1ada10d2013-09-13 18:01:38 -0400138
Chris Wren92aa4232013-10-04 11:29:36 -0400139 private final Context mContext;
Chris Wren1ada10d2013-09-13 18:01:38 -0400140
Chris Wren4b171362014-01-13 17:39:02 -0500141 private final boolean mRestoreEnabled;
142
Chris Wren22e130d2013-09-23 18:25:57 -0400143 private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap;
144
Chris Wren92aa4232013-10-04 11:29:36 -0400145 private ArrayList<Key> mKeys;
Chris Wren1ada10d2013-09-13 18:01:38 -0400146
Chris Wren4b171362014-01-13 17:39:02 -0500147 public LauncherBackupHelper(Context context, boolean restoreEnabled) {
Chris Wren92aa4232013-10-04 11:29:36 -0400148 mContext = context;
Chris Wren4b171362014-01-13 17:39:02 -0500149 mRestoreEnabled = restoreEnabled;
Chris Wren92aa4232013-10-04 11:29:36 -0400150 }
151
152 private void dataChanged() {
Chris Wren1ada10d2013-09-13 18:01:38 -0400153 if (sBackupManager == null) {
Chris Wren92aa4232013-10-04 11:29:36 -0400154 sBackupManager = new BackupManager(mContext);
Chris Wren1ada10d2013-09-13 18:01:38 -0400155 }
156 sBackupManager.dataChanged();
157 }
158
159 /**
160 * Back up launcher data so we can restore the user's state on a new device.
161 *
162 * <P>The journal is a timestamp and a list of keys that were saved as of that time.
163 *
164 * <P>Keys may come back in any order, so each key/value is one complete row of the database.
165 *
166 * @param oldState notes from the last backup
167 * @param data incremental key/value pairs to persist off-device
168 * @param newState notes for the next backup
169 * @throws IOException
170 */
171 @Override
Chris Wren92aa4232013-10-04 11:29:36 -0400172 public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
173 ParcelFileDescriptor newState) {
Chris Wren1ada10d2013-09-13 18:01:38 -0400174 Log.v(TAG, "onBackup");
175
176 Journal in = readJournal(oldState);
177 Journal out = new Journal();
178
179 long lastBackupTime = in.t;
180 out.t = System.currentTimeMillis();
181 out.rows = 0;
182 out.bytes = 0;
183
184 Log.v(TAG, "lastBackupTime=" + lastBackupTime);
185
186 ArrayList<Key> keys = new ArrayList<Key>();
Chris Wren92aa4232013-10-04 11:29:36 -0400187 try {
188 backupFavorites(in, data, out, keys);
189 backupScreens(in, data, out, keys);
190 backupIcons(in, data, out, keys);
191 backupWidgets(in, data, out, keys);
192 } catch (IOException e) {
193 Log.e(TAG, "launcher backup has failed", e);
194 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400195
196 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
197 writeJournal(newState, out);
198 Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
Chris Wren1ada10d2013-09-13 18:01:38 -0400199 }
200
201 /**
Chris Wren92aa4232013-10-04 11:29:36 -0400202 * Restore launcher configuration from the restored data stream.
Chris Wren1ada10d2013-09-13 18:01:38 -0400203 *
204 * <P>Keys may arrive in any order.
205 *
Chris Wren92aa4232013-10-04 11:29:36 -0400206 * @param data the key/value pair from the server
Chris Wren1ada10d2013-09-13 18:01:38 -0400207 */
208 @Override
Chris Wren92aa4232013-10-04 11:29:36 -0400209 public void restoreEntity(BackupDataInputStream data) {
210 Log.v(TAG, "restoreEntity");
211 if (mKeys == null) {
212 mKeys = new ArrayList<Key>();
213 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400214 byte[] buffer = new byte[512];
Chris Wren1ada10d2013-09-13 18:01:38 -0400215 String backupKey = data.getKey();
Chris Wren92aa4232013-10-04 11:29:36 -0400216 int dataSize = data.size();
Chris Wren1ada10d2013-09-13 18:01:38 -0400217 if (buffer.length < dataSize) {
218 buffer = new byte[dataSize];
219 }
220 Key key = null;
Chris Wren92aa4232013-10-04 11:29:36 -0400221 int bytesRead = 0;
222 try {
223 bytesRead = data.read(buffer, 0, dataSize);
224 if (DEBUG) Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available");
225 } catch (IOException e) {
226 Log.d(TAG, "failed to read entity from restore data", e);
227 }
228 try {
229 key = backupKeyToKey(backupKey);
230 switch (key.type) {
231 case Key.FAVORITE:
232 restoreFavorite(key, buffer, dataSize, mKeys);
233 break;
234
235 case Key.SCREEN:
236 restoreScreen(key, buffer, dataSize, mKeys);
237 break;
238
239 case Key.ICON:
240 restoreIcon(key, buffer, dataSize, mKeys);
241 break;
242
243 case Key.WIDGET:
244 restoreWidget(key, buffer, dataSize, mKeys);
245 break;
246
247 default:
248 Log.w(TAG, "unknown restore entity type: " + key.type);
249 break;
Chris Wren1ada10d2013-09-13 18:01:38 -0400250 }
Chris Wren92aa4232013-10-04 11:29:36 -0400251 } catch (KeyParsingException e) {
252 Log.w(TAG, "ignoring unparsable backup key: " + backupKey);
Chris Wren1ada10d2013-09-13 18:01:38 -0400253 }
254
Chris Wren92aa4232013-10-04 11:29:36 -0400255 }
256
257 /**
258 * Record the restore state for the next backup.
259 *
260 * @param newState notes about the backup state after restore.
261 */
262 @Override
263 public void writeNewStateDescription(ParcelFileDescriptor newState) {
Chris Wren1ada10d2013-09-13 18:01:38 -0400264 // clear the output journal time, to force a full backup to
265 // will catch any changes the restore process might have made
Chris Wren92aa4232013-10-04 11:29:36 -0400266 Journal out = new Journal();
Chris Wren1ada10d2013-09-13 18:01:38 -0400267 out.t = 0;
Chris Wren92aa4232013-10-04 11:29:36 -0400268 out.key = mKeys.toArray(BackupProtos.Key.EMPTY_ARRAY);
Chris Wren1ada10d2013-09-13 18:01:38 -0400269 writeJournal(newState, out);
Chris Wren92aa4232013-10-04 11:29:36 -0400270 Log.v(TAG, "onRestore: read " + mKeys.size() + " rows");
271 mKeys.clear();
Chris Wren1ada10d2013-09-13 18:01:38 -0400272 }
273
274 /**
275 * Write all modified favorites to the data stream.
276 *
277 *
278 * @param in notes from last backup
279 * @param data output stream for key/value pairs
280 * @param out notes about this backup
281 * @param keys keys to mark as clean in the notes for next backup
282 * @throws IOException
283 */
284 private void backupFavorites(Journal in, BackupDataOutput data, Journal out,
285 ArrayList<Key> keys)
286 throws IOException {
287 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400288 Set<String> savedIds = getSavedIdsByType(Key.FAVORITE, in);
Chris Wren1ada10d2013-09-13 18:01:38 -0400289 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
290
291 // persist things that have changed since the last backup
Chris Wren92aa4232013-10-04 11:29:36 -0400292 ContentResolver cr = mContext.getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400293 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
294 null, null, null);
295 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400296 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400297 cursor.moveToPosition(-1);
298 while(cursor.moveToNext()) {
299 final long id = cursor.getLong(ID_INDEX);
300 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400301 Key key = getKey(Key.FAVORITE, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400302 keys.add(key);
Chris Wren5743aa92014-01-10 18:02:06 -0500303 final String backupKey = keyToBackupKey(key);
304 currentIds.add(backupKey);
305 if (!savedIds.contains(backupKey) || updateTime >= in.t) {
Chris Wren22e130d2013-09-23 18:25:57 -0400306 byte[] blob = packFavorite(cursor);
307 writeRowToBackup(key, blob, out, data);
308 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400309 }
310 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400311 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400312 }
313 if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
314
315 // these IDs must have been deleted
316 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400317 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400318 }
319
320 /**
321 * Read a favorite from the stream.
322 *
323 * <P>Keys arrive in any order, so screens and containers may not exist yet.
324 *
325 * @param key identifier for the row
326 * @param buffer the serialized proto from the stream, may be larger than dataSize
327 * @param dataSize the size of the proto from the stream
328 * @param keys keys to mark as clean in the notes for next backup
329 */
330 private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
331 Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)");
332 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
333 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
334
Chris Wren5dee7af2013-12-20 17:22:11 -0500335 if (!mRestoreEnabled) {
336 if (DEBUG) Log.v(TAG, "restore not enabled: skipping database mutation");
337 return;
338 }
339
Chris Wren1ada10d2013-09-13 18:01:38 -0400340 try {
Chris Wren5dee7af2013-12-20 17:22:11 -0500341 ContentResolver cr = mContext.getContentResolver();
342 ContentValues values = unpackFavorite(buffer, 0, dataSize);
343 cr.insert(Favorites.CONTENT_URI, values);
Chris Wren1ada10d2013-09-13 18:01:38 -0400344 } catch (InvalidProtocolBufferNanoException e) {
345 Log.w(TAG, "failed to decode proto", e);
346 }
347 }
348
349 /**
350 * Write all modified screens to the data stream.
351 *
352 *
353 * @param in notes from last backup
354 * @param data output stream for key/value pairs
355 * @param out notes about this backup
Chris Wren22e130d2013-09-23 18:25:57 -0400356 * @param keys keys to mark as clean in the notes for next backup
357 * @throws IOException
Chris Wren1ada10d2013-09-13 18:01:38 -0400358 */
359 private void backupScreens(Journal in, BackupDataOutput data, Journal out,
360 ArrayList<Key> keys)
361 throws IOException {
362 // read the old ID set
Chris Wren22e130d2013-09-23 18:25:57 -0400363 Set<String> savedIds = getSavedIdsByType(Key.SCREEN, in);
364 if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
Chris Wren1ada10d2013-09-13 18:01:38 -0400365
366 // persist things that have changed since the last backup
Chris Wren92aa4232013-10-04 11:29:36 -0400367 ContentResolver cr = mContext.getContentResolver();
Chris Wren22e130d2013-09-23 18:25:57 -0400368 Cursor cursor = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
369 null, null, null);
370 Set<String> currentIds = new HashSet<String>(cursor.getCount());
Chris Wren1ada10d2013-09-13 18:01:38 -0400371 try {
Chris Wren22e130d2013-09-23 18:25:57 -0400372 cursor.moveToPosition(-1);
Chris Wren5dee7af2013-12-20 17:22:11 -0500373 Log.d(TAG, "dumping screens after: " + in.t);
Chris Wren22e130d2013-09-23 18:25:57 -0400374 while(cursor.moveToNext()) {
375 final long id = cursor.getLong(ID_INDEX);
376 final long updateTime = cursor.getLong(ID_MODIFIED);
Chris Wren1ada10d2013-09-13 18:01:38 -0400377 Key key = getKey(Key.SCREEN, id);
Chris Wren1ada10d2013-09-13 18:01:38 -0400378 keys.add(key);
Chris Wren5743aa92014-01-10 18:02:06 -0500379 final String backupKey = keyToBackupKey(key);
380 currentIds.add(backupKey);
381 if (!savedIds.contains(backupKey) || updateTime >= in.t) {
Chris Wren22e130d2013-09-23 18:25:57 -0400382 byte[] blob = packScreen(cursor);
383 writeRowToBackup(key, blob, out, data);
Chris Wren5dee7af2013-12-20 17:22:11 -0500384 if (DEBUG) Log.d(TAG, "wrote screen " + id);
385 } else {
386 if (DEBUG) Log.d(TAG, "screen " + id + " was too old: " + updateTime);
Chris Wren22e130d2013-09-23 18:25:57 -0400387 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400388 }
389 } finally {
Chris Wren22e130d2013-09-23 18:25:57 -0400390 cursor.close();
Chris Wren1ada10d2013-09-13 18:01:38 -0400391 }
392 if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
393
394 // these IDs must have been deleted
395 savedIds.removeAll(currentIds);
Chris Wren22e130d2013-09-23 18:25:57 -0400396 out.rows += removeDeletedKeysFromBackup(savedIds, data);
Chris Wren1ada10d2013-09-13 18:01:38 -0400397 }
398
399 /**
400 * Read a screen from the stream.
401 *
402 * <P>Keys arrive in any order, so children of this screen may already exist.
403 *
404 * @param key identifier for the row
405 * @param buffer the serialized proto from the stream, may be larger than dataSize
406 * @param dataSize the size of the proto from the stream
407 * @param keys keys to mark as clean in the notes for next backup
408 */
409 private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
410 Log.v(TAG, "unpacking screen " + key.id);
411 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
412 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
Chris Wren5dee7af2013-12-20 17:22:11 -0500413
414 if (!mRestoreEnabled) {
415 if (DEBUG) Log.v(TAG, "restore not enabled: skipping database mutation");
416 return;
417 }
418
Chris Wren1ada10d2013-09-13 18:01:38 -0400419 try {
Chris Wren5dee7af2013-12-20 17:22:11 -0500420 ContentResolver cr = mContext.getContentResolver();
421 ContentValues values = unpackScreen(buffer, 0, dataSize);
422 cr.insert(WorkspaceScreens.CONTENT_URI, values);
423
Chris Wren1ada10d2013-09-13 18:01:38 -0400424 } catch (InvalidProtocolBufferNanoException e) {
425 Log.w(TAG, "failed to decode proto", e);
426 }
427 }
428
Chris Wren22e130d2013-09-23 18:25:57 -0400429 /**
430 * Write all the static icon resources we need to render placeholders
431 * for a package that is not installed.
432 *
433 * @param in notes from last backup
434 * @param data output stream for key/value pairs
435 * @param out notes about this backup
436 * @param keys keys to mark as clean in the notes for next backup
437 * @throws IOException
438 */
439 private void backupIcons(Journal in, BackupDataOutput data, Journal out,
440 ArrayList<Key> keys) throws IOException {
Chris Wrenfd13c712013-09-27 15:45:19 -0400441 // persist icons that haven't been persisted yet
Chris Wren92aa4232013-10-04 11:29:36 -0400442 final LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
443 if (appState == null) {
444 dataChanged(); // try again later
Chris Wrend8fe6de2013-10-04 10:42:14 -0400445 if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying icon backup");
446 return;
447 }
Chris Wren92aa4232013-10-04 11:29:36 -0400448 final ContentResolver cr = mContext.getContentResolver();
449 final IconCache iconCache = appState.getIconCache();
450 final int dpi = mContext.getResources().getDisplayMetrics().densityDpi;
Chris Wren22e130d2013-09-23 18:25:57 -0400451
452 // read the old ID set
453 Set<String> savedIds = getSavedIdsByType(Key.ICON, in);
454 if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size());
455
456 int startRows = out.rows;
457 if (DEBUG) Log.d(TAG, "starting here: " + startRows);
458 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION;
459 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
460 where, null, null);
461 Set<String> currentIds = new HashSet<String>(cursor.getCount());
462 try {
463 cursor.moveToPosition(-1);
464 while(cursor.moveToNext()) {
465 final long id = cursor.getLong(ID_INDEX);
466 final String intentDescription = cursor.getString(INTENT_INDEX);
467 try {
468 Intent intent = Intent.parseUri(intentDescription, 0);
469 ComponentName cn = intent.getComponent();
470 Key key = null;
471 String backupKey = null;
472 if (cn != null) {
473 key = getKey(Key.ICON, cn.flattenToShortString());
474 backupKey = keyToBackupKey(key);
475 currentIds.add(backupKey);
476 } else {
477 Log.w(TAG, "empty intent on application favorite: " + id);
478 }
479 if (savedIds.contains(backupKey)) {
480 if (DEBUG) Log.d(TAG, "already saved icon " + backupKey);
481
482 // remember that we already backed this up previously
483 keys.add(key);
484 } else if (backupKey != null) {
485 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
486 if ((out.rows - startRows) < MAX_ICONS_PER_PASS) {
487 if (DEBUG) Log.d(TAG, "saving icon " + backupKey);
488 Bitmap icon = iconCache.getIcon(intent);
489 keys.add(key);
490 if (icon != null && !iconCache.isDefaultIcon(icon)) {
491 byte[] blob = packIcon(dpi, icon);
492 writeRowToBackup(key, blob, out, data);
493 }
494 } else {
Chris Wrenfd13c712013-09-27 15:45:19 -0400495 if (DEBUG) Log.d(TAG, "scheduling another run for icon " + backupKey);
Chris Wren22e130d2013-09-23 18:25:57 -0400496 // too many icons for this pass, request another.
Chris Wren92aa4232013-10-04 11:29:36 -0400497 dataChanged();
Chris Wren22e130d2013-09-23 18:25:57 -0400498 }
499 }
500 } catch (URISyntaxException e) {
501 Log.w(TAG, "invalid URI on application favorite: " + id);
502 } catch (IOException e) {
503 Log.w(TAG, "unable to save application icon for favorite: " + id);
504 }
505
506 }
507 } finally {
508 cursor.close();
509 }
510 if (DEBUG) Log.d(TAG, "icon currentIds.size()=" + currentIds.size());
511
512 // these IDs must have been deleted
513 savedIds.removeAll(currentIds);
514 out.rows += removeDeletedKeysFromBackup(savedIds, data);
515 }
516
517 /**
518 * Read an icon from the stream.
519 *
Chris Wrenfd13c712013-09-27 15:45:19 -0400520 * <P>Keys arrive in any order, so shortcuts that use this icon may already exist.
Chris Wren22e130d2013-09-23 18:25:57 -0400521 *
522 * @param key identifier for the row
523 * @param buffer the serialized proto from the stream, may be larger than dataSize
524 * @param dataSize the size of the proto from the stream
525 * @param keys keys to mark as clean in the notes for next backup
526 */
527 private void restoreIcon(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
528 Log.v(TAG, "unpacking icon " + key.id);
529 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
530 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
531 try {
532 Resource res = unpackIcon(buffer, 0, dataSize);
533 if (DEBUG) Log.d(TAG, "unpacked " + res.dpi);
534 if (DEBUG) Log.d(TAG, "read " +
535 Base64.encodeToString(res.data, 0, res.data.length,
536 Base64.NO_WRAP));
537 Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length);
538 if (icon == null) {
539 Log.w(TAG, "failed to unpack icon for " + key.name);
540 }
Chris Wren5dee7af2013-12-20 17:22:11 -0500541
542 if (!mRestoreEnabled) {
543 if (DEBUG) Log.v(TAG, "restore not enabled: skipping database mutation");
544 return;
545 } else {
546 // future site of icon cache mutation
547 }
Chris Wren22e130d2013-09-23 18:25:57 -0400548 } catch (InvalidProtocolBufferNanoException e) {
549 Log.w(TAG, "failed to decode proto", e);
550 }
551 }
552
Chris Wrenfd13c712013-09-27 15:45:19 -0400553 /**
554 * Write all the static widget resources we need to render placeholders
555 * for a package that is not installed.
556 *
557 * @param in notes from last backup
558 * @param data output stream for key/value pairs
559 * @param out notes about this backup
560 * @param keys keys to mark as clean in the notes for next backup
561 * @throws IOException
562 */
563 private void backupWidgets(Journal in, BackupDataOutput data, Journal out,
564 ArrayList<Key> keys) throws IOException {
565 // persist static widget info that hasn't been persisted yet
Chris Wrend8fe6de2013-10-04 10:42:14 -0400566 final LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
567 if (appState == null) {
Chris Wren92aa4232013-10-04 11:29:36 -0400568 dataChanged(); // try again later
Chris Wrend8fe6de2013-10-04 10:42:14 -0400569 if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying widget backup");
570 return;
571 }
Chris Wren92aa4232013-10-04 11:29:36 -0400572 final ContentResolver cr = mContext.getContentResolver();
573 final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext);
574 final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext);
Chris Wrenfd13c712013-09-27 15:45:19 -0400575 final IconCache iconCache = appState.getIconCache();
Chris Wren92aa4232013-10-04 11:29:36 -0400576 final int dpi = mContext.getResources().getDisplayMetrics().densityDpi;
Chris Wrenfd13c712013-09-27 15:45:19 -0400577 final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile();
578 if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx);
579
580 // read the old ID set
581 Set<String> savedIds = getSavedIdsByType(Key.WIDGET, in);
582 if (DEBUG) Log.d(TAG, "widgets savedIds.size()=" + savedIds.size());
583
584 int startRows = out.rows;
585 if (DEBUG) Log.d(TAG, "starting here: " + startRows);
586 String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET;
587 Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
588 where, null, null);
589 Set<String> currentIds = new HashSet<String>(cursor.getCount());
590 try {
591 cursor.moveToPosition(-1);
592 while(cursor.moveToNext()) {
593 final long id = cursor.getLong(ID_INDEX);
594 final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX);
595 final int spanX = cursor.getInt(SPANX_INDEX);
596 final int spanY = cursor.getInt(SPANY_INDEX);
597 final ComponentName provider = ComponentName.unflattenFromString(providerName);
598 Key key = null;
599 String backupKey = null;
600 if (provider != null) {
601 key = getKey(Key.WIDGET, providerName);
602 backupKey = keyToBackupKey(key);
603 currentIds.add(backupKey);
604 } else {
605 Log.w(TAG, "empty intent on appwidget: " + id);
606 }
607 if (savedIds.contains(backupKey)) {
608 if (DEBUG) Log.d(TAG, "already saved widget " + backupKey);
609
610 // remember that we already backed this up previously
611 keys.add(key);
612 } else if (backupKey != null) {
613 if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows);
614 if ((out.rows - startRows) < MAX_WIDGETS_PER_PASS) {
615 if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
616 previewLoader.setPreviewSize(spanX * profile.cellWidthPx,
617 spanY * profile.cellHeightPx, widgetSpacingLayout);
618 byte[] blob = packWidget(dpi, previewLoader, iconCache, provider);
Chris Wrenb1fd63b2013-10-03 15:43:58 -0400619 keys.add(key);
Chris Wrenfd13c712013-09-27 15:45:19 -0400620 writeRowToBackup(key, blob, out, data);
621
622 } else {
623 if (DEBUG) Log.d(TAG, "scheduling another run for widget " + backupKey);
624 // too many widgets for this pass, request another.
Chris Wren92aa4232013-10-04 11:29:36 -0400625 dataChanged();
Chris Wrenfd13c712013-09-27 15:45:19 -0400626 }
627 }
628 }
629 } finally {
630 cursor.close();
631 }
632 if (DEBUG) Log.d(TAG, "widget currentIds.size()=" + currentIds.size());
633
634 // these IDs must have been deleted
635 savedIds.removeAll(currentIds);
636 out.rows += removeDeletedKeysFromBackup(savedIds, data);
637 }
638
639 /**
640 * Read a widget from the stream.
641 *
642 * <P>Keys arrive in any order, so widgets that use this data may already exist.
643 *
644 * @param key identifier for the row
645 * @param buffer the serialized proto from the stream, may be larger than dataSize
646 * @param dataSize the size of the proto from the stream
647 * @param keys keys to mark as clean in the notes for next backup
648 */
649 private void restoreWidget(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
650 Log.v(TAG, "unpacking widget " + key.id);
651 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
652 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
653 try {
654 Widget widget = unpackWidget(buffer, 0, dataSize);
655 if (DEBUG) Log.d(TAG, "unpacked " + widget.provider);
656 if (widget.icon.data != null) {
657 Bitmap icon = BitmapFactory
658 .decodeByteArray(widget.icon.data, 0, widget.icon.data.length);
659 if (icon == null) {
660 Log.w(TAG, "failed to unpack widget icon for " + key.name);
661 }
662 }
Chris Wren5dee7af2013-12-20 17:22:11 -0500663
664 if (!mRestoreEnabled) {
665 if (DEBUG) Log.v(TAG, "restore not enabled: skipping database mutation");
666 return;
667 } else {
668 // future site of widget table mutation
669 }
Chris Wrenfd13c712013-09-27 15:45:19 -0400670 } catch (InvalidProtocolBufferNanoException e) {
671 Log.w(TAG, "failed to decode proto", e);
672 }
673 }
674
Chris Wren22e130d2013-09-23 18:25:57 -0400675 /** create a new key, with an integer ID.
Chris Wren1ada10d2013-09-13 18:01:38 -0400676 *
677 * <P> Keys contain their own checksum instead of using
678 * the heavy-weight CheckedMessage wrapper.
679 */
680 private Key getKey(int type, long id) {
681 Key key = new Key();
682 key.type = type;
683 key.id = id;
684 key.checksum = checkKey(key);
685 return key;
686 }
687
Chris Wren22e130d2013-09-23 18:25:57 -0400688 /** create a new key for a named object.
689 *
690 * <P> Keys contain their own checksum instead of using
691 * the heavy-weight CheckedMessage wrapper.
692 */
693 private Key getKey(int type, String name) {
694 Key key = new Key();
695 key.type = type;
696 key.name = name;
697 key.checksum = checkKey(key);
698 return key;
699 }
700
Chris Wren1ada10d2013-09-13 18:01:38 -0400701 /** keys need to be strings, serialize and encode. */
702 private String keyToBackupKey(Key key) {
Chris Wren978194c2013-10-03 17:47:22 -0400703 return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP);
Chris Wren1ada10d2013-09-13 18:01:38 -0400704 }
705
706 /** keys need to be strings, decode and parse. */
707 private Key backupKeyToKey(String backupKey) throws KeyParsingException {
708 try {
709 Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT));
710 if (key.checksum != checkKey(key)) {
711 key = null;
712 throw new KeyParsingException("invalid key read from stream" + backupKey);
713 }
714 return key;
715 } catch (InvalidProtocolBufferNanoException e) {
716 throw new KeyParsingException(e);
717 } catch (IllegalArgumentException e) {
718 throw new KeyParsingException(e);
719 }
720 }
721
Chris Wren22e130d2013-09-23 18:25:57 -0400722 private String getKeyName(Key key) {
723 if (TextUtils.isEmpty(key.name)) {
724 return Long.toString(key.id);
725 } else {
726 return key.name;
727 }
728
729 }
730
731 private String geKeyType(Key key) {
732 switch (key.type) {
733 case Key.FAVORITE:
734 return "favorite";
735 case Key.SCREEN:
736 return "screen";
737 case Key.ICON:
738 return "icon";
Chris Wrenfd13c712013-09-27 15:45:19 -0400739 case Key.WIDGET:
740 return "widget";
Chris Wren22e130d2013-09-23 18:25:57 -0400741 default:
742 return "anonymous";
743 }
744 }
745
Chris Wren1ada10d2013-09-13 18:01:38 -0400746 /** Compute the checksum over the important bits of a key. */
747 private long checkKey(Key key) {
748 CRC32 checksum = new CRC32();
749 checksum.update(key.type);
750 checksum.update((int) (key.id & 0xffff));
751 checksum.update((int) ((key.id >> 32) & 0xffff));
752 if (!TextUtils.isEmpty(key.name)) {
753 checksum.update(key.name.getBytes());
754 }
755 return checksum.getValue();
756 }
757
758 /** Serialize a Favorite for persistence, including a checksum wrapper. */
759 private byte[] packFavorite(Cursor c) {
760 Favorite favorite = new Favorite();
761 favorite.id = c.getLong(ID_INDEX);
762 favorite.screen = c.getInt(SCREEN_INDEX);
763 favorite.container = c.getInt(CONTAINER_INDEX);
764 favorite.cellX = c.getInt(CELLX_INDEX);
765 favorite.cellY = c.getInt(CELLY_INDEX);
766 favorite.spanX = c.getInt(SPANX_INDEX);
767 favorite.spanY = c.getInt(SPANY_INDEX);
768 favorite.iconType = c.getInt(ICON_TYPE_INDEX);
769 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
770 String iconPackage = c.getString(ICON_PACKAGE_INDEX);
771 if (!TextUtils.isEmpty(iconPackage)) {
772 favorite.iconPackage = iconPackage;
773 }
774 String iconResource = c.getString(ICON_RESOURCE_INDEX);
775 if (!TextUtils.isEmpty(iconResource)) {
776 favorite.iconResource = iconResource;
777 }
778 }
779 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
780 byte[] blob = c.getBlob(ICON_INDEX);
781 if (blob != null && blob.length > 0) {
782 favorite.icon = blob;
783 }
784 }
785 String title = c.getString(TITLE_INDEX);
786 if (!TextUtils.isEmpty(title)) {
787 favorite.title = title;
788 }
789 String intent = c.getString(INTENT_INDEX);
790 if (!TextUtils.isEmpty(intent)) {
791 favorite.intent = intent;
792 }
793 favorite.itemType = c.getInt(ITEM_TYPE_INDEX);
794 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
795 favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX);
796 String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX);
797 if (!TextUtils.isEmpty(appWidgetProvider)) {
798 favorite.appWidgetProvider = appWidgetProvider;
799 }
800 }
801
802 return writeCheckedBytes(favorite);
803 }
804
805 /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */
Chris Wren5dee7af2013-12-20 17:22:11 -0500806 private ContentValues unpackFavorite(byte[] buffer, int offset, int dataSize)
Chris Wren1ada10d2013-09-13 18:01:38 -0400807 throws InvalidProtocolBufferNanoException {
808 Favorite favorite = new Favorite();
809 MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize));
Chris Wren5dee7af2013-12-20 17:22:11 -0500810 if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType + ", " + favorite.id);
811 ContentValues values = new ContentValues();
812 values.put(Favorites._ID, favorite.id);
813 values.put(Favorites.SCREEN, favorite.screen);
814 values.put(Favorites.CONTAINER, favorite.container);
815 values.put(Favorites.CELLX, favorite.cellX);
816 values.put(Favorites.CELLY, favorite.cellY);
817 values.put(Favorites.SPANX, favorite.spanX);
818 values.put(Favorites.SPANY, favorite.spanY);
819 values.put(Favorites.ICON_TYPE, favorite.iconType);
820 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
821 values.put(Favorites.ICON_PACKAGE, favorite.iconPackage);
822 values.put(Favorites.ICON_RESOURCE, favorite.iconResource);
823 }
824 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
825 values.put(Favorites.ICON, favorite.icon);
826 }
827 if (!TextUtils.isEmpty(favorite.title)) {
828 values.put(Favorites.TITLE, favorite.title);
829 } else {
830 values.put(Favorites.TITLE, "");
831 }
832 if (!TextUtils.isEmpty(favorite.intent)) {
833 values.put(Favorites.INTENT, favorite.intent);
834 }
835 values.put(Favorites.ITEM_TYPE, favorite.itemType);
836 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
837 if (!TextUtils.isEmpty(favorite.appWidgetProvider)) {
838 values.put(Favorites.APPWIDGET_PROVIDER, favorite.appWidgetProvider);
839 }
840 values.put(Favorites.APPWIDGET_ID, favorite.appWidgetId);
841 }
842 return values;
Chris Wren1ada10d2013-09-13 18:01:38 -0400843 }
844
845 /** Serialize a Screen for persistence, including a checksum wrapper. */
846 private byte[] packScreen(Cursor c) {
847 Screen screen = new Screen();
848 screen.id = c.getLong(ID_INDEX);
849 screen.rank = c.getInt(SCREEN_RANK_INDEX);
850
851 return writeCheckedBytes(screen);
852 }
853
854 /** Deserialize a Screen from persistence, after verifying checksum wrapper. */
Chris Wren5dee7af2013-12-20 17:22:11 -0500855 private ContentValues unpackScreen(byte[] buffer, int offset, int dataSize)
Chris Wren1ada10d2013-09-13 18:01:38 -0400856 throws InvalidProtocolBufferNanoException {
857 Screen screen = new Screen();
858 MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize));
Chris Wren5dee7af2013-12-20 17:22:11 -0500859 if (DEBUG) Log.d(TAG, "unpacked " + screen.id + "/" + screen.rank);
860 ContentValues values = new ContentValues();
861 values.put(WorkspaceScreens._ID, screen.id);
862 values.put(WorkspaceScreens.SCREEN_RANK, screen.rank);
863 return values;
Chris Wren1ada10d2013-09-13 18:01:38 -0400864 }
865
Chris Wren22e130d2013-09-23 18:25:57 -0400866 /** Serialize an icon Resource for persistence, including a checksum wrapper. */
867 private byte[] packIcon(int dpi, Bitmap icon) {
868 Resource res = new Resource();
869 res.dpi = dpi;
870 ByteArrayOutputStream os = new ByteArrayOutputStream();
Chris Wrenb86f0762013-10-04 10:10:21 -0400871 if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
Chris Wren22e130d2013-09-23 18:25:57 -0400872 res.data = os.toByteArray();
873 }
874 return writeCheckedBytes(res);
875 }
876
877 /** Deserialize an icon resource from persistence, after verifying checksum wrapper. */
878 private Resource unpackIcon(byte[] buffer, int offset, int dataSize)
879 throws InvalidProtocolBufferNanoException {
880 Resource res = new Resource();
881 MessageNano.mergeFrom(res, readCheckedBytes(buffer, offset, dataSize));
882 return res;
883 }
884
Chris Wrenfd13c712013-09-27 15:45:19 -0400885 /** Serialize a widget for persistence, including a checksum wrapper. */
886 private byte[] packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache,
887 ComponentName provider) {
888 final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider);
889 Widget widget = new Widget();
890 widget.provider = provider.flattenToShortString();
891 widget.label = info.label;
892 widget.configure = info.configure != null;
893 if (info.icon != 0) {
894 widget.icon = new Resource();
895 Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon);
Chris Wren92aa4232013-10-04 11:29:36 -0400896 Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext);
Chris Wrenfd13c712013-09-27 15:45:19 -0400897 ByteArrayOutputStream os = new ByteArrayOutputStream();
Chris Wrenb86f0762013-10-04 10:10:21 -0400898 if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
Chris Wrenfd13c712013-09-27 15:45:19 -0400899 widget.icon.data = os.toByteArray();
900 widget.icon.dpi = dpi;
901 }
902 }
903 if (info.previewImage != 0) {
904 widget.preview = new Resource();
905 Bitmap preview = previewLoader.generateWidgetPreview(info, null);
906 ByteArrayOutputStream os = new ByteArrayOutputStream();
Chris Wrenb86f0762013-10-04 10:10:21 -0400907 if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
Chris Wrenfd13c712013-09-27 15:45:19 -0400908 widget.preview.data = os.toByteArray();
909 widget.preview.dpi = dpi;
910 }
911 }
912 return writeCheckedBytes(widget);
913 }
914
915 /** Deserialize a widget from persistence, after verifying checksum wrapper. */
916 private Widget unpackWidget(byte[] buffer, int offset, int dataSize)
917 throws InvalidProtocolBufferNanoException {
918 Widget widget = new Widget();
919 MessageNano.mergeFrom(widget, readCheckedBytes(buffer, offset, dataSize));
920 return widget;
921 }
922
Chris Wren1ada10d2013-09-13 18:01:38 -0400923 /**
924 * Read the old journal from the input file.
925 *
926 * In the event of any error, just pretend we didn't have a journal,
927 * in that case, do a full backup.
928 *
929 * @param oldState the read-0only file descriptor pointing to the old journal
Chris Wren65b6a602014-01-10 14:11:25 -0500930 * @return a Journal protocol buffer
Chris Wren1ada10d2013-09-13 18:01:38 -0400931 */
932 private Journal readJournal(ParcelFileDescriptor oldState) {
Chris Wren1ada10d2013-09-13 18:01:38 -0400933 Journal journal = new Journal();
Chris Wren92aa4232013-10-04 11:29:36 -0400934 if (oldState == null) {
935 return journal;
936 }
937 FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor());
938 try {
Chris Wren65b6a602014-01-10 14:11:25 -0500939 int availableBytes = inStream.available();
940 if (DEBUG) Log.d(TAG, "available " + availableBytes);
941 if (availableBytes < MAX_JOURNAL_SIZE) {
942 byte[] buffer = new byte[availableBytes];
Chris Wren1ada10d2013-09-13 18:01:38 -0400943 int bytesRead = 0;
Chris Wren65b6a602014-01-10 14:11:25 -0500944 boolean valid = false;
945 while (availableBytes > 0) {
Chris Wren92aa4232013-10-04 11:29:36 -0400946 try {
Chris Wren65b6a602014-01-10 14:11:25 -0500947 // OMG what are you doing? This is crazy inefficient!
948 // If we read a byte that is not ours, we will cause trouble: b/12491813
949 // However, we don't know how many bytes to expect (oops).
950 // So we have to step through *slowly*, watching for the end.
951 int result = inStream.read(buffer, bytesRead, 1);
Chris Wren92aa4232013-10-04 11:29:36 -0400952 if (result > 0) {
Chris Wren65b6a602014-01-10 14:11:25 -0500953 availableBytes -= result;
Chris Wren92aa4232013-10-04 11:29:36 -0400954 bytesRead += result;
Chris Wren65b6a602014-01-10 14:11:25 -0500955 if (DEBUG && (bytesRead % 100 == 0)) {
956 Log.d(TAG, "read some bytes: " + bytesRead);
957 }
Chris Wren92aa4232013-10-04 11:29:36 -0400958 } else {
Chris Wren65b6a602014-01-10 14:11:25 -0500959 Log.w(TAG, "unexpected end of file while reading journal.");
960 // stop reading and see what there is to parse
961 availableBytes = 0;
Chris Wren92aa4232013-10-04 11:29:36 -0400962 }
963 } catch (IOException e) {
Chris Wren65b6a602014-01-10 14:11:25 -0500964 Log.e(TAG, "failed to read the journal", e);
Chris Wren92aa4232013-10-04 11:29:36 -0400965 buffer = null;
Chris Wren65b6a602014-01-10 14:11:25 -0500966 availableBytes = 0;
967 }
968
969 // check the buffer to see if we have a valid journal
970 try {
971 MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, bytesRead));
972 // if we are here, then we have read a valid, checksum-verified journal
973 valid = true;
974 availableBytes = 0;
975 } catch (InvalidProtocolBufferNanoException e) {
976 // if we don't have the whole journal yet, mergeFrom will throw. keep going.
977 journal.clear();
Chris Wren92aa4232013-10-04 11:29:36 -0400978 }
Chris Wren1ada10d2013-09-13 18:01:38 -0400979 }
Chris Wren92aa4232013-10-04 11:29:36 -0400980 if (DEBUG) Log.d(TAG, "journal bytes read: " + bytesRead);
Chris Wren65b6a602014-01-10 14:11:25 -0500981 if (!valid) {
982 Log.w(TAG, "failed to read the journal: could not find a valid journal");
Chris Wren1ada10d2013-09-13 18:01:38 -0400983 }
984 }
Chris Wren92aa4232013-10-04 11:29:36 -0400985 } catch (IOException e) {
986 Log.d(TAG, "failed to close the journal", e);
987 } finally {
Chris Wren1ada10d2013-09-13 18:01:38 -0400988 try {
989 inStream.close();
990 } catch (IOException e) {
991 Log.d(TAG, "failed to close the journal", e);
992 }
993 }
994 return journal;
995 }
996
Chris Wren22e130d2013-09-23 18:25:57 -0400997 private void writeRowToBackup(Key key, byte[] blob, Journal out,
998 BackupDataOutput data) throws IOException {
999 String backupKey = keyToBackupKey(key);
1000 data.writeEntityHeader(backupKey, blob.length);
1001 data.writeEntityData(blob, blob.length);
1002 out.rows++;
1003 out.bytes += blob.length;
1004 Log.v(TAG, "saving " + geKeyType(key) + " " + backupKey + ": " +
1005 getKeyName(key) + "/" + blob.length);
Chris Wren92aa4232013-10-04 11:29:36 -04001006 if(DEBUG_PAYLOAD) {
Chris Wren2b6c21d2013-10-02 14:16:04 -04001007 String encoded = Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP);
1008 final int chunkSize = 1024;
1009 for (int offset = 0; offset < encoded.length(); offset += chunkSize) {
1010 int end = offset + chunkSize;
1011 end = Math.min(end, encoded.length());
1012 Log.d(TAG, "wrote " + encoded.substring(offset, end));
1013 }
1014 }
Chris Wren22e130d2013-09-23 18:25:57 -04001015 }
1016
1017 private Set<String> getSavedIdsByType(int type, Journal in) {
1018 Set<String> savedIds = new HashSet<String>();
1019 for(int i = 0; i < in.key.length; i++) {
1020 Key key = in.key[i];
1021 if (key.type == type) {
1022 savedIds.add(keyToBackupKey(key));
1023 }
1024 }
1025 return savedIds;
1026 }
1027
1028 private int removeDeletedKeysFromBackup(Set<String> deletedIds, BackupDataOutput data)
1029 throws IOException {
1030 int rows = 0;
1031 for(String deleted: deletedIds) {
1032 Log.v(TAG, "dropping icon " + deleted);
1033 data.writeEntityHeader(deleted, -1);
1034 rows++;
1035 }
1036 return rows;
1037 }
1038
Chris Wren1ada10d2013-09-13 18:01:38 -04001039 /**
1040 * Write the new journal to the output file.
1041 *
1042 * In the event of any error, just pretend we didn't have a journal,
1043 * in that case, do a full backup.
1044
1045 * @param newState the write-only file descriptor pointing to the new journal
1046 * @param journal a Journal protocol buffer
1047 */
1048 private void writeJournal(ParcelFileDescriptor newState, Journal journal) {
1049 FileOutputStream outStream = null;
1050 try {
1051 outStream = new FileOutputStream(newState.getFileDescriptor());
Chris Wren65b6a602014-01-10 14:11:25 -05001052 final byte[] journalBytes = writeCheckedBytes(journal);
1053 if (DEBUG) Log.d(TAG, "writing " + journalBytes.length + " bytes of journal");
1054 outStream.write(journalBytes);
Chris Wren1ada10d2013-09-13 18:01:38 -04001055 outStream.close();
1056 } catch (IOException e) {
1057 Log.d(TAG, "failed to write backup journal", e);
1058 }
1059 }
1060
1061 /** Wrap a proto in a CheckedMessage and compute the checksum. */
1062 private byte[] writeCheckedBytes(MessageNano proto) {
1063 CheckedMessage wrapper = new CheckedMessage();
1064 wrapper.payload = MessageNano.toByteArray(proto);
1065 CRC32 checksum = new CRC32();
1066 checksum.update(wrapper.payload);
1067 wrapper.checksum = checksum.getValue();
1068 return MessageNano.toByteArray(wrapper);
1069 }
1070
1071 /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */
1072 private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize)
1073 throws InvalidProtocolBufferNanoException {
1074 CheckedMessage wrapper = new CheckedMessage();
1075 MessageNano.mergeFrom(wrapper, buffer, offset, dataSize);
1076 CRC32 checksum = new CRC32();
1077 checksum.update(wrapper.payload);
1078 if (wrapper.checksum != checksum.getValue()) {
1079 throw new InvalidProtocolBufferNanoException("checksum does not match");
1080 }
1081 return wrapper.payload;
1082 }
1083
Chris Wrenfd13c712013-09-27 15:45:19 -04001084 private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) {
1085 if (mWidgetMap == null) {
1086 List<AppWidgetProviderInfo> widgets =
Chris Wren92aa4232013-10-04 11:29:36 -04001087 AppWidgetManager.getInstance(mContext).getInstalledProviders();
Chris Wrenfd13c712013-09-27 15:45:19 -04001088 mWidgetMap = new HashMap<ComponentName, AppWidgetProviderInfo>(widgets.size());
1089 for (AppWidgetProviderInfo info : widgets) {
1090 mWidgetMap.put(info.provider, info);
1091 }
1092 }
1093 return mWidgetMap.get(component);
1094 }
1095
Chris Wren1ada10d2013-09-13 18:01:38 -04001096 private class KeyParsingException extends Throwable {
1097 private KeyParsingException(Throwable cause) {
1098 super(cause);
1099 }
1100
1101 public KeyParsingException(String reason) {
1102 super(reason);
1103 }
1104 }
1105}