blob: bb15ca18484a4b869bf5e84ab956f1feac22c5eb [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
22import com.android.launcher3.LauncherSettings.ChangeLogColumns;
23import com.android.launcher3.LauncherSettings.Favorites;
24import com.android.launcher3.LauncherSettings.WorkspaceScreens;
25import com.android.launcher3.backup.BackupProtos;
26import com.android.launcher3.backup.BackupProtos.CheckedMessage;
27import com.android.launcher3.backup.BackupProtos.Favorite;
28import com.android.launcher3.backup.BackupProtos.Journal;
29import com.android.launcher3.backup.BackupProtos.Key;
30import com.android.launcher3.backup.BackupProtos.Screen;
31
32import android.app.backup.BackupAgent;
33import android.app.backup.BackupDataInput;
34import android.app.backup.BackupDataOutput;
35import android.app.backup.BackupManager;
36import android.content.ContentResolver;
37import android.content.Context;
38import android.database.Cursor;
39import android.os.ParcelFileDescriptor;
40import android.provider.BaseColumns;
41import android.text.TextUtils;
42import android.util.Base64;
43import android.util.Log;
44
45import java.io.FileInputStream;
46import java.io.FileOutputStream;
47import java.io.IOException;
48import java.util.ArrayList;
49import java.util.HashSet;
50import java.util.Set;
51import java.util.zip.CRC32;
52
53/**
54 * Persist the launcher home state across calamities.
55 */
56public class LauncherBackupAgent extends BackupAgent {
57
58 private static final String TAG = "LauncherBackupAgent";
59 private static final boolean DEBUG = false;
60
61 private static final int MAX_JOURNAL_SIZE = 1000000;
62
63 private static BackupManager sBackupManager;
64
65 private static final String[] FAVORITE_PROJECTION = {
66 Favorites._ID, // 0
67 Favorites.APPWIDGET_ID, // 1
68 Favorites.APPWIDGET_PROVIDER, // 2
69 Favorites.CELLX, // 3
70 Favorites.CELLY, // 4
71 Favorites.CONTAINER, // 5
72 Favorites.ICON, // 6
73 Favorites.ICON_PACKAGE, // 7
74 Favorites.ICON_RESOURCE, // 8
75 Favorites.ICON_TYPE, // 9
76 Favorites.ITEM_TYPE, // 10
77 Favorites.INTENT, // 11
78 Favorites.SCREEN, // 12
79 Favorites.SPANX, // 13
80 Favorites.SPANY, // 14
81 Favorites.TITLE, // 15
82 };
83
84 private static final int ID_INDEX = 0;
85 private static final int APPWIDGET_ID_INDEX = 1;
86 private static final int APPWIDGET_PROVIDER_INDEX = 2;
87 private static final int CELLX_INDEX = 3;
88 private static final int CELLY_INDEX = 4;
89 private static final int CONTAINER_INDEX = 5;
90 private static final int ICON_INDEX = 6;
91 private static final int ICON_PACKAGE_INDEX = 7;
92 private static final int ICON_RESOURCE_INDEX = 8;
93 private static final int ICON_TYPE_INDEX = 9;
94 private static final int ITEM_TYPE_INDEX = 10;
95 private static final int INTENT_INDEX = 11;
96 private static final int SCREEN_INDEX = 12;
97 private static final int SPANX_INDEX = 13 ;
98 private static final int SPANY_INDEX = 14;
99 private static final int TITLE_INDEX = 15;
100
101 private static final String[] SCREEN_PROJECTION = {
102 WorkspaceScreens._ID, // 0
103 WorkspaceScreens.SCREEN_RANK // 1
104 };
105
106 private static final int SCREEN_RANK_INDEX = 1;
107
108 private static final String[] ID_ONLY_PROJECTION = {
109 BaseColumns._ID
110 };
111
112
113 /**
114 * Notify the backup manager that out database is dirty.
115 *
116 * <P>This does not force an immediate backup.
117 *
118 * @param context application context
119 */
120 public static void dataChanged(Context context) {
121 if (sBackupManager == null) {
122 sBackupManager = new BackupManager(context);
123 }
124 sBackupManager.dataChanged();
125 }
126
127 /**
128 * Back up launcher data so we can restore the user's state on a new device.
129 *
130 * <P>The journal is a timestamp and a list of keys that were saved as of that time.
131 *
132 * <P>Keys may come back in any order, so each key/value is one complete row of the database.
133 *
134 * @param oldState notes from the last backup
135 * @param data incremental key/value pairs to persist off-device
136 * @param newState notes for the next backup
137 * @throws IOException
138 */
139 @Override
140 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
141 ParcelFileDescriptor newState)
142 throws IOException {
143 Log.v(TAG, "onBackup");
144
145 Journal in = readJournal(oldState);
146 Journal out = new Journal();
147
148 long lastBackupTime = in.t;
149 out.t = System.currentTimeMillis();
150 out.rows = 0;
151 out.bytes = 0;
152
153 Log.v(TAG, "lastBackupTime=" + lastBackupTime);
154
155 ArrayList<Key> keys = new ArrayList<Key>();
156 backupFavorites(in, data, out, keys);
157 backupScreens(in, data, out, keys);
158
159 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
160 writeJournal(newState, out);
161 Log.v(TAG, "onBackup: wrote " + out.bytes + "b in " + out.rows + " rows.");
162
163 Log.v(TAG, "onBackup: finished");
164 }
165
166 /**
167 * Restore home screen from the restored data stream.
168 *
169 * <P>Keys may arrive in any order.
170 *
171 * @param data the key/value pairs from the server
172 * @param versionCode the version of the app that generated the data
173 * @param newState notes for the next backup
174 * @throws IOException
175 */
176 @Override
177 public void onRestore(BackupDataInput data, int versionCode, ParcelFileDescriptor newState)
178 throws IOException {
179 Log.v(TAG, "onRestore");
180 int numRows = 0;
181 Journal out = new Journal();
182
183 ArrayList<Key> keys = new ArrayList<Key>();
184 byte[] buffer = new byte[512];
185 while (data.readNextHeader()) {
186 numRows++;
187 String backupKey = data.getKey();
188 int dataSize = data.getDataSize();
189 if (buffer.length < dataSize) {
190 buffer = new byte[dataSize];
191 }
192 Key key = null;
193 int bytesRead = data.readEntityData(buffer, 0, dataSize);
194 if (DEBUG) {
195 Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available");
196 }
197 try {
198 key = backupKeyToKey(backupKey);
199 switch (key.type) {
200 case Key.FAVORITE:
201 restoreFavorite(key, buffer, dataSize, keys);
202 break;
203
204 case Key.SCREEN:
205 restoreScreen(key, buffer, dataSize, keys);
206 break;
207
208 default:
209 Log.w(TAG, "unknown restore entity type: " + key.type);
210 break;
211 }
212 } catch (KeyParsingException e) {
213 Log.w(TAG, "ignoring unparsable backup key: " + backupKey);
214 }
215 }
216
217 // clear the output journal time, to force a full backup to
218 // will catch any changes the restore process might have made
219 out.t = 0;
220 out.key = keys.toArray(BackupProtos.Key.EMPTY_ARRAY);
221 writeJournal(newState, out);
222 Log.v(TAG, "onRestore: read " + numRows + " rows");
223 }
224
225 /**
226 * Write all modified favorites to the data stream.
227 *
228 *
229 * @param in notes from last backup
230 * @param data output stream for key/value pairs
231 * @param out notes about this backup
232 * @param keys keys to mark as clean in the notes for next backup
233 * @throws IOException
234 */
235 private void backupFavorites(Journal in, BackupDataOutput data, Journal out,
236 ArrayList<Key> keys)
237 throws IOException {
238 // read the old ID set
239 Set<String> savedIds = new HashSet<String>();
240 for(int i = 0; i < in.key.length; i++) {
241 Key key = in.key[i];
242 if (key.type == Key.FAVORITE) {
243 savedIds.add(keyToBackupKey(key));
244 }
245 }
246 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
247
248 // persist things that have changed since the last backup
249 ContentResolver cr = getContentResolver();
250 String where = ChangeLogColumns.MODIFIED + " > ?";
251 String[] args = {Long.toString(in.t)};
252 String updateOrder = ChangeLogColumns.MODIFIED;
253 Cursor updated = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION,
254 where, args, updateOrder);
255 if (DEBUG) Log.d(TAG, "favorite updated.getCount()=" + updated.getCount());
256 try {
257 updated.moveToPosition(-1);
258 while(updated.moveToNext()) {
259 final long id = updated.getLong(ID_INDEX);
260 Key key = getKey(Key.FAVORITE, id);
261 byte[] blob = packFavorite(updated);
262 String backupKey = keyToBackupKey(key);
263 data.writeEntityHeader(backupKey, blob.length);
264 data.writeEntityData(blob, blob.length);
265 out.rows++;
266 out.bytes += blob.length;
267 Log.v(TAG, "saving favorite " + backupKey + ": " + id + "/" + blob.length);
268 if(DEBUG) Log.d(TAG, "wrote " +
269 Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
270 // remember that is was a new column, so we don't delete it.
271 savedIds.add(backupKey);
272 }
273 } finally {
274 updated.close();
275 }
276 if (DEBUG) Log.d(TAG, "favorite savedIds.size()=" + savedIds.size());
277
278 // build the current ID set
279 String idOrder = BaseColumns._ID;
280 Cursor idCursor = cr.query(Favorites.CONTENT_URI, ID_ONLY_PROJECTION,
281 null, null, idOrder);
282 Set<String> currentIds = new HashSet<String>(idCursor.getCount());
283 try {
284 idCursor.moveToPosition(-1);
285 while(idCursor.moveToNext()) {
286 Key key = getKey(Key.FAVORITE, idCursor.getLong(ID_INDEX));
287 currentIds.add(keyToBackupKey(key));
288 // save the IDs for next time
289 keys.add(key);
290 }
291 } finally {
292 idCursor.close();
293 }
294 if (DEBUG) Log.d(TAG, "favorite currentIds.size()=" + currentIds.size());
295
296 // these IDs must have been deleted
297 savedIds.removeAll(currentIds);
298 for (String deleted : savedIds) {
299 Log.v(TAG, "dropping favorite " + deleted);
300 data.writeEntityHeader(deleted, -1);
301 out.rows++;
302 }
303 }
304
305 /**
306 * Read a favorite from the stream.
307 *
308 * <P>Keys arrive in any order, so screens and containers may not exist yet.
309 *
310 * @param key identifier for the row
311 * @param buffer the serialized proto from the stream, may be larger than dataSize
312 * @param dataSize the size of the proto from the stream
313 * @param keys keys to mark as clean in the notes for next backup
314 */
315 private void restoreFavorite(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
316 Log.v(TAG, "unpacking favorite " + key.id + " (" + dataSize + " bytes)");
317 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
318 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
319
320 try {
321 Favorite favorite = unpackFavorite(buffer, 0, dataSize);
322 if (DEBUG) Log.d(TAG, "unpacked " + favorite.itemType);
323 } catch (InvalidProtocolBufferNanoException e) {
324 Log.w(TAG, "failed to decode proto", e);
325 }
326 }
327
328 /**
329 * Write all modified screens to the data stream.
330 *
331 *
332 * @param in notes from last backup
333 * @param data output stream for key/value pairs
334 * @param out notes about this backup
335 * @param keys keys to mark as clean in the notes for next backup @throws IOException
336 */
337 private void backupScreens(Journal in, BackupDataOutput data, Journal out,
338 ArrayList<Key> keys)
339 throws IOException {
340 // read the old ID set
341 Set<String> savedIds = new HashSet<String>();
342 for(int i = 0; i < in.key.length; i++) {
343 Key key = in.key[i];
344 if (key.type == Key.SCREEN) {
345 savedIds.add(keyToBackupKey(key));
346 }
347 }
348 if (DEBUG) Log.d(TAG, "screens savedIds.size()=" + savedIds.size());
349
350 // persist things that have changed since the last backup
351 ContentResolver cr = getContentResolver();
352 String where = ChangeLogColumns.MODIFIED + " > ?";
353 String[] args = {Long.toString(in.t)};
354 String updateOrder = ChangeLogColumns.MODIFIED;
355 Cursor updated = cr.query(WorkspaceScreens.CONTENT_URI, SCREEN_PROJECTION,
356 where, args, updateOrder);
357 updated.moveToPosition(-1);
358 if (DEBUG) Log.d(TAG, "screens updated.getCount()=" + updated.getCount());
359 try {
360 while(updated.moveToNext()) {
361 final long id = updated.getLong(ID_INDEX);
362 Key key = getKey(Key.SCREEN, id);
363 byte[] blob = packScreen(updated);
364 String backupKey = keyToBackupKey(key);
365 data.writeEntityHeader(backupKey, blob.length);
366 data.writeEntityData(blob, blob.length);
367 out.rows++;
368 out.bytes += blob.length;
369 Log.v(TAG, "saving screen " + backupKey + ": " + id + "/" + blob.length);
370 if(DEBUG) Log.d(TAG, "wrote " +
371 Base64.encodeToString(blob, 0, blob.length, Base64.NO_WRAP));
372 // remember that is was a new column, so we don't delete it.
373 savedIds.add(backupKey);
374 }
375 } finally {
376 updated.close();
377 }
378 if (DEBUG) Log.d(TAG, "screen savedIds.size()=" + savedIds.size());
379
380 // build the current ID set
381 String idOrder = BaseColumns._ID;
382 Cursor idCursor = cr.query(WorkspaceScreens.CONTENT_URI, ID_ONLY_PROJECTION,
383 null, null, idOrder);
384 idCursor.moveToPosition(-1);
385 Set<String> currentIds = new HashSet<String>(idCursor.getCount());
386 try {
387 while(idCursor.moveToNext()) {
388 Key key = getKey(Key.SCREEN, idCursor.getLong(ID_INDEX));
389 currentIds.add(keyToBackupKey(key));
390 // save the IDs for next time
391 keys.add(key);
392 }
393 } finally {
394 idCursor.close();
395 }
396 if (DEBUG) Log.d(TAG, "screen currentIds.size()=" + currentIds.size());
397
398 // these IDs must have been deleted
399 savedIds.removeAll(currentIds);
400 for(String deleted: savedIds) {
401 Log.v(TAG, "dropping screen " + deleted);
402 data.writeEntityHeader(deleted, -1);
403 out.rows++;
404 }
405 }
406
407 /**
408 * Read a screen from the stream.
409 *
410 * <P>Keys arrive in any order, so children of this screen may already exist.
411 *
412 * @param key identifier for the row
413 * @param buffer the serialized proto from the stream, may be larger than dataSize
414 * @param dataSize the size of the proto from the stream
415 * @param keys keys to mark as clean in the notes for next backup
416 */
417 private void restoreScreen(Key key, byte[] buffer, int dataSize, ArrayList<Key> keys) {
418 Log.v(TAG, "unpacking screen " + key.id);
419 if (DEBUG) Log.d(TAG, "read (" + buffer.length + "): " +
420 Base64.encodeToString(buffer, 0, dataSize, Base64.NO_WRAP));
421 try {
422 Screen screen = unpackScreen(buffer, 0, dataSize);
423 if (DEBUG) Log.d(TAG, "unpacked " + screen.rank);
424 } catch (InvalidProtocolBufferNanoException e) {
425 Log.w(TAG, "failed to decode proto", e);
426 }
427 }
428
429 /** create a new key object.
430 *
431 * <P> Keys contain their own checksum instead of using
432 * the heavy-weight CheckedMessage wrapper.
433 */
434 private Key getKey(int type, long id) {
435 Key key = new Key();
436 key.type = type;
437 key.id = id;
438 key.checksum = checkKey(key);
439 return key;
440 }
441
442 /** keys need to be strings, serialize and encode. */
443 private String keyToBackupKey(Key key) {
444 return Base64.encodeToString(Key.toByteArray(key), Base64.NO_WRAP | Base64.NO_PADDING);
445 }
446
447 /** keys need to be strings, decode and parse. */
448 private Key backupKeyToKey(String backupKey) throws KeyParsingException {
449 try {
450 Key key = Key.parseFrom(Base64.decode(backupKey, Base64.DEFAULT));
451 if (key.checksum != checkKey(key)) {
452 key = null;
453 throw new KeyParsingException("invalid key read from stream" + backupKey);
454 }
455 return key;
456 } catch (InvalidProtocolBufferNanoException e) {
457 throw new KeyParsingException(e);
458 } catch (IllegalArgumentException e) {
459 throw new KeyParsingException(e);
460 }
461 }
462
463 /** Compute the checksum over the important bits of a key. */
464 private long checkKey(Key key) {
465 CRC32 checksum = new CRC32();
466 checksum.update(key.type);
467 checksum.update((int) (key.id & 0xffff));
468 checksum.update((int) ((key.id >> 32) & 0xffff));
469 if (!TextUtils.isEmpty(key.name)) {
470 checksum.update(key.name.getBytes());
471 }
472 return checksum.getValue();
473 }
474
475 /** Serialize a Favorite for persistence, including a checksum wrapper. */
476 private byte[] packFavorite(Cursor c) {
477 Favorite favorite = new Favorite();
478 favorite.id = c.getLong(ID_INDEX);
479 favorite.screen = c.getInt(SCREEN_INDEX);
480 favorite.container = c.getInt(CONTAINER_INDEX);
481 favorite.cellX = c.getInt(CELLX_INDEX);
482 favorite.cellY = c.getInt(CELLY_INDEX);
483 favorite.spanX = c.getInt(SPANX_INDEX);
484 favorite.spanY = c.getInt(SPANY_INDEX);
485 favorite.iconType = c.getInt(ICON_TYPE_INDEX);
486 if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) {
487 String iconPackage = c.getString(ICON_PACKAGE_INDEX);
488 if (!TextUtils.isEmpty(iconPackage)) {
489 favorite.iconPackage = iconPackage;
490 }
491 String iconResource = c.getString(ICON_RESOURCE_INDEX);
492 if (!TextUtils.isEmpty(iconResource)) {
493 favorite.iconResource = iconResource;
494 }
495 }
496 if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) {
497 byte[] blob = c.getBlob(ICON_INDEX);
498 if (blob != null && blob.length > 0) {
499 favorite.icon = blob;
500 }
501 }
502 String title = c.getString(TITLE_INDEX);
503 if (!TextUtils.isEmpty(title)) {
504 favorite.title = title;
505 }
506 String intent = c.getString(INTENT_INDEX);
507 if (!TextUtils.isEmpty(intent)) {
508 favorite.intent = intent;
509 }
510 favorite.itemType = c.getInt(ITEM_TYPE_INDEX);
511 if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
512 favorite.appWidgetId = c.getInt(APPWIDGET_ID_INDEX);
513 String appWidgetProvider = c.getString(APPWIDGET_PROVIDER_INDEX);
514 if (!TextUtils.isEmpty(appWidgetProvider)) {
515 favorite.appWidgetProvider = appWidgetProvider;
516 }
517 }
518
519 return writeCheckedBytes(favorite);
520 }
521
522 /** Deserialize a Favorite from persistence, after verifying checksum wrapper. */
523 private Favorite unpackFavorite(byte[] buffer, int offset, int dataSize)
524 throws InvalidProtocolBufferNanoException {
525 Favorite favorite = new Favorite();
526 MessageNano.mergeFrom(favorite, readCheckedBytes(buffer, offset, dataSize));
527 return favorite;
528 }
529
530 /** Serialize a Screen for persistence, including a checksum wrapper. */
531 private byte[] packScreen(Cursor c) {
532 Screen screen = new Screen();
533 screen.id = c.getLong(ID_INDEX);
534 screen.rank = c.getInt(SCREEN_RANK_INDEX);
535
536 return writeCheckedBytes(screen);
537 }
538
539 /** Deserialize a Screen from persistence, after verifying checksum wrapper. */
540 private Screen unpackScreen(byte[] buffer, int offset, int dataSize)
541 throws InvalidProtocolBufferNanoException {
542 Screen screen = new Screen();
543 MessageNano.mergeFrom(screen, readCheckedBytes(buffer, offset, dataSize));
544 return screen;
545 }
546
547 /**
548 * Read the old journal from the input file.
549 *
550 * In the event of any error, just pretend we didn't have a journal,
551 * in that case, do a full backup.
552 *
553 * @param oldState the read-0only file descriptor pointing to the old journal
554 * @return a Journal protocol bugffer
555 */
556 private Journal readJournal(ParcelFileDescriptor oldState) {
557 int fileSize = (int) oldState.getStatSize();
558 int remaining = fileSize;
559 byte[] buffer = null;
560 Journal journal = new Journal();
561 if (remaining < MAX_JOURNAL_SIZE) {
562 FileInputStream inStream = new FileInputStream(oldState.getFileDescriptor());
563 int offset = 0;
564
565 buffer = new byte[remaining];
566 while (remaining > 0) {
567 int bytesRead = 0;
568 try {
569 bytesRead = inStream.read(buffer, offset, remaining);
570 } catch (IOException e) {
571 Log.w(TAG, "failed to read the journal", e);
572 buffer = null;
573 remaining = 0;
574 }
575 if (bytesRead > 0) {
576 remaining -= bytesRead;
577 } else {
578 // act like there is not journal
579 Log.w(TAG, "failed to read the journal");
580 buffer = null;
581 remaining = 0;
582 }
583 }
584
585 if (buffer != null) {
586 try {
587 MessageNano.mergeFrom(journal, readCheckedBytes(buffer, 0, fileSize));
588 } catch (InvalidProtocolBufferNanoException e) {
589 Log.d(TAG, "failed to read the journal", e);
590 journal.clear();
591 }
592 }
593
594 try {
595 inStream.close();
596 } catch (IOException e) {
597 Log.d(TAG, "failed to close the journal", e);
598 }
599 }
600 return journal;
601 }
602
603 /**
604 * Write the new journal to the output file.
605 *
606 * In the event of any error, just pretend we didn't have a journal,
607 * in that case, do a full backup.
608
609 * @param newState the write-only file descriptor pointing to the new journal
610 * @param journal a Journal protocol buffer
611 */
612 private void writeJournal(ParcelFileDescriptor newState, Journal journal) {
613 FileOutputStream outStream = null;
614 try {
615 outStream = new FileOutputStream(newState.getFileDescriptor());
616 outStream.write(writeCheckedBytes(journal));
617 outStream.close();
618 } catch (IOException e) {
619 Log.d(TAG, "failed to write backup journal", e);
620 }
621 }
622
623 /** Wrap a proto in a CheckedMessage and compute the checksum. */
624 private byte[] writeCheckedBytes(MessageNano proto) {
625 CheckedMessage wrapper = new CheckedMessage();
626 wrapper.payload = MessageNano.toByteArray(proto);
627 CRC32 checksum = new CRC32();
628 checksum.update(wrapper.payload);
629 wrapper.checksum = checksum.getValue();
630 return MessageNano.toByteArray(wrapper);
631 }
632
633 /** Unwrap a proto message from a CheckedMessage, verifying the checksum. */
634 private byte[] readCheckedBytes(byte[] buffer, int offset, int dataSize)
635 throws InvalidProtocolBufferNanoException {
636 CheckedMessage wrapper = new CheckedMessage();
637 MessageNano.mergeFrom(wrapper, buffer, offset, dataSize);
638 CRC32 checksum = new CRC32();
639 checksum.update(wrapper.payload);
640 if (wrapper.checksum != checksum.getValue()) {
641 throw new InvalidProtocolBufferNanoException("checksum does not match");
642 }
643 return wrapper.payload;
644 }
645
646 private class KeyParsingException extends Throwable {
647 private KeyParsingException(Throwable cause) {
648 super(cause);
649 }
650
651 public KeyParsingException(String reason) {
652 super(reason);
653 }
654 }
655}