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