Home | History | Annotate | Download | only in backup
      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