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