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