1 /* 2 * Copyright (C) 2009 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 17 package com.android.internal.backup; 18 19 import android.app.backup.BackupAgent; 20 import android.app.backup.BackupDataInput; 21 import android.app.backup.BackupDataOutput; 22 import android.app.backup.BackupTransport; 23 import android.app.backup.RestoreDescription; 24 import android.app.backup.RestoreSet; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageInfo; 29 import android.os.Environment; 30 import android.os.ParcelFileDescriptor; 31 import android.system.ErrnoException; 32 import android.system.Os; 33 import android.system.StructStat; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 37 import com.android.org.bouncycastle.util.encoders.Base64; 38 39 import libcore.io.IoUtils; 40 41 import java.io.BufferedOutputStream; 42 import java.io.File; 43 import java.io.FileInputStream; 44 import java.io.FileNotFoundException; 45 import java.io.FileOutputStream; 46 import java.io.IOException; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 50 /** 51 * Backup transport for stashing stuff into a known location on disk, and 52 * later restoring from there. For testing only. 53 */ 54 55 public class LocalTransport extends BackupTransport { 56 private static final String TAG = "LocalTransport"; 57 private static final boolean DEBUG = false; 58 59 private static final String TRANSPORT_DIR_NAME 60 = "com.android.internal.backup.LocalTransport"; 61 62 private static final String TRANSPORT_DESTINATION_STRING 63 = "Backing up to debug-only private cache"; 64 65 private static final String TRANSPORT_DATA_MANAGEMENT_LABEL 66 = ""; 67 68 private static final String INCREMENTAL_DIR = "_delta"; 69 private static final String FULL_DATA_DIR = "_full"; 70 71 // The currently-active restore set always has the same (nonzero!) token 72 private static final long CURRENT_SET_TOKEN = 1; 73 74 // Size quotas at reasonable values, similar to the current cloud-storage limits 75 private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024; 76 private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024; 77 78 private Context mContext; 79 private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); 80 private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN)); 81 private File mCurrentSetIncrementalDir = new File(mCurrentSetDir, INCREMENTAL_DIR); 82 private File mCurrentSetFullDir = new File(mCurrentSetDir, FULL_DATA_DIR); 83 84 private PackageInfo[] mRestorePackages = null; 85 private int mRestorePackage = -1; // Index into mRestorePackages 86 private int mRestoreType; 87 private File mRestoreSetDir; 88 private File mRestoreSetIncrementalDir; 89 private File mRestoreSetFullDir; 90 91 // Additional bookkeeping for full backup 92 private String mFullTargetPackage; 93 private ParcelFileDescriptor mSocket; 94 private FileInputStream mSocketInputStream; 95 private BufferedOutputStream mFullBackupOutputStream; 96 private byte[] mFullBackupBuffer; 97 private long mFullBackupSize; 98 99 private FileInputStream mCurFullRestoreStream; 100 private FileOutputStream mFullRestoreSocketStream; 101 private byte[] mFullRestoreBuffer; 102 private final LocalTransportParameters mParameters; 103 104 private void makeDataDirs() { 105 mCurrentSetDir.mkdirs(); 106 mCurrentSetFullDir.mkdir(); 107 mCurrentSetIncrementalDir.mkdir(); 108 } 109 110 public LocalTransport(Context context, LocalTransportParameters parameters) { 111 mContext = context; 112 mParameters = parameters; 113 makeDataDirs(); 114 } 115 116 LocalTransportParameters getParameters() { 117 return mParameters; 118 } 119 120 @Override 121 public String name() { 122 return new ComponentName(mContext, this.getClass()).flattenToShortString(); 123 } 124 125 @Override 126 public Intent configurationIntent() { 127 // The local transport is not user-configurable 128 return null; 129 } 130 131 @Override 132 public String currentDestinationString() { 133 return TRANSPORT_DESTINATION_STRING; 134 } 135 136 public Intent dataManagementIntent() { 137 // The local transport does not present a data-management UI 138 // TODO: consider adding simple UI to wipe the archives entirely, 139 // for cleaning up the cache partition. 140 return null; 141 } 142 143 public String dataManagementLabel() { 144 return TRANSPORT_DATA_MANAGEMENT_LABEL; 145 } 146 147 @Override 148 public String transportDirName() { 149 return TRANSPORT_DIR_NAME; 150 } 151 152 @Override 153 public int getTransportFlags() { 154 int flags = super.getTransportFlags(); 155 // Testing for a fake flag and having it set as a boolean in settings prevents anyone from 156 // using this it to pull data from the agent 157 if (mParameters.isFakeEncryptionFlag()) { 158 flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED; 159 } 160 return flags; 161 } 162 163 @Override 164 public long requestBackupTime() { 165 // any time is a good time for local backup 166 return 0; 167 } 168 169 @Override 170 public int initializeDevice() { 171 if (DEBUG) Log.v(TAG, "wiping all data"); 172 deleteContents(mCurrentSetDir); 173 makeDataDirs(); 174 return TRANSPORT_OK; 175 } 176 177 // Encapsulation of a single k/v element change 178 private class KVOperation { 179 final String key; // Element filename, not the raw key, for efficiency 180 final byte[] value; // null when this is a deletion operation 181 182 KVOperation(String k, byte[] v) { 183 key = k; 184 value = v; 185 } 186 } 187 188 @Override 189 public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { 190 return performBackup(packageInfo, data, /*flags=*/ 0); 191 } 192 193 @Override 194 public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data, int flags) { 195 boolean isIncremental = (flags & FLAG_INCREMENTAL) != 0; 196 boolean isNonIncremental = (flags & FLAG_NON_INCREMENTAL) != 0; 197 198 if (isIncremental) { 199 Log.i(TAG, "Performing incremental backup for " + packageInfo.packageName); 200 } else if (isNonIncremental) { 201 Log.i(TAG, "Performing non-incremental backup for " + packageInfo.packageName); 202 } else { 203 Log.i(TAG, "Performing backup for " + packageInfo.packageName); 204 } 205 206 if (DEBUG) { 207 try { 208 StructStat ss = Os.fstat(data.getFileDescriptor()); 209 Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName 210 + " size=" + ss.st_size + " flags=" + flags); 211 } catch (ErrnoException e) { 212 Log.w(TAG, "Unable to stat input file in performBackup() on " 213 + packageInfo.packageName); 214 } 215 } 216 217 File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); 218 boolean hasDataForPackage = !packageDir.mkdirs(); 219 220 if (isIncremental) { 221 if (mParameters.isNonIncrementalOnly() || !hasDataForPackage) { 222 if (mParameters.isNonIncrementalOnly()) { 223 Log.w(TAG, "Transport is in non-incremental only mode."); 224 225 } else { 226 Log.w(TAG, 227 "Requested incremental, but transport currently stores no data for the " 228 + "package, requesting non-incremental retry."); 229 } 230 return TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED; 231 } 232 } 233 if (isNonIncremental && hasDataForPackage) { 234 Log.w(TAG, "Requested non-incremental, deleting existing data."); 235 clearBackupData(packageInfo); 236 packageDir.mkdirs(); 237 } 238 239 // Each 'record' in the restore set is kept in its own file, named by 240 // the record key. Wind through the data file, extracting individual 241 // record operations and building a list of all the updates to apply 242 // in this update. 243 final ArrayList<KVOperation> changeOps; 244 try { 245 changeOps = parseBackupStream(data); 246 } catch (IOException e) { 247 // oops, something went wrong. abort the operation and return error. 248 Log.v(TAG, "Exception reading backup input", e); 249 return TRANSPORT_ERROR; 250 } 251 252 // Okay, now we've parsed out the delta's individual operations. We need to measure 253 // the effect against what we already have in the datastore to detect quota overrun. 254 // So, we first need to tally up the current in-datastore size per key. 255 final ArrayMap<String, Integer> datastore = new ArrayMap<>(); 256 int totalSize = parseKeySizes(packageDir, datastore); 257 258 // ... and now figure out the datastore size that will result from applying the 259 // sequence of delta operations 260 if (DEBUG) { 261 if (changeOps.size() > 0) { 262 Log.v(TAG, "Calculating delta size impact"); 263 } else { 264 Log.v(TAG, "No operations in backup stream, so no size change"); 265 } 266 } 267 int updatedSize = totalSize; 268 for (KVOperation op : changeOps) { 269 // Deduct the size of the key we're about to replace, if any 270 final Integer curSize = datastore.get(op.key); 271 if (curSize != null) { 272 updatedSize -= curSize.intValue(); 273 if (DEBUG && op.value == null) { 274 Log.v(TAG, " delete " + op.key + ", updated total " + updatedSize); 275 } 276 } 277 278 // And add back the size of the value we're about to store, if any 279 if (op.value != null) { 280 updatedSize += op.value.length; 281 if (DEBUG) { 282 Log.v(TAG, ((curSize == null) ? " new " : " replace ") 283 + op.key + ", updated total " + updatedSize); 284 } 285 } 286 } 287 288 // If our final size is over quota, report the failure 289 if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) { 290 if (DEBUG) { 291 Log.i(TAG, "New datastore size " + updatedSize 292 + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA); 293 } 294 return TRANSPORT_QUOTA_EXCEEDED; 295 } 296 297 // No problem with storage size, so go ahead and apply the delta operations 298 // (in the order that the app provided them) 299 for (KVOperation op : changeOps) { 300 File element = new File(packageDir, op.key); 301 302 // this is either a deletion or a rewrite-from-zero, so we can just remove 303 // the existing file and proceed in either case. 304 element.delete(); 305 306 // if this wasn't a deletion, put the new data in place 307 if (op.value != null) { 308 try (FileOutputStream out = new FileOutputStream(element)) { 309 out.write(op.value, 0, op.value.length); 310 } catch (IOException e) { 311 Log.e(TAG, "Unable to update key file " + element); 312 return TRANSPORT_ERROR; 313 } 314 } 315 } 316 return TRANSPORT_OK; 317 } 318 319 // Parses a backup stream into individual key/value operations 320 private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data) 321 throws IOException { 322 ArrayList<KVOperation> changeOps = new ArrayList<>(); 323 BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); 324 while (changeSet.readNextHeader()) { 325 String key = changeSet.getKey(); 326 String base64Key = new String(Base64.encode(key.getBytes())); 327 int dataSize = changeSet.getDataSize(); 328 if (DEBUG) { 329 Log.v(TAG, " Delta operation key " + key + " size " + dataSize 330 + " key64 " + base64Key); 331 } 332 333 byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null; 334 if (dataSize >= 0) { 335 changeSet.readEntityData(buf, 0, dataSize); 336 } 337 changeOps.add(new KVOperation(base64Key, buf)); 338 } 339 return changeOps; 340 } 341 342 // Reads the given datastore directory, building a table of the value size of each 343 // keyed element, and returning the summed total. 344 private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) { 345 int totalSize = 0; 346 final String[] elements = packageDir.list(); 347 if (elements != null) { 348 if (DEBUG) { 349 Log.v(TAG, "Existing datastore contents:"); 350 } 351 for (String file : elements) { 352 File element = new File(packageDir, file); 353 String key = file; // filename 354 int size = (int) element.length(); 355 totalSize += size; 356 if (DEBUG) { 357 Log.v(TAG, " key " + key + " size " + size); 358 } 359 datastore.put(key, size); 360 } 361 if (DEBUG) { 362 Log.v(TAG, " TOTAL: " + totalSize); 363 } 364 } else { 365 if (DEBUG) { 366 Log.v(TAG, "No existing data for this package"); 367 } 368 } 369 return totalSize; 370 } 371 372 // Deletes the contents but not the given directory 373 private void deleteContents(File dirname) { 374 File[] contents = dirname.listFiles(); 375 if (contents != null) { 376 for (File f : contents) { 377 if (f.isDirectory()) { 378 // delete the directory's contents then fall through 379 // and delete the directory itself. 380 deleteContents(f); 381 } 382 f.delete(); 383 } 384 } 385 } 386 387 @Override 388 public int clearBackupData(PackageInfo packageInfo) { 389 if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); 390 391 File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); 392 final File[] fileset = packageDir.listFiles(); 393 if (fileset != null) { 394 for (File f : fileset) { 395 f.delete(); 396 } 397 packageDir.delete(); 398 } 399 400 packageDir = new File(mCurrentSetFullDir, packageInfo.packageName); 401 final File[] tarballs = packageDir.listFiles(); 402 if (tarballs != null) { 403 for (File f : tarballs) { 404 f.delete(); 405 } 406 packageDir.delete(); 407 } 408 409 return TRANSPORT_OK; 410 } 411 412 @Override 413 public int finishBackup() { 414 if (DEBUG) Log.v(TAG, "finishBackup() of " + mFullTargetPackage); 415 return tearDownFullBackup(); 416 } 417 418 // ------------------------------------------------------------------------------------ 419 // Full backup handling 420 421 private int tearDownFullBackup() { 422 if (mSocket != null) { 423 try { 424 if (mFullBackupOutputStream != null) { 425 mFullBackupOutputStream.flush(); 426 mFullBackupOutputStream.close(); 427 } 428 mSocketInputStream = null; 429 mFullTargetPackage = null; 430 mSocket.close(); 431 } catch (IOException e) { 432 if (DEBUG) { 433 Log.w(TAG, "Exception caught in tearDownFullBackup()", e); 434 } 435 return TRANSPORT_ERROR; 436 } finally { 437 mSocket = null; 438 mFullBackupOutputStream = null; 439 } 440 } 441 return TRANSPORT_OK; 442 } 443 444 private File tarballFile(String pkgName) { 445 return new File(mCurrentSetFullDir, pkgName); 446 } 447 448 @Override 449 public long requestFullBackupTime() { 450 return 0; 451 } 452 453 @Override 454 public int checkFullBackupSize(long size) { 455 int result = TRANSPORT_OK; 456 // Decline zero-size "backups" 457 if (size <= 0) { 458 result = TRANSPORT_PACKAGE_REJECTED; 459 } else if (size > FULL_BACKUP_SIZE_QUOTA) { 460 result = TRANSPORT_QUOTA_EXCEEDED; 461 } 462 if (result != TRANSPORT_OK) { 463 if (DEBUG) { 464 Log.v(TAG, "Declining backup of size " + size); 465 } 466 } 467 return result; 468 } 469 470 @Override 471 public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) { 472 if (mSocket != null) { 473 Log.e(TAG, "Attempt to initiate full backup while one is in progress"); 474 return TRANSPORT_ERROR; 475 } 476 477 if (DEBUG) { 478 Log.i(TAG, "performFullBackup : " + targetPackage); 479 } 480 481 // We know a priori that we run in the system process, so we need to make 482 // sure to dup() our own copy of the socket fd. Transports which run in 483 // their own processes must not do this. 484 try { 485 mFullBackupSize = 0; 486 mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); 487 mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor()); 488 } catch (IOException e) { 489 Log.e(TAG, "Unable to process socket for full backup"); 490 return TRANSPORT_ERROR; 491 } 492 493 mFullTargetPackage = targetPackage.packageName; 494 mFullBackupBuffer = new byte[4096]; 495 496 return TRANSPORT_OK; 497 } 498 499 @Override 500 public int sendBackupData(final int numBytes) { 501 if (mSocket == null) { 502 Log.w(TAG, "Attempted sendBackupData before performFullBackup"); 503 return TRANSPORT_ERROR; 504 } 505 506 mFullBackupSize += numBytes; 507 if (mFullBackupSize > FULL_BACKUP_SIZE_QUOTA) { 508 return TRANSPORT_QUOTA_EXCEEDED; 509 } 510 511 if (numBytes > mFullBackupBuffer.length) { 512 mFullBackupBuffer = new byte[numBytes]; 513 } 514 515 if (mFullBackupOutputStream == null) { 516 FileOutputStream tarstream; 517 try { 518 File tarball = tarballFile(mFullTargetPackage); 519 tarstream = new FileOutputStream(tarball); 520 } catch (FileNotFoundException e) { 521 return TRANSPORT_ERROR; 522 } 523 mFullBackupOutputStream = new BufferedOutputStream(tarstream); 524 } 525 526 int bytesLeft = numBytes; 527 while (bytesLeft > 0) { 528 try { 529 int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft); 530 if (nRead < 0) { 531 // Something went wrong if we expect data but saw EOD 532 Log.w(TAG, "Unexpected EOD; failing backup"); 533 return TRANSPORT_ERROR; 534 } 535 mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead); 536 bytesLeft -= nRead; 537 } catch (IOException e) { 538 Log.e(TAG, "Error handling backup data for " + mFullTargetPackage); 539 return TRANSPORT_ERROR; 540 } 541 } 542 if (DEBUG) { 543 Log.v(TAG, " stored " + numBytes + " of data"); 544 } 545 return TRANSPORT_OK; 546 } 547 548 // For now we can't roll back, so just tear everything down. 549 @Override 550 public void cancelFullBackup() { 551 if (DEBUG) { 552 Log.i(TAG, "Canceling full backup of " + mFullTargetPackage); 553 } 554 File archive = tarballFile(mFullTargetPackage); 555 tearDownFullBackup(); 556 if (archive.exists()) { 557 archive.delete(); 558 } 559 } 560 561 // ------------------------------------------------------------------------------------ 562 // Restore handling 563 static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; 564 565 @Override 566 public RestoreSet[] getAvailableRestoreSets() { 567 long[] existing = new long[POSSIBLE_SETS.length + 1]; 568 int num = 0; 569 570 // see which possible non-current sets exist... 571 for (long token : POSSIBLE_SETS) { 572 if ((new File(mDataDir, Long.toString(token))).exists()) { 573 existing[num++] = token; 574 } 575 } 576 // ...and always the currently-active set last 577 existing[num++] = CURRENT_SET_TOKEN; 578 579 RestoreSet[] available = new RestoreSet[num]; 580 for (int i = 0; i < available.length; i++) { 581 available[i] = new RestoreSet("Local disk image", "flash", existing[i]); 582 } 583 return available; 584 } 585 586 @Override 587 public long getCurrentRestoreSet() { 588 // The current restore set always has the same token 589 return CURRENT_SET_TOKEN; 590 } 591 592 @Override 593 public int startRestore(long token, PackageInfo[] packages) { 594 if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length 595 + " matching packages"); 596 mRestorePackages = packages; 597 mRestorePackage = -1; 598 mRestoreSetDir = new File(mDataDir, Long.toString(token)); 599 mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR); 600 mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR); 601 return TRANSPORT_OK; 602 } 603 604 @Override 605 public RestoreDescription nextRestorePackage() { 606 if (DEBUG) { 607 Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage 608 + " length=" + mRestorePackages.length); 609 } 610 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 611 612 boolean found = false; 613 while (++mRestorePackage < mRestorePackages.length) { 614 String name = mRestorePackages[mRestorePackage].packageName; 615 616 // If we have key/value data for this package, deliver that 617 // skip packages where we have a data dir but no actual contents 618 String[] contents = (new File(mRestoreSetIncrementalDir, name)).list(); 619 if (contents != null && contents.length > 0) { 620 if (DEBUG) { 621 Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ " 622 + mRestorePackage + " = " + name); 623 } 624 mRestoreType = RestoreDescription.TYPE_KEY_VALUE; 625 found = true; 626 } 627 628 if (!found) { 629 // No key/value data; check for [non-empty] full data 630 File maybeFullData = new File(mRestoreSetFullDir, name); 631 if (maybeFullData.length() > 0) { 632 if (DEBUG) { 633 Log.v(TAG, " nextRestorePackage(TYPE_FULL_STREAM) @ " 634 + mRestorePackage + " = " + name); 635 } 636 mRestoreType = RestoreDescription.TYPE_FULL_STREAM; 637 mCurFullRestoreStream = null; // ensure starting from the ground state 638 found = true; 639 } 640 } 641 642 if (found) { 643 return new RestoreDescription(name, mRestoreType); 644 } 645 646 if (DEBUG) { 647 Log.v(TAG, " ... package @ " + mRestorePackage + " = " + name 648 + " has no data; skipping"); 649 } 650 } 651 652 if (DEBUG) Log.v(TAG, " no more packages to restore"); 653 return RestoreDescription.NO_MORE_PACKAGES; 654 } 655 656 @Override 657 public int getRestoreData(ParcelFileDescriptor outFd) { 658 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 659 if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); 660 if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) { 661 throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset"); 662 } 663 File packageDir = new File(mRestoreSetIncrementalDir, 664 mRestorePackages[mRestorePackage].packageName); 665 666 // The restore set is the concatenation of the individual record blobs, 667 // each of which is a file in the package's directory. We return the 668 // data in lexical order sorted by key, so that apps which use synthetic 669 // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious 670 // order. 671 ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); 672 if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error 673 Log.e(TAG, "No keys for package: " + packageDir); 674 return TRANSPORT_ERROR; 675 } 676 677 // We expect at least some data if the directory exists in the first place 678 if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.size() + " key files"); 679 BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); 680 try { 681 for (DecodedFilename keyEntry : blobs) { 682 File f = keyEntry.file; 683 FileInputStream in = new FileInputStream(f); 684 try { 685 int size = (int) f.length(); 686 byte[] buf = new byte[size]; 687 in.read(buf); 688 if (DEBUG) Log.v(TAG, " ... key=" + keyEntry.key + " size=" + size); 689 out.writeEntityHeader(keyEntry.key, size); 690 out.writeEntityData(buf, size); 691 } finally { 692 in.close(); 693 } 694 } 695 return TRANSPORT_OK; 696 } catch (IOException e) { 697 Log.e(TAG, "Unable to read backup records", e); 698 return TRANSPORT_ERROR; 699 } 700 } 701 702 static class DecodedFilename implements Comparable<DecodedFilename> { 703 public File file; 704 public String key; 705 706 public DecodedFilename(File f) { 707 file = f; 708 key = new String(Base64.decode(f.getName())); 709 } 710 711 @Override 712 public int compareTo(DecodedFilename other) { 713 // sorts into ascending lexical order by decoded key 714 return key.compareTo(other.key); 715 } 716 } 717 718 // Return a list of the files in the given directory, sorted lexically by 719 // the Base64-decoded file name, not by the on-disk filename 720 private ArrayList<DecodedFilename> contentsByKey(File dir) { 721 File[] allFiles = dir.listFiles(); 722 if (allFiles == null || allFiles.length == 0) { 723 return null; 724 } 725 726 // Decode the filenames into keys then sort lexically by key 727 ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>(); 728 for (File f : allFiles) { 729 contents.add(new DecodedFilename(f)); 730 } 731 Collections.sort(contents); 732 return contents; 733 } 734 735 @Override 736 public void finishRestore() { 737 if (DEBUG) Log.v(TAG, "finishRestore()"); 738 if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) { 739 resetFullRestoreState(); 740 } 741 mRestoreType = 0; 742 } 743 744 // ------------------------------------------------------------------------------------ 745 // Full restore handling 746 747 private void resetFullRestoreState() { 748 IoUtils.closeQuietly(mCurFullRestoreStream); 749 mCurFullRestoreStream = null; 750 mFullRestoreSocketStream = null; 751 mFullRestoreBuffer = null; 752 } 753 754 /** 755 * Ask the transport to provide data for the "current" package being restored. The 756 * transport then writes some data to the socket supplied to this call, and returns 757 * the number of bytes written. The system will then read that many bytes and 758 * stream them to the application's agent for restore, then will call this method again 759 * to receive the next chunk of the archive. This sequence will be repeated until the 760 * transport returns zero indicating that all of the package's data has been delivered 761 * (or returns a negative value indicating some sort of hard error condition at the 762 * transport level). 763 * 764 * <p>After this method returns zero, the system will then call 765 * {@link #getNextFullRestorePackage()} to begin the restore process for the next 766 * application, and the sequence begins again. 767 * 768 * @param socket The file descriptor that the transport will use for delivering the 769 * streamed archive. 770 * @return 0 when no more data for the current package is available. A positive value 771 * indicates the presence of that much data to be delivered to the app. A negative 772 * return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR}, 773 * indicating a fatal error condition that precludes further restore operations 774 * on the current dataset. 775 */ 776 @Override 777 public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) { 778 if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { 779 throw new IllegalStateException("Asked for full restore data for non-stream package"); 780 } 781 782 // first chunk? 783 if (mCurFullRestoreStream == null) { 784 final String name = mRestorePackages[mRestorePackage].packageName; 785 if (DEBUG) Log.i(TAG, "Starting full restore of " + name); 786 File dataset = new File(mRestoreSetFullDir, name); 787 try { 788 mCurFullRestoreStream = new FileInputStream(dataset); 789 } catch (IOException e) { 790 // If we can't open the target package's tarball, we return the single-package 791 // error code and let the caller go on to the next package. 792 Log.e(TAG, "Unable to read archive for " + name); 793 return TRANSPORT_PACKAGE_REJECTED; 794 } 795 mFullRestoreSocketStream = new FileOutputStream(socket.getFileDescriptor()); 796 mFullRestoreBuffer = new byte[2*1024]; 797 } 798 799 int nRead; 800 try { 801 nRead = mCurFullRestoreStream.read(mFullRestoreBuffer); 802 if (nRead < 0) { 803 // EOF: tell the caller we're done 804 nRead = NO_MORE_DATA; 805 } else if (nRead == 0) { 806 // This shouldn't happen when reading a FileInputStream; we should always 807 // get either a positive nonzero byte count or -1. Log the situation and 808 // treat it as EOF. 809 Log.w(TAG, "read() of archive file returned 0; treating as EOF"); 810 nRead = NO_MORE_DATA; 811 } else { 812 if (DEBUG) { 813 Log.i(TAG, " delivering restore chunk: " + nRead); 814 } 815 mFullRestoreSocketStream.write(mFullRestoreBuffer, 0, nRead); 816 } 817 } catch (IOException e) { 818 return TRANSPORT_ERROR; // Hard error accessing the file; shouldn't happen 819 } finally { 820 // Most transports will need to explicitly close 'socket' here, but this transport 821 // is in the same process as the caller so it can leave it up to the backup manager 822 // to manage both socket fds. 823 } 824 825 return nRead; 826 } 827 828 /** 829 * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM} 830 * data for restore, it will invoke this method to tell the transport that it should 831 * abandon the data download for the current package. The OS will then either call 832 * {@link #nextRestorePackage()} again to move on to restoring the next package in the 833 * set being iterated over, or will call {@link #finishRestore()} to shut down the restore 834 * operation. 835 * 836 * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the 837 * current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious 838 * transport-level failure. If the transport reports an error here, the entire restore 839 * operation will immediately be finished with no further attempts to restore app data. 840 */ 841 @Override 842 public int abortFullRestore() { 843 if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { 844 throw new IllegalStateException("abortFullRestore() but not currently restoring"); 845 } 846 resetFullRestoreState(); 847 mRestoreType = 0; 848 return TRANSPORT_OK; 849 } 850 851 @Override 852 public long getBackupQuota(String packageName, boolean isFullBackup) { 853 return isFullBackup ? FULL_BACKUP_SIZE_QUOTA : KEY_VALUE_BACKUP_SIZE_QUOTA; 854 } 855 } 856