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