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