Implement full data backup through transport

Currently no timed/scheduled full-data backup operations are
performed by the OS, but the plumbing is now in place and can
be tested using 'adb shell bmgr fullbackup pkg [pkg2 pkg3 ...]'.

The LocalTransport test transport implementation has been augmented
to support the new full-data backup API as well.

In addition, 'adb backup' now takes the -compress/-nocompress
command line options to control whether the resulting archive is
compressed before leaving the device.  Previously the archive was
always compressed.  (The default is still to compress, as it will
usually reduce the archive size considerably.)

Internally, the core implementation of gathering the full backup
data stream from the target application has been refactored into
an "engine" component that is shared by both 'adb backup' and the
transport-oriented full backup task.  The archive file header
generation, encryption, and compression logic are now factored out
of the engine itself instead of being hardwired into the data
handling.

Bug 15329632

Change-Id: I4a044faa4070d684ef457bd3e11771198cdf557c
diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java
index 7292116..c9d621d 100644
--- a/core/java/com/android/internal/backup/LocalTransport.java
+++ b/core/java/com/android/internal/backup/LocalTransport.java
@@ -34,12 +34,17 @@
 
 import com.android.org.bouncycastle.util.encoders.Base64;
 
+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.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
 
 import static android.system.OsConstants.*;
 
@@ -64,16 +69,29 @@
     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, "_delta");
+    private File mCurrentSetFullDir = new File(mCurrentSetDir, "_full");
 
     private PackageInfo[] mRestorePackages = null;
     private int mRestorePackage = -1;  // Index into mRestorePackages
     private File mRestoreDataDir;
     private long mRestoreToken;
 
+    // Additional bookkeeping for full backup
+    private String mFullTargetPackage;
+    private ParcelFileDescriptor mSocket;
+    private FileInputStream mSocketInputStream;
+    private BufferedOutputStream mFullBackupOutputStream;
+    private byte[] mFullBackupBuffer;
+
+    private File mFullRestoreSetDir;
+    private HashSet<String> mFullRestorePackages;
 
     public LocalTransport(Context context) {
         mContext = context;
         mCurrentSetDir.mkdirs();
+        mCurrentSetFullDir.mkdir();
+        mCurrentSetIncrementalDir.mkdir();
         if (!SELinux.restorecon(mCurrentSetDir)) {
             Log.e(TAG, "SELinux restorecon failed for " + mCurrentSetDir);
         }
@@ -119,7 +137,7 @@
             }
         }
 
-        File packageDir = new File(mCurrentSetDir, packageInfo.packageName);
+        File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
         packageDir.mkdirs();
 
         // Each 'record' in the restore set is kept in its own file, named by
@@ -200,7 +218,7 @@
     public int clearBackupData(PackageInfo packageInfo) {
         if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName);
 
-        File packageDir = new File(mCurrentSetDir, packageInfo.packageName);
+        File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName);
         final File[] fileset = packageDir.listFiles();
         if (fileset != null) {
             for (File f : fileset) {
@@ -208,27 +226,122 @@
             }
             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 BackupTransport.TRANSPORT_OK;
     }
 
     public int finishBackup() {
         if (DEBUG) Log.v(TAG, "finishBackup()");
+        if (mSocket != null) {
+            if (DEBUG) {
+                Log.v(TAG, "Concluding full backup of " + mFullTargetPackage);
+            }
+            try {
+                mFullBackupOutputStream.flush();
+                mFullBackupOutputStream.close();
+                mSocketInputStream = null;
+                mFullTargetPackage = null;
+                mSocket.close();
+            } catch (IOException e) {
+                return BackupTransport.TRANSPORT_ERROR;
+            } finally {
+                mSocket = null;
+            }
+        }
         return BackupTransport.TRANSPORT_OK;
     }
 
+    // ------------------------------------------------------------------------------------
+    // Full backup handling
+    public long requestFullBackupTime() {
+        return 0;
+    }
+
+    public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) {
+        if (mSocket != null) {
+            Log.e(TAG, "Attempt to initiate full backup while one is in progress");
+            return BackupTransport.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 {
+            mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
+            mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor());
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to process socket for full backup");
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        mFullTargetPackage = targetPackage.packageName;
+        FileOutputStream tarstream;
+        try {
+            File tarball = new File(mCurrentSetFullDir, mFullTargetPackage);
+            tarstream = new FileOutputStream(tarball);
+        } catch (FileNotFoundException e) {
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+        mFullBackupOutputStream = new BufferedOutputStream(tarstream);
+        mFullBackupBuffer = new byte[4096];
+
+        return BackupTransport.TRANSPORT_OK;
+    }
+
+    public int sendBackupData(int numBytes) {
+        if (mFullBackupBuffer == null) {
+            Log.w(TAG, "Attempted sendBackupData before performFullBackup");
+            return BackupTransport.TRANSPORT_ERROR;
+        }
+
+        if (numBytes > mFullBackupBuffer.length) {
+            mFullBackupBuffer = new byte[numBytes];
+        }
+        while (numBytes > 0) {
+            try {
+            int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, numBytes);
+            if (nRead < 0) {
+                // Something went wrong if we expect data but saw EOD
+                Log.w(TAG, "Unexpected EOD; failing backup");
+                return BackupTransport.TRANSPORT_ERROR;
+            }
+            mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
+            numBytes -= nRead;
+            } catch (IOException e) {
+                Log.e(TAG, "Error handling backup data for " + mFullTargetPackage);
+                return BackupTransport.TRANSPORT_ERROR;
+            }
+        }
+        return BackupTransport.TRANSPORT_OK;
+    }
+
+    // ------------------------------------------------------------------------------------
     // Restore handling
     static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; 
     public RestoreSet[] getAvailableRestoreSets() {
         long[] existing = new long[POSSIBLE_SETS.length + 1];
         int num = 0;
 
-        // see which possible non-current sets exist, then put the current set at the end
+        // 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
+        // ...and always the currently-active set last
         existing[num++] = CURRENT_SET_TOKEN;
 
         RestoreSet[] available = new RestoreSet[num];
@@ -345,4 +458,60 @@
     public void finishRestore() {
         if (DEBUG) Log.v(TAG, "finishRestore()");
     }
+
+    // ------------------------------------------------------------------------------------
+    // Full restore handling
+
+    public int prepareFullRestore(long token, String[] targetPackages) {
+        mRestoreDataDir = new File(mDataDir, Long.toString(token));
+        mFullRestoreSetDir = new File(mRestoreDataDir, "_full");
+        mFullRestorePackages = new HashSet<String>();
+        if (mFullRestoreSetDir.exists()) {
+            List<String> pkgs = Arrays.asList(mFullRestoreSetDir.list());
+            HashSet<String> available = new HashSet<String>(pkgs);
+
+            for (int i = 0; i < targetPackages.length; i++) {
+                if (available.contains(targetPackages[i])) {
+                    mFullRestorePackages.add(targetPackages[i]);
+                }
+            }
+        }
+        return BackupTransport.TRANSPORT_OK;
+    }
+
+    /**
+     * Ask the transport what package's full data will be restored next.  When all apps'
+     * data has been delivered, the transport should return {@code null} here.
+     * @return The package name of the next application whose data will be restored, or
+     *    {@code null} if all available package has been delivered.
+     */
+    public String getNextFullRestorePackage() {
+        return 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).
+     *
+     * <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.
+     */
+    public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
+        return 0;
+    }
 }