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