/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.backup;

import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupTransport;
import android.app.backup.RestoreDescription;
import android.app.backup.RestoreSet;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.util.ArrayMap;
import android.util.Log;

import com.android.org.bouncycastle.util.encoders.Base64;

import libcore.io.IoUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;

Backup transport for stashing stuff into a known location on disk, and later restoring from there. For testing only.
/** * Backup transport for stashing stuff into a known location on disk, and * later restoring from there. For testing only. */
public class LocalTransport extends BackupTransport { private static final String TAG = "LocalTransport"; private static final boolean DEBUG = false; private static final String TRANSPORT_DIR_NAME = "com.android.internal.backup.LocalTransport"; private static final String TRANSPORT_DESTINATION_STRING = "Backing up to debug-only private cache"; private static final String TRANSPORT_DATA_MANAGEMENT_LABEL = ""; private static final String INCREMENTAL_DIR = "_delta"; private static final String FULL_DATA_DIR = "_full"; // The currently-active restore set always has the same (nonzero!) token private static final long CURRENT_SET_TOKEN = 1; // Size quotas at reasonable values, similar to the current cloud-storage limits private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024; private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024; private Context mContext; private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN)); private File mCurrentSetIncrementalDir = new File(mCurrentSetDir, INCREMENTAL_DIR); private File mCurrentSetFullDir = new File(mCurrentSetDir, FULL_DATA_DIR); private PackageInfo[] mRestorePackages = null; private int mRestorePackage = -1; // Index into mRestorePackages private int mRestoreType; private File mRestoreSetDir; private File mRestoreSetIncrementalDir; private File mRestoreSetFullDir; // Additional bookkeeping for full backup private String mFullTargetPackage; private ParcelFileDescriptor mSocket; private FileInputStream mSocketInputStream; private BufferedOutputStream mFullBackupOutputStream; private byte[] mFullBackupBuffer; private long mFullBackupSize; private FileInputStream mCurFullRestoreStream; private FileOutputStream mFullRestoreSocketStream; private byte[] mFullRestoreBuffer; private final LocalTransportParameters mParameters; private void makeDataDirs() { mCurrentSetDir.mkdirs(); mCurrentSetFullDir.mkdir(); mCurrentSetIncrementalDir.mkdir(); } public LocalTransport(Context context, LocalTransportParameters parameters) { mContext = context; mParameters = parameters; makeDataDirs(); } LocalTransportParameters getParameters() { return mParameters; } @Override public String name() { return new ComponentName(mContext, this.getClass()).flattenToShortString(); } @Override public Intent configurationIntent() { // The local transport is not user-configurable return null; } @Override public String currentDestinationString() { return TRANSPORT_DESTINATION_STRING; } public Intent dataManagementIntent() { // The local transport does not present a data-management UI // TODO: consider adding simple UI to wipe the archives entirely, // for cleaning up the cache partition. return null; } public String dataManagementLabel() { return TRANSPORT_DATA_MANAGEMENT_LABEL; } @Override public String transportDirName() { return TRANSPORT_DIR_NAME; } @Override public int getTransportFlags() { int flags = super.getTransportFlags(); // Testing for a fake flag and having it set as a boolean in settings prevents anyone from // using this it to pull data from the agent if (mParameters.isFakeEncryptionFlag()) { flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED; } return flags; } @Override public long requestBackupTime() { // any time is a good time for local backup return 0; } @Override public int initializeDevice() { if (DEBUG) Log.v(TAG, "wiping all data"); deleteContents(mCurrentSetDir); makeDataDirs(); return TRANSPORT_OK; } // Encapsulation of a single k/v element change private class KVOperation { final String key; // Element filename, not the raw key, for efficiency final byte[] value; // null when this is a deletion operation KVOperation(String k, byte[] v) { key = k; value = v; } } @Override public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { return performBackup(packageInfo, data, /*flags=*/ 0); } @Override public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data, int flags) { boolean isIncremental = (flags & FLAG_INCREMENTAL) != 0; boolean isNonIncremental = (flags & FLAG_NON_INCREMENTAL) != 0; if (isIncremental) { Log.i(TAG, "Performing incremental backup for " + packageInfo.packageName); } else if (isNonIncremental) { Log.i(TAG, "Performing non-incremental backup for " + packageInfo.packageName); } else { Log.i(TAG, "Performing backup for " + packageInfo.packageName); } if (DEBUG) { try { StructStat ss = Os.fstat(data.getFileDescriptor()); Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName + " size=" + ss.st_size + " flags=" + flags); } catch (ErrnoException e) { Log.w(TAG, "Unable to stat input file in performBackup() on " + packageInfo.packageName); } } File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); boolean hasDataForPackage = !packageDir.mkdirs(); if (isIncremental) { if (mParameters.isNonIncrementalOnly() || !hasDataForPackage) { if (mParameters.isNonIncrementalOnly()) { Log.w(TAG, "Transport is in non-incremental only mode."); } else { Log.w(TAG, "Requested incremental, but transport currently stores no data for the " + "package, requesting non-incremental retry."); } return TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED; } } if (isNonIncremental && hasDataForPackage) { Log.w(TAG, "Requested non-incremental, deleting existing data."); clearBackupData(packageInfo); packageDir.mkdirs(); } // Each 'record' in the restore set is kept in its own file, named by // the record key. Wind through the data file, extracting individual // record operations and building a list of all the updates to apply // in this update. final ArrayList<KVOperation> changeOps; try { changeOps = parseBackupStream(data); } catch (IOException e) { // oops, something went wrong. abort the operation and return error. Log.v(TAG, "Exception reading backup input", e); return TRANSPORT_ERROR; } // Okay, now we've parsed out the delta's individual operations. We need to measure // the effect against what we already have in the datastore to detect quota overrun. // So, we first need to tally up the current in-datastore size per key. final ArrayMap<String, Integer> datastore = new ArrayMap<>(); int totalSize = parseKeySizes(packageDir, datastore); // ... and now figure out the datastore size that will result from applying the // sequence of delta operations if (DEBUG) { if (changeOps.size() > 0) { Log.v(TAG, "Calculating delta size impact"); } else { Log.v(TAG, "No operations in backup stream, so no size change"); } } int updatedSize = totalSize; for (KVOperation op : changeOps) { // Deduct the size of the key we're about to replace, if any final Integer curSize = datastore.get(op.key); if (curSize != null) { updatedSize -= curSize.intValue(); if (DEBUG && op.value == null) { Log.v(TAG, " delete " + op.key + ", updated total " + updatedSize); } } // And add back the size of the value we're about to store, if any if (op.value != null) { updatedSize += op.value.length; if (DEBUG) { Log.v(TAG, ((curSize == null) ? " new " : " replace ") + op.key + ", updated total " + updatedSize); } } } // If our final size is over quota, report the failure if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) { if (DEBUG) { Log.i(TAG, "New datastore size " + updatedSize + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA); } return TRANSPORT_QUOTA_EXCEEDED; } // No problem with storage size, so go ahead and apply the delta operations // (in the order that the app provided them) for (KVOperation op : changeOps) { File element = new File(packageDir, op.key); // this is either a deletion or a rewrite-from-zero, so we can just remove // the existing file and proceed in either case. element.delete(); // if this wasn't a deletion, put the new data in place if (op.value != null) { try (FileOutputStream out = new FileOutputStream(element)) { out.write(op.value, 0, op.value.length); } catch (IOException e) { Log.e(TAG, "Unable to update key file " + element); return TRANSPORT_ERROR; } } } return TRANSPORT_OK; } // Parses a backup stream into individual key/value operations private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data) throws IOException { ArrayList<KVOperation> changeOps = new ArrayList<>(); BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); while (changeSet.readNextHeader()) { String key = changeSet.getKey(); String base64Key = new String(Base64.encode(key.getBytes())); int dataSize = changeSet.getDataSize(); if (DEBUG) { Log.v(TAG, " Delta operation key " + key + " size " + dataSize + " key64 " + base64Key); } byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null; if (dataSize >= 0) { changeSet.readEntityData(buf, 0, dataSize); } changeOps.add(new KVOperation(base64Key, buf)); } return changeOps; } // Reads the given datastore directory, building a table of the value size of each // keyed element, and returning the summed total. private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) { int totalSize = 0; final String[] elements = packageDir.list(); if (elements != null) { if (DEBUG) { Log.v(TAG, "Existing datastore contents:"); } for (String file : elements) { File element = new File(packageDir, file); String key = file; // filename int size = (int) element.length(); totalSize += size; if (DEBUG) { Log.v(TAG, " key " + key + " size " + size); } datastore.put(key, size); } if (DEBUG) { Log.v(TAG, " TOTAL: " + totalSize); } } else { if (DEBUG) { Log.v(TAG, "No existing data for this package"); } } return totalSize; } // Deletes the contents but not the given directory private void deleteContents(File dirname) { File[] contents = dirname.listFiles(); if (contents != null) { for (File f : contents) { if (f.isDirectory()) { // delete the directory's contents then fall through // and delete the directory itself. deleteContents(f); } f.delete(); } } } @Override public int clearBackupData(PackageInfo packageInfo) { if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); final File[] fileset = packageDir.listFiles(); if (fileset != null) { for (File f : fileset) { f.delete(); } packageDir.delete(); } packageDir = new File(mCurrentSetFullDir, packageInfo.packageName); final File[] tarballs = packageDir.listFiles(); if (tarballs != null) { for (File f : tarballs) { f.delete(); } packageDir.delete(); } return TRANSPORT_OK; } @Override public int finishBackup() { if (DEBUG) Log.v(TAG, "finishBackup() of " + mFullTargetPackage); return tearDownFullBackup(); } // ------------------------------------------------------------------------------------ // Full backup handling private int tearDownFullBackup() { if (mSocket != null) { try { if (mFullBackupOutputStream != null) { mFullBackupOutputStream.flush(); mFullBackupOutputStream.close(); } mSocketInputStream = null; mFullTargetPackage = null; mSocket.close(); } catch (IOException e) { if (DEBUG) { Log.w(TAG, "Exception caught in tearDownFullBackup()", e); } return TRANSPORT_ERROR; } finally { mSocket = null; mFullBackupOutputStream = null; } } return TRANSPORT_OK; } private File tarballFile(String pkgName) { return new File(mCurrentSetFullDir, pkgName); } @Override public long requestFullBackupTime() { return 0; } @Override public int checkFullBackupSize(long size) { int result = TRANSPORT_OK; // Decline zero-size "backups" if (size <= 0) { result = TRANSPORT_PACKAGE_REJECTED; } else if (size > FULL_BACKUP_SIZE_QUOTA) { result = TRANSPORT_QUOTA_EXCEEDED; } if (result != TRANSPORT_OK) { if (DEBUG) { Log.v(TAG, "Declining backup of size " + size); } } return result; } @Override public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) { if (mSocket != null) { Log.e(TAG, "Attempt to initiate full backup while one is in progress"); return TRANSPORT_ERROR; } if (DEBUG) { Log.i(TAG, "performFullBackup : " + targetPackage); } // We know a priori that we run in the system process, so we need to make // sure to dup() our own copy of the socket fd. Transports which run in // their own processes must not do this. try { mFullBackupSize = 0; mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor()); } catch (IOException e) { Log.e(TAG, "Unable to process socket for full backup"); return TRANSPORT_ERROR; } mFullTargetPackage = targetPackage.packageName; mFullBackupBuffer = new byte[4096]; return TRANSPORT_OK; } @Override public int sendBackupData(final int numBytes) { if (mSocket == null) { Log.w(TAG, "Attempted sendBackupData before performFullBackup"); return TRANSPORT_ERROR; } mFullBackupSize += numBytes; if (mFullBackupSize > FULL_BACKUP_SIZE_QUOTA) { return TRANSPORT_QUOTA_EXCEEDED; } if (numBytes > mFullBackupBuffer.length) { mFullBackupBuffer = new byte[numBytes]; } if (mFullBackupOutputStream == null) { FileOutputStream tarstream; try { File tarball = tarballFile(mFullTargetPackage); tarstream = new FileOutputStream(tarball); } catch (FileNotFoundException e) { return TRANSPORT_ERROR; } mFullBackupOutputStream = new BufferedOutputStream(tarstream); } int bytesLeft = numBytes; while (bytesLeft > 0) { try { int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, bytesLeft); if (nRead < 0) { // Something went wrong if we expect data but saw EOD Log.w(TAG, "Unexpected EOD; failing backup"); return TRANSPORT_ERROR; } mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead); bytesLeft -= nRead; } catch (IOException e) { Log.e(TAG, "Error handling backup data for " + mFullTargetPackage); return TRANSPORT_ERROR; } } if (DEBUG) { Log.v(TAG, " stored " + numBytes + " of data"); } return TRANSPORT_OK; } // For now we can't roll back, so just tear everything down. @Override public void cancelFullBackup() { if (DEBUG) { Log.i(TAG, "Canceling full backup of " + mFullTargetPackage); } File archive = tarballFile(mFullTargetPackage); tearDownFullBackup(); if (archive.exists()) { archive.delete(); } } // ------------------------------------------------------------------------------------ // Restore handling static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; @Override public RestoreSet[] getAvailableRestoreSets() { long[] existing = new long[POSSIBLE_SETS.length + 1]; int num = 0; // see which possible non-current sets exist... for (long token : POSSIBLE_SETS) { if ((new File(mDataDir, Long.toString(token))).exists()) { existing[num++] = token; } } // ...and always the currently-active set last existing[num++] = CURRENT_SET_TOKEN; RestoreSet[] available = new RestoreSet[num]; for (int i = 0; i < available.length; i++) { available[i] = new RestoreSet("Local disk image", "flash", existing[i]); } return available; } @Override public long getCurrentRestoreSet() { // The current restore set always has the same token return CURRENT_SET_TOKEN; } @Override public int startRestore(long token, PackageInfo[] packages) { if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length + " matching packages"); mRestorePackages = packages; mRestorePackage = -1; mRestoreSetDir = new File(mDataDir, Long.toString(token)); mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR); mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR); return TRANSPORT_OK; } @Override public RestoreDescription nextRestorePackage() { if (DEBUG) { Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage + " length=" + mRestorePackages.length); } if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); boolean found = false; while (++mRestorePackage < mRestorePackages.length) { String name = mRestorePackages[mRestorePackage].packageName; // If we have key/value data for this package, deliver that // skip packages where we have a data dir but no actual contents String[] contents = (new File(mRestoreSetIncrementalDir, name)).list(); if (contents != null && contents.length > 0) { if (DEBUG) { Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ " + mRestorePackage + " = " + name); } mRestoreType = RestoreDescription.TYPE_KEY_VALUE; found = true; } if (!found) { // No key/value data; check for [non-empty] full data File maybeFullData = new File(mRestoreSetFullDir, name); if (maybeFullData.length() > 0) { if (DEBUG) { Log.v(TAG, " nextRestorePackage(TYPE_FULL_STREAM) @ " + mRestorePackage + " = " + name); } mRestoreType = RestoreDescription.TYPE_FULL_STREAM; mCurFullRestoreStream = null; // ensure starting from the ground state found = true; } } if (found) { return new RestoreDescription(name, mRestoreType); } if (DEBUG) { Log.v(TAG, " ... package @ " + mRestorePackage + " = " + name + " has no data; skipping"); } } if (DEBUG) Log.v(TAG, " no more packages to restore"); return RestoreDescription.NO_MORE_PACKAGES; } @Override public int getRestoreData(ParcelFileDescriptor outFd) { if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) { throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset"); } File packageDir = new File(mRestoreSetIncrementalDir, mRestorePackages[mRestorePackage].packageName); // The restore set is the concatenation of the individual record blobs, // each of which is a file in the package's directory. We return the // data in lexical order sorted by key, so that apps which use synthetic // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious // order. ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error Log.e(TAG, "No keys for package: " + packageDir); return TRANSPORT_ERROR; } // We expect at least some data if the directory exists in the first place if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.size() + " key files"); BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); try { for (DecodedFilename keyEntry : blobs) { File f = keyEntry.file; FileInputStream in = new FileInputStream(f); try { int size = (int) f.length(); byte[] buf = new byte[size]; in.read(buf); if (DEBUG) Log.v(TAG, " ... key=" + keyEntry.key + " size=" + size); out.writeEntityHeader(keyEntry.key, size); out.writeEntityData(buf, size); } finally { in.close(); } } return TRANSPORT_OK; } catch (IOException e) { Log.e(TAG, "Unable to read backup records", e); return TRANSPORT_ERROR; } } static class DecodedFilename implements Comparable<DecodedFilename> { public File file; public String key; public DecodedFilename(File f) { file = f; key = new String(Base64.decode(f.getName())); } @Override public int compareTo(DecodedFilename other) { // sorts into ascending lexical order by decoded key return key.compareTo(other.key); } } // Return a list of the files in the given directory, sorted lexically by // the Base64-decoded file name, not by the on-disk filename private ArrayList<DecodedFilename> contentsByKey(File dir) { File[] allFiles = dir.listFiles(); if (allFiles == null || allFiles.length == 0) { return null; } // Decode the filenames into keys then sort lexically by key ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>(); for (File f : allFiles) { contents.add(new DecodedFilename(f)); } Collections.sort(contents); return contents; } @Override public void finishRestore() { if (DEBUG) Log.v(TAG, "finishRestore()"); if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) { resetFullRestoreState(); } mRestoreType = 0; } // ------------------------------------------------------------------------------------ // Full restore handling private void resetFullRestoreState() { IoUtils.closeQuietly(mCurFullRestoreStream); mCurFullRestoreStream = null; mFullRestoreSocketStream = null; mFullRestoreBuffer = null; }
Ask the transport to provide data for the "current" package being restored. The transport then writes some data to the socket supplied to this call, and returns the number of bytes written. The system will then read that many bytes and stream them to the application's agent for restore, then will call this method again to receive the next chunk of the archive. This sequence will be repeated until the transport returns zero indicating that all of the package's data has been delivered (or returns a negative value indicating some sort of hard error condition at the transport level).

After this method returns zero, the system will then call getNextFullRestorePackage() to begin the restore process for the next application, and the sequence begins again.

Params:
  • socket – The file descriptor that the transport will use for delivering the streamed archive.
Returns:0 when no more data for the current package is available. A positive value indicates the presence of that much data to be delivered to the app. A negative return value is treated as equivalent to BackupTransport.TRANSPORT_ERROR, indicating a fatal error condition that precludes further restore operations on the current dataset.
/** * Ask the transport to provide data for the "current" package being restored. The * transport then writes some data to the socket supplied to this call, and returns * the number of bytes written. The system will then read that many bytes and * stream them to the application's agent for restore, then will call this method again * to receive the next chunk of the archive. This sequence will be repeated until the * transport returns zero indicating that all of the package's data has been delivered * (or returns a negative value indicating some sort of hard error condition at the * transport level). * * <p>After this method returns zero, the system will then call * {@link #getNextFullRestorePackage()} to begin the restore process for the next * application, and the sequence begins again. * * @param socket The file descriptor that the transport will use for delivering the * streamed archive. * @return 0 when no more data for the current package is available. A positive value * indicates the presence of that much data to be delivered to the app. A negative * return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR}, * indicating a fatal error condition that precludes further restore operations * on the current dataset. */
@Override public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) { if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { throw new IllegalStateException("Asked for full restore data for non-stream package"); } // first chunk? if (mCurFullRestoreStream == null) { final String name = mRestorePackages[mRestorePackage].packageName; if (DEBUG) Log.i(TAG, "Starting full restore of " + name); File dataset = new File(mRestoreSetFullDir, name); try { mCurFullRestoreStream = new FileInputStream(dataset); } catch (IOException e) { // If we can't open the target package's tarball, we return the single-package // error code and let the caller go on to the next package. Log.e(TAG, "Unable to read archive for " + name); return TRANSPORT_PACKAGE_REJECTED; } mFullRestoreSocketStream = new FileOutputStream(socket.getFileDescriptor()); mFullRestoreBuffer = new byte[2*1024]; } int nRead; try { nRead = mCurFullRestoreStream.read(mFullRestoreBuffer); if (nRead < 0) { // EOF: tell the caller we're done nRead = NO_MORE_DATA; } else if (nRead == 0) { // This shouldn't happen when reading a FileInputStream; we should always // get either a positive nonzero byte count or -1. Log the situation and // treat it as EOF. Log.w(TAG, "read() of archive file returned 0; treating as EOF"); nRead = NO_MORE_DATA; } else { if (DEBUG) { Log.i(TAG, " delivering restore chunk: " + nRead); } mFullRestoreSocketStream.write(mFullRestoreBuffer, 0, nRead); } } catch (IOException e) { return TRANSPORT_ERROR; // Hard error accessing the file; shouldn't happen } finally { // Most transports will need to explicitly close 'socket' here, but this transport // is in the same process as the caller so it can leave it up to the backup manager // to manage both socket fds. } return nRead; }
If the OS encounters an error while processing RestoreDescription.TYPE_FULL_STREAM data for restore, it will invoke this method to tell the transport that it should abandon the data download for the current package. The OS will then either call nextRestorePackage() again to move on to restoring the next package in the set being iterated over, or will call finishRestore() to shut down the restore operation.
Returns:BackupTransport.TRANSPORT_OK if the transport was successful in shutting down the current stream cleanly, or BackupTransport.TRANSPORT_ERROR to indicate a serious transport-level failure. If the transport reports an error here, the entire restore operation will immediately be finished with no further attempts to restore app data.
/** * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM} * data for restore, it will invoke this method to tell the transport that it should * abandon the data download for the current package. The OS will then either call * {@link #nextRestorePackage()} again to move on to restoring the next package in the * set being iterated over, or will call {@link #finishRestore()} to shut down the restore * operation. * * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the * current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious * transport-level failure. If the transport reports an error here, the entire restore * operation will immediately be finished with no further attempts to restore app data. */
@Override public int abortFullRestore() { if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { throw new IllegalStateException("abortFullRestore() but not currently restoring"); } resetFullRestoreState(); mRestoreType = 0; return TRANSPORT_OK; } @Override public long getBackupQuota(String packageName, boolean isFullBackup) { return isFullBackup ? FULL_BACKUP_SIZE_QUOTA : KEY_VALUE_BACKUP_SIZE_QUOTA; } }