[FBR] Extract app metadata backup to helper

In full backup, we backup additional metadata about the app
(manifest, widget, apk, obb) not specified by the app's backup agent.
This CL extracts these methods out to their own helper (AppMetadataBackupWriter)
and adds unit tests for these methods.

** Note: The backup behavior is the same, only the structure has changed.
Behavioral changes will be done in future CLs. **

What this CL covers:
- Move the backup of this extra app data out of the FullBackupEngine to
separate agent data backup and non-agent data backup.
- Move logic of deciding what data to backup from FullBackupEngine to
FullBackupRunner (where the writer is used).
- Add unit tests for metadata backup.
- Some style fixes/clean up.

Not covered (future CLs):
- Refactoring FullBackupEngine/FullBackupRunner mechanism.
- Streaming backup data directly instead of writing to temporary files.
- Separating out and fixing apk and obb backup.

Bug: 110081582
Test: 1) atest AppDataBackupWriterTest
2) atest RunFrameworksServicesRoboTests
3) atest GtsBackupHostTestCases
4) Verify success for:
 - adb shell bmgr backupnow <full backup package>; adb restore 1 <full
backup package>
 - adb backup <full backup package>; adb restore
 - cloud backup and restore
5) Use local transport and adb backup to inspect manifest and widget data
written and file metadata consistent between runs.
6) Verify compatibility with adb backup -keyvalue manifest

Change-Id: Icb43fd2e0505c2416738ee3ef370b206363fac68
diff --git a/core/java/com/android/server/AppWidgetBackupBridge.java b/core/java/com/android/server/AppWidgetBackupBridge.java
index 2ea2f79..7d82d35 100644
--- a/core/java/com/android/server/AppWidgetBackupBridge.java
+++ b/core/java/com/android/server/AppWidgetBackupBridge.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
+
 import java.util.List;
 
 /**
@@ -37,6 +39,8 @@
                 : null;
     }
 
+    /** Returns a byte array of widget data for the specified package or {@code null}. */
+    @Nullable
     public static byte[] getWidgetState(String packageName, int userId) {
         return (sAppWidgetService != null)
                 ? sAppWidgetService.getWidgetState(packageName, userId)
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
index 30ec8ab..125c225 100644
--- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
@@ -9,9 +9,9 @@
 
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
-import android.app.backup.IBackupCallback;
 import android.app.backup.FullBackup;
 import android.app.backup.FullBackupDataOutput;
+import android.app.backup.IBackupCallback;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -21,6 +21,7 @@
 import android.util.Slog;
 
 import com.android.internal.util.Preconditions;
+import com.android.server.backup.fullbackup.AppMetadataBackupWriter;
 import com.android.server.backup.remote.ServiceBackupCallback;
 import com.android.server.backup.utils.FullBackupUtils;
 
@@ -202,16 +203,20 @@
         public void run() {
             try {
                 FullBackupDataOutput output = new FullBackupDataOutput(mPipe);
+                AppMetadataBackupWriter writer =
+                        new AppMetadataBackupWriter(output, mPackageManager);
 
                 if (DEBUG) {
                     Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
                 }
-                FullBackupUtils.writeAppManifest(
-                        mPackage, mPackageManager, mManifestFile, false, false);
-                FullBackup.backupToTar(mPackage.packageName, FullBackup.KEY_VALUE_DATA_TOKEN, null,
-                        mDataDir.getAbsolutePath(),
-                        mManifestFile.getAbsolutePath(),
-                        output);
+
+                writer.backupManifest(
+                        mPackage,
+                        mManifestFile,
+                        mDataDir,
+                        FullBackup.KEY_VALUE_DATA_TOKEN,
+                        /* linkDomain */ null,
+                        /* withApk */ false);
                 mManifestFile.delete();
 
                 if (DEBUG) {
diff --git a/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
new file mode 100644
index 0000000..94365d7
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
@@ -0,0 +1,283 @@
+package com.android.server.backup.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_VERSION;
+import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_VERSION;
+import static com.android.server.backup.BackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
+import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.annotation.Nullable;
+import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.StringBuilderPrinter;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Writes the backup of app-specific metadata to {@link FullBackupDataOutput}. This data is not
+ * backed up by the app's backup agent and is written before the agent writes its own data. This
+ * includes the app's:
+ *
+ * <ul>
+ *   <li>manifest
+ *   <li>widget data
+ *   <li>apk
+ *   <li>obb content
+ * </ul>
+ */
+// TODO(b/113807190): Fix or remove apk and obb implementation (only used for adb).
+public class AppMetadataBackupWriter {
+    private final FullBackupDataOutput mOutput;
+    private final PackageManager mPackageManager;
+
+    /** The destination of the backup is specified by {@code output}. */
+    public AppMetadataBackupWriter(FullBackupDataOutput output, PackageManager packageManager) {
+        mOutput = output;
+        mPackageManager = packageManager;
+    }
+
+    /**
+     * Back up the app's manifest without specifying a pseudo-directory for the TAR stream.
+     *
+     * @see #backupManifest(PackageInfo, File, File, String, String, boolean)
+     */
+    public void backupManifest(
+            PackageInfo packageInfo, File manifestFile, File filesDir, boolean withApk)
+            throws IOException {
+        backupManifest(
+                packageInfo,
+                manifestFile,
+                filesDir,
+                /* domain */ null,
+                /* linkDomain */ null,
+                withApk);
+    }
+
+    /**
+     * Back up the app's manifest.
+     *
+     * <ol>
+     *   <li>Write the app's manifest data to the specified temporary file {@code manifestFile}.
+     *   <li>Backup the file in TAR format to the backup destination {@link #mOutput}.
+     * </ol>
+     *
+     * <p>Note: {@code domain} and {@code linkDomain} are only used by adb to specify a
+     * pseudo-directory for the TAR stream.
+     */
+    // TODO(b/113806991): Look into streaming the backup data directly.
+    public void backupManifest(
+            PackageInfo packageInfo,
+            File manifestFile,
+            File filesDir,
+            @Nullable String domain,
+            @Nullable String linkDomain,
+            boolean withApk)
+            throws IOException {
+        byte[] manifestBytes = getManifestBytes(packageInfo, withApk);
+        FileOutputStream outputStream = new FileOutputStream(manifestFile);
+        outputStream.write(manifestBytes);
+        outputStream.close();
+
+        // We want the manifest block in the archive stream to be constant each time we generate
+        // a backup stream for the app. However, the underlying TAR mechanism sees it as a file and
+        // will propagate its last modified time. We pin the last modified time to zero to prevent
+        // the TAR header from varying.
+        manifestFile.setLastModified(0);
+
+        FullBackup.backupToTar(
+                packageInfo.packageName,
+                domain,
+                linkDomain,
+                filesDir.getAbsolutePath(),
+                manifestFile.getAbsolutePath(),
+                mOutput);
+    }
+
+    /**
+     * Gets the app's manifest as a byte array. All data are strings ending in LF.
+     *
+     * <p>The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    private byte[] getManifestBytes(PackageInfo packageInfo, boolean withApk) {
+        String packageName = packageInfo.packageName;
+        StringBuilder builder = new StringBuilder(4096);
+        StringBuilderPrinter printer = new StringBuilderPrinter(builder);
+
+        printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
+        printer.println(packageName);
+        printer.println(Long.toString(packageInfo.getLongVersionCode()));
+        printer.println(Integer.toString(Build.VERSION.SDK_INT));
+
+        String installerName = mPackageManager.getInstallerPackageName(packageName);
+        printer.println((installerName != null) ? installerName : "");
+
+        printer.println(withApk ? "1" : "0");
+
+        // Write the signature block.
+        SigningInfo signingInfo = packageInfo.signingInfo;
+        if (signingInfo == null) {
+            printer.println("0");
+        } else {
+            // Retrieve the newest signatures to write.
+            // TODO (b/73988180) use entire signing history in case of rollbacks.
+            Signature[] signatures = signingInfo.getApkContentsSigners();
+            printer.println(Integer.toString(signatures.length));
+            for (Signature sig : signatures) {
+                printer.println(sig.toCharsString());
+            }
+        }
+        return builder.toString().getBytes();
+    }
+
+    /**
+     * Backup specified widget data. The widget data is prefaced by a metadata header.
+     *
+     * <ol>
+     *   <li>Write a metadata header to the specified temporary file {@code metadataFile}.
+     *   <li>Write widget data bytes to the same file.
+     *   <li>Backup the file in TAR format to the backup destination {@link #mOutput}.
+     * </ol>
+     *
+     * @throws IllegalArgumentException if the widget data provided is empty.
+     */
+    // TODO(b/113806991): Look into streaming the backup data directly.
+    public void backupWidget(
+            PackageInfo packageInfo, File metadataFile, File filesDir, byte[] widgetData)
+            throws IOException {
+        Preconditions.checkArgument(widgetData.length > 0, "Can't backup widget with no data.");
+
+        String packageName = packageInfo.packageName;
+        FileOutputStream fileOutputStream = new FileOutputStream(metadataFile);
+        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+        DataOutputStream dataOutputStream = new DataOutputStream(bufferedOutputStream);
+
+        byte[] metadata = getMetadataBytes(packageName);
+        bufferedOutputStream.write(metadata); // bypassing DataOutputStream
+        writeWidgetData(dataOutputStream, widgetData);
+        bufferedOutputStream.flush();
+        dataOutputStream.close();
+
+        // As with the manifest file, guarantee consistency of the archive metadata for the widget
+        // block by using a fixed last modified time on the metadata file.
+        metadataFile.setLastModified(0);
+
+        FullBackup.backupToTar(
+                packageName,
+                /* domain */ null,
+                /* linkDomain */ null,
+                filesDir.getAbsolutePath(),
+                metadataFile.getAbsolutePath(),
+                mOutput);
+    }
+
+    /**
+     * Gets the app's metadata as a byte array. All entries are strings ending in LF.
+     *
+     * <p>The metadata format is:
+     *
+     * <pre>
+     *     BACKUP_METADATA_VERSION
+     *     package name
+     * </pre>
+     */
+    private byte[] getMetadataBytes(String packageName) {
+        StringBuilder builder = new StringBuilder(512);
+        StringBuilderPrinter printer = new StringBuilderPrinter(builder);
+        printer.println(Integer.toString(BACKUP_METADATA_VERSION));
+        printer.println(packageName);
+        return builder.toString().getBytes();
+    }
+
+    /**
+     * Write a byte array of widget data to the specified output stream. All integers are binary in
+     * network byte order.
+     *
+     * <p>The widget data format:
+     *
+     * <pre>
+     *     4 : Integer token identifying the widget data blob.
+     *     4 : Integer size of the widget data.
+     *     N : Raw bytes of the widget data.
+     * </pre>
+     */
+    private void writeWidgetData(DataOutputStream out, byte[] widgetData) throws IOException {
+        out.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
+        out.writeInt(widgetData.length);
+        out.write(widgetData);
+    }
+
+    /**
+     * Backup the app's .apk to the backup destination {@link #mOutput}. Currently only used for
+     * 'adb backup'.
+     */
+    // TODO(b/113807190): Investigate and potentially remove.
+    public void backupApk(PackageInfo packageInfo) {
+        // TODO: handle backing up split APKs
+        String appSourceDir = packageInfo.applicationInfo.getBaseCodePath();
+        String apkDir = new File(appSourceDir).getParent();
+        FullBackup.backupToTar(
+                packageInfo.packageName,
+                FullBackup.APK_TREE_TOKEN,
+                /* linkDomain */ null,
+                apkDir,
+                appSourceDir,
+                mOutput);
+    }
+
+    /**
+     * Backup the app's .obb files to the backup destination {@link #mOutput}. Currently only used
+     * for 'adb backup'.
+     */
+    // TODO(b/113807190): Investigate and potentially remove.
+    public void backupObb(PackageInfo packageInfo) {
+        // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM doesn't have access to
+        // external storage.
+        // TODO: http://b/22388012
+        Environment.UserEnvironment userEnv =
+                new Environment.UserEnvironment(UserHandle.USER_SYSTEM);
+        File obbDir = userEnv.buildExternalStorageAppObbDirs(packageInfo.packageName)[0];
+        if (obbDir != null) {
+            if (MORE_DEBUG) {
+                Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
+            }
+            File[] obbFiles = obbDir.listFiles();
+            if (obbFiles != null) {
+                String obbDirName = obbDir.getAbsolutePath();
+                for (File obb : obbFiles) {
+                    FullBackup.backupToTar(
+                            packageInfo.packageName,
+                            FullBackup.OBB_TREE_TOKEN,
+                            /* linkDomain */ null,
+                            obbDirName,
+                            obb.getAbsolutePath(),
+                            mOutput);
+                }
+            }
+        }
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index 16906f7..c9f7218 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -18,8 +18,6 @@
 
 import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_FILENAME;
-import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_VERSION;
-import static com.android.server.backup.BackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
 import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.OP_TYPE_BACKUP_WAIT;
@@ -29,17 +27,14 @@
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
 import android.app.backup.BackupTransport;
-import android.app.backup.FullBackup;
 import android.app.backup.FullBackupDataOutput;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
-import android.os.Environment.UserEnvironment;
+import android.content.pm.PackageManager;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.util.Log;
 import android.util.Slog;
-import android.util.StringBuilderPrinter;
 
 import com.android.internal.util.Preconditions;
 import com.android.server.AppWidgetBackupBridge;
@@ -49,27 +44,20 @@
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.utils.FullBackupUtils;
 
-import java.io.BufferedOutputStream;
-import java.io.DataOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 
 /**
- * Core logic for performing one package's full backup, gathering the tarball from the
- * application and emitting it to the designated OutputStream.
+ * Core logic for performing one package's full backup, gathering the tarball from the application
+ * and emitting it to the designated OutputStream.
  */
 public class FullBackupEngine {
-
     private BackupManagerService backupManagerService;
     OutputStream mOutput;
     FullBackupPreflight mPreflightHook;
     BackupRestoreTask mTimeoutMonitor;
     IBackupAgent mAgent;
-    File mFilesDir;
-    File mManifestFile;
-    File mMetadataFile;
     boolean mIncludeApks;
     PackageInfo mPkg;
     private final long mQuota;
@@ -78,79 +66,91 @@
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
 
     class FullBackupRunner implements Runnable {
+        private final PackageManager mPackageManager;
+        private final PackageInfo mPackage;
+        private final IBackupAgent mAgent;
+        private final ParcelFileDescriptor mPipe;
+        private final int mToken;
+        private final boolean mIncludeApks;
+        private final File mFilesDir;
 
-        PackageInfo mPackage;
-        byte[] mWidgetData;
-        IBackupAgent mAgent;
-        ParcelFileDescriptor mPipe;
-        int mToken;
-        boolean mSendApk;
-        boolean mWriteManifest;
-
-        FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe,
-                int token, boolean sendApk, boolean writeManifest, byte[] widgetData)
+        FullBackupRunner(
+                PackageInfo packageInfo,
+                IBackupAgent agent,
+                ParcelFileDescriptor pipe,
+                int token,
+                boolean includeApks)
                 throws IOException {
-            mPackage = pack;
-            mWidgetData = widgetData;
+            mPackageManager = backupManagerService.getPackageManager();
+            mPackage = packageInfo;
             mAgent = agent;
             mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor());
             mToken = token;
-            mSendApk = sendApk;
-            mWriteManifest = writeManifest;
+            mIncludeApks = includeApks;
+            mFilesDir = new File("/data/system");
         }
 
         @Override
         public void run() {
             try {
-                FullBackupDataOutput output = new FullBackupDataOutput(
-                        mPipe, -1, mTransportFlags);
+                FullBackupDataOutput output =
+                        new FullBackupDataOutput(mPipe, /* quota */ -1, mTransportFlags);
+                AppMetadataBackupWriter appMetadataBackupWriter =
+                        new AppMetadataBackupWriter(output, mPackageManager);
 
-                if (mWriteManifest) {
-                    final boolean writeWidgetData = mWidgetData != null;
+                String packageName = mPackage.packageName;
+                boolean isSharedStorage = SHARED_BACKUP_AGENT_PACKAGE.equals(packageName);
+                boolean writeApk =
+                        shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage);
+
+                if (!isSharedStorage) {
                     if (MORE_DEBUG) {
-                        Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
+                        Slog.d(TAG, "Writing manifest for " + packageName);
                     }
-                    FullBackupUtils
-                            .writeAppManifest(mPackage, backupManagerService.getPackageManager(),
-                                    mManifestFile, mSendApk,
-                                    writeWidgetData);
-                    FullBackup.backupToTar(mPackage.packageName, null, null,
-                            mFilesDir.getAbsolutePath(),
-                            mManifestFile.getAbsolutePath(),
-                            output);
-                    mManifestFile.delete();
 
-                    // We only need to write a metadata file if we have widget data to stash
-                    if (writeWidgetData) {
-                        writeMetadata(mPackage, mMetadataFile, mWidgetData);
-                        FullBackup.backupToTar(mPackage.packageName, null, null,
-                                mFilesDir.getAbsolutePath(),
-                                mMetadataFile.getAbsolutePath(),
-                                output);
-                        mMetadataFile.delete();
+                    File manifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
+                    appMetadataBackupWriter.backupManifest(
+                            mPackage, manifestFile, mFilesDir, writeApk);
+                    manifestFile.delete();
+
+                    // Write widget data.
+                    // TODO: http://b/22388012
+                    byte[] widgetData =
+                            AppWidgetBackupBridge.getWidgetState(
+                                    packageName, UserHandle.USER_SYSTEM);
+                    if (widgetData != null && widgetData.length > 0) {
+                        File metadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
+                        appMetadataBackupWriter.backupWidget(
+                                mPackage, metadataFile, mFilesDir, widgetData);
+                        metadataFile.delete();
                     }
                 }
 
-                if (mSendApk) {
-                    writeApkToBackup(mPackage, output);
+                // TODO(b/113807190): Look into removing, only used for 'adb backup'.
+                if (writeApk) {
+                    appMetadataBackupWriter.backupApk(mPackage);
+                    appMetadataBackupWriter.backupObb(mPackage);
                 }
 
-                final boolean isSharedStorage =
-                        mPackage.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
-                final long timeout = isSharedStorage ?
-                        mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis() :
-                        mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
-
                 if (DEBUG) {
-                    Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName);
+                    Slog.d(TAG, "Calling doFullBackup() on " + packageName);
                 }
-                backupManagerService
-                        .prepareOperationTimeout(mToken,
-                                timeout,
-                                mTimeoutMonitor /* in parent class */,
-                                OP_TYPE_BACKUP_WAIT);
-                mAgent.doFullBackup(mPipe, mQuota, mToken,
-                        backupManagerService.getBackupManagerBinder(), mTransportFlags);
+
+                long timeout =
+                        isSharedStorage
+                                ? mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis()
+                                : mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
+                backupManagerService.prepareOperationTimeout(
+                        mToken,
+                        timeout,
+                        mTimeoutMonitor /* in parent class */,
+                        OP_TYPE_BACKUP_WAIT);
+                mAgent.doFullBackup(
+                        mPipe,
+                        mQuota,
+                        mToken,
+                        backupManagerService.getBackupManagerBinder(),
+                        mTransportFlags);
             } catch (IOException e) {
                 Slog.e(TAG, "Error running full backup for " + mPackage.packageName);
             } catch (RemoteException e) {
@@ -162,12 +162,33 @@
                 }
             }
         }
+
+        /**
+         * Don't write apks for forward-locked apps or system-bundled apps that are not upgraded.
+         */
+        private boolean shouldWriteApk(
+                ApplicationInfo applicationInfo, boolean includeApks, boolean isSharedStorage) {
+            boolean isForwardLocked =
+                    (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) != 0;
+            boolean isSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+            boolean isUpdatedSystemApp =
+                    (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+            return includeApks
+                    && !isSharedStorage
+                    && !isForwardLocked
+                    && (!isSystemApp || isUpdatedSystemApp);
+        }
     }
 
-    public FullBackupEngine(BackupManagerService backupManagerService,
+    public FullBackupEngine(
+            BackupManagerService backupManagerService,
             OutputStream output,
-            FullBackupPreflight preflightHook, PackageInfo pkg,
-            boolean alsoApks, BackupRestoreTask timeoutMonitor, long quota, int opToken,
+            FullBackupPreflight preflightHook,
+            PackageInfo pkg,
+            boolean alsoApks,
+            BackupRestoreTask timeoutMonitor,
+            long quota,
+            int opToken,
             int transportFlags) {
         this.backupManagerService = backupManagerService;
         mOutput = output;
@@ -175,15 +196,13 @@
         mPkg = pkg;
         mIncludeApks = alsoApks;
         mTimeoutMonitor = timeoutMonitor;
-        mFilesDir = new File("/data/system");
-        mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
-        mMetadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
         mQuota = quota;
         mOpToken = opToken;
         mTransportFlags = transportFlags;
-        mAgentTimeoutParameters = Preconditions.checkNotNull(
-                backupManagerService.getAgentTimeoutParameters(),
-                "Timeout parameters cannot be null");
+        mAgentTimeoutParameters =
+                Preconditions.checkNotNull(
+                        backupManagerService.getAgentTimeoutParameters(),
+                        "Timeout parameters cannot be null");
     }
 
     public int preflightCheck() throws RemoteException {
@@ -213,27 +232,13 @@
             try {
                 pipes = ParcelFileDescriptor.createPipe();
 
-                ApplicationInfo app = mPkg.applicationInfo;
-                final boolean isSharedStorage =
-                        mPkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
-                final boolean sendApk = mIncludeApks
-                        && !isSharedStorage
-                        && ((app.privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) == 0)
-                        && ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ||
-                        (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
-
-                // TODO: http://b/22388012
-                byte[] widgetBlob = AppWidgetBackupBridge.getWidgetState(mPkg.packageName,
-                        UserHandle.USER_SYSTEM);
-
-                FullBackupRunner runner = new FullBackupRunner(mPkg, mAgent, pipes[1],
-                        mOpToken, sendApk, !isSharedStorage, widgetBlob);
-                pipes[1].close();   // the runner has dup'd it
+                FullBackupRunner runner =
+                        new FullBackupRunner(mPkg, mAgent, pipes[1], mOpToken, mIncludeApks);
+                pipes[1].close(); // the runner has dup'd it
                 pipes[1] = null;
                 Thread t = new Thread(runner, "app-data-runner");
                 t.start();
 
-                // Now pull data from the app and stuff it into the output
                 FullBackupUtils.routeSocketDataToOutput(pipes[0], mOutput);
 
                 if (!backupManagerService.waitUntilOperationComplete(mOpToken)) {
@@ -288,84 +293,13 @@
             if (MORE_DEBUG) {
                 Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName);
             }
-            mAgent = backupManagerService.bindToAgentSynchronous(mPkg.applicationInfo,
-                    ApplicationThreadConstants.BACKUP_MODE_FULL);
+            mAgent =
+                    backupManagerService.bindToAgentSynchronous(
+                            mPkg.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_FULL);
         }
         return mAgent != null;
     }
 
-    private void writeApkToBackup(PackageInfo pkg, FullBackupDataOutput output) {
-        // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here
-        // TODO: handle backing up split APKs
-        final String appSourceDir = pkg.applicationInfo.getBaseCodePath();
-        final String apkDir = new File(appSourceDir).getParent();
-        FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null,
-                apkDir, appSourceDir, output);
-
-        // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM
-        // doesn't have access to external storage.
-
-        // Save associated .obb content if it exists and we did save the apk
-        // check for .obb and save those too
-        // TODO: http://b/22388012
-        final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_SYSTEM);
-        final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0];
-        if (obbDir != null) {
-            if (MORE_DEBUG) {
-                Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
-            }
-            File[] obbFiles = obbDir.listFiles();
-            if (obbFiles != null) {
-                final String obbDirName = obbDir.getAbsolutePath();
-                for (File obb : obbFiles) {
-                    FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null,
-                            obbDirName, obb.getAbsolutePath(), output);
-                }
-            }
-        }
-    }
-
-    // Widget metadata format. All header entries are strings ending in LF:
-    //
-    // Version 1 header:
-    //     BACKUP_METADATA_VERSION, currently "1"
-    //     package name
-    //
-    // File data (all integers are binary in network byte order)
-    // *N: 4 : integer token identifying which metadata blob
-    //     4 : integer size of this blob = N
-    //     N : raw bytes of this metadata blob
-    //
-    // Currently understood blobs (always in network byte order):
-    //
-    //     widgets : metadata token = 0x01FFED01 (BACKUP_WIDGET_METADATA_TOKEN)
-    //
-    // Unrecognized blobs are *ignored*, not errors.
-    private void writeMetadata(PackageInfo pkg, File destination, byte[] widgetData)
-            throws IOException {
-        StringBuilder b = new StringBuilder(512);
-        StringBuilderPrinter printer = new StringBuilderPrinter(b);
-        printer.println(Integer.toString(BACKUP_METADATA_VERSION));
-        printer.println(pkg.packageName);
-
-        FileOutputStream fout = new FileOutputStream(destination);
-        BufferedOutputStream bout = new BufferedOutputStream(fout);
-        DataOutputStream out = new DataOutputStream(bout);
-        bout.write(b.toString().getBytes());    // bypassing DataOutputStream
-
-        if (widgetData != null && widgetData.length > 0) {
-            out.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
-            out.writeInt(widgetData.length);
-            out.write(widgetData);
-        }
-        bout.flush();
-        out.close();
-
-        // As with the manifest file, guarantee idempotence of the archive metadata
-        // for the widget block by using a fixed mtime on the transient file.
-        destination.setLastModified(0);
-    }
-
     private void tearDown() {
         if (mPkg != null) {
             backupManagerService.tearDownAgentAndKill(mPkg.applicationInfo);
diff --git a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
index a3d5601..dbe3cd9 100644
--- a/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/FullBackupUtils.java
@@ -16,23 +16,14 @@
 
 package com.android.server.backup.utils;
 
-import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_VERSION;
 import static com.android.server.backup.BackupManagerService.TAG;
 
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.Signature;
-import android.content.pm.SigningInfo;
-import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.util.Slog;
-import android.util.StringBuilderPrinter;
 
 import java.io.DataInputStream;
 import java.io.EOFException;
-import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 
@@ -68,67 +59,4 @@
             }
         }
     }
-
-    /**
-     * Writes app manifest to the given manifest file.
-     *
-     * @param pkg - app package, which manifest to write.
-     * @param packageManager - {@link PackageManager} instance.
-     * @param manifestFile - target manifest file.
-     * @param withApk - whether include apk or not.
-     * @param withWidgets - whether to write widgets data.
-     * @throws IOException - in case of an error.
-     */
-    // TODO: withWidgets is not used, decide whether it is needed.
-    public static void writeAppManifest(PackageInfo pkg, PackageManager packageManager,
-            File manifestFile, boolean withApk, boolean withWidgets) throws IOException {
-        // Manifest format. All data are strings ending in LF:
-        //     BACKUP_MANIFEST_VERSION, currently 1
-        //
-        // Version 1:
-        //     package name
-        //     package's versionCode
-        //     platform versionCode
-        //     getInstallerPackageName() for this package (maybe empty)
-        //     boolean: "1" if archive includes .apk; any other string means not
-        //     number of signatures == N
-        // N*:    signature byte array in ascii format per Signature.toCharsString()
-        StringBuilder builder = new StringBuilder(4096);
-        StringBuilderPrinter printer = new StringBuilderPrinter(builder);
-
-        printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
-        printer.println(pkg.packageName);
-        printer.println(Long.toString(pkg.getLongVersionCode()));
-        printer.println(Integer.toString(Build.VERSION.SDK_INT));
-
-        String installerName = packageManager.getInstallerPackageName(pkg.packageName);
-        printer.println((installerName != null) ? installerName : "");
-
-        printer.println(withApk ? "1" : "0");
-
-        // write the signature block
-        SigningInfo signingInfo = pkg.signingInfo;
-        if (signingInfo == null) {
-            printer.println("0");
-        } else {
-            // retrieve the newest sigs to write
-            // TODO (b/73988180) use entire signing history in case of rollbacks
-            Signature[] signatures = signingInfo.getApkContentsSigners();
-            printer.println(Integer.toString(signatures.length));
-            for (Signature sig : signatures) {
-                printer.println(sig.toCharsString());
-            }
-        }
-
-        FileOutputStream outstream = new FileOutputStream(manifestFile);
-        outstream.write(builder.toString().getBytes());
-        outstream.close();
-
-        // We want the manifest block in the archive stream to be idempotent:
-        // each time we generate a backup stream for the app, we want the manifest
-        // block to be identical.  The underlying tar mechanism sees it as a file,
-        // though, and will propagate its mtime, causing the tar header to vary.
-        // Avoid this problem by pinning the mtime to zero.
-        manifestFile.setLastModified(0);
-    }
 }
diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk
index 3d7fdbdd..2691701 100644
--- a/services/robotests/Android.mk
+++ b/services/robotests/Android.mk
@@ -63,7 +63,9 @@
     $(call all-Iaidl-files-under, ../../core/java/android/app/backup) \
     ../../core/java/android/content/pm/PackageInfo.java \
     ../../core/java/android/app/IBackupAgent.aidl \
-    ../../core/java/android/util/KeyValueSettingObserver.java
+    ../../core/java/android/util/KeyValueSettingObserver.java \
+    ../../core/java/android/content/pm/PackageParser.java \
+    ../../core/java/android/content/pm/SigningInfo.java
 
 LOCAL_AIDL_INCLUDES := \
     $(call all-Iaidl-files-under, $(INTERNAL_BACKUP)) \
diff --git a/services/robotests/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java b/services/robotests/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
new file mode 100644
index 0000000..112e1e3
--- /dev/null
+++ b/services/robotests/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java
@@ -0,0 +1,495 @@
+package com.android.server.backup.fullbackup;
+
+import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_FILENAME;
+import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_VERSION;
+import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_FILENAME;
+import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_VERSION;
+import static com.android.server.backup.BackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.Nullable;
+import android.app.Application;
+import android.app.backup.BackupDataInput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser.SigningDetails;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.UserHandle;
+
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
+import com.android.server.testing.SystemLoaderPackages;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+import com.android.server.testing.shadows.ShadowFullBackup;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.attribute.FileTime;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+import org.robolectric.shadows.ShadowEnvironment;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+        manifest = Config.NONE,
+        sdk = 26,
+        shadows = {
+            ShadowBackupDataInput.class,
+            ShadowBackupDataOutput.class,
+            ShadowEnvironment.class,
+            ShadowFullBackup.class,
+        })
+@SystemLoaderPackages({"com.android.server.backup", "android.app.backup"})
+@SystemLoaderClasses({PackageInfo.class, SigningInfo.class})
+public class AppMetadataBackupWriterTest {
+    private static final String TEST_PACKAGE = "com.test.package";
+    private static final String TEST_PACKAGE_INSTALLER = "com.test.package.installer";
+    private static final Long TEST_PACKAGE_VERSION_CODE = 100L;
+
+    private ShadowApplicationPackageManager mShadowPackageManager;
+    private File mFilesDir;
+    private File mBackupDataOutputFile;
+    private AppMetadataBackupWriter mBackupWriter;
+
+    @Before
+    public void setUp() throws Exception {
+        Application application = RuntimeEnvironment.application;
+
+        PackageManager packageManager = application.getPackageManager();
+        mShadowPackageManager = (ShadowApplicationPackageManager) shadowOf(packageManager);
+
+        mFilesDir = RuntimeEnvironment.application.getFilesDir();
+        mBackupDataOutputFile = new File(mFilesDir, "output");
+        mBackupDataOutputFile.createNewFile();
+        ParcelFileDescriptor pfd =
+                ParcelFileDescriptor.open(
+                        mBackupDataOutputFile, ParcelFileDescriptor.MODE_READ_WRITE);
+        FullBackupDataOutput output =
+                new FullBackupDataOutput(pfd, /* quota */ -1, /* transportFlags */ 0);
+        mBackupWriter = new AppMetadataBackupWriter(output, packageManager);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mBackupDataOutputFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withoutApkOrSignatures_writesCorrectData() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[0]).isEqualTo(Integer.toString(BACKUP_MANIFEST_VERSION));
+        assertThat(manifest[1]).isEqualTo(TEST_PACKAGE);
+        assertThat(manifest[2]).isEqualTo(Long.toString(TEST_PACKAGE_VERSION_CODE));
+        assertThat(manifest[3]).isEqualTo(Integer.toString(Build.VERSION.SDK_INT));
+        assertThat(manifest[4]).isEqualTo(TEST_PACKAGE_INSTALLER);
+        assertThat(manifest[5]).isEqualTo("0"); // withApk
+        assertThat(manifest[6]).isEqualTo("0"); // signatures
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withApk_writesApk() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ true);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[5]).isEqualTo("1"); // withApk
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withSignatures_writesCorrectSignatures() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        packageInfo.signingInfo =
+                new SigningInfo(
+                        new SigningDetails(
+                                new Signature[] {new Signature("1234"), new Signature("5678")},
+                                SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                                null,
+                                null,
+                                null));
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(9);
+        assertThat(manifest[6]).isEqualTo("2"); // # of signatures
+        assertThat(manifest[7]).isEqualTo("1234"); // first signature
+        assertThat(manifest[8]).isEqualTo("5678"); // second signature
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Config(sdk = VERSION_CODES.O)
+    @Test
+    public void testBackupManifest_whenApiO_writesCorrectApi() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[3]).isEqualTo(Integer.toString(VERSION_CODES.O)); // platform version
+        manifestFile.delete();
+    }
+
+    /**
+     * The manifest format is:
+     *
+     * <pre>
+     *     BACKUP_MANIFEST_VERSION
+     *     package name
+     *     package version code
+     *     platform version code
+     *     installer package name (can be empty)
+     *     boolean (1 if archive includes .apk, otherwise 0)
+     *     # of signatures N
+     *     N* (signature byte array in ascii format per Signature.toCharsString())
+     * </pre>
+     */
+    @Test
+    public void testBackupManifest_withoutInstallerPackage_writesEmptyInstaller() throws Exception {
+        PackageInfo packageInfo = createPackageInfo(TEST_PACKAGE, null, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] manifestBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] manifest = new String(manifestBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(manifest.length).isEqualTo(7);
+        assertThat(manifest[4]).isEqualTo(""); // installer package name
+        manifestFile.delete();
+    }
+
+    @Test
+    public void testBackupManifest_whenRunPreviouslyWithSameData_producesSameBytesOnSecondRun()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File manifestFile = createFile(BACKUP_MANIFEST_FILENAME);
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+        byte[] firstRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        // Simulate modifying the manifest file to ensure that file metadata does not change the
+        // backup bytes produced.
+        modifyFileMetadata(manifestFile);
+
+        mBackupWriter.backupManifest(packageInfo, manifestFile, mFilesDir, /* withApk */ false);
+
+        byte[] secondRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        assertThat(firstRunBytes).isEqualTo(secondRunBytes);
+        manifestFile.delete();
+    }
+
+    /**
+     * The widget data format with metadata is:
+     *
+     * <pre>
+     *     BACKUP_METADATA_VERSION
+     *     package name
+     *     4 : Integer token identifying the widget data blob.
+     *     4 : Integer size of the widget data.
+     *     N : Raw bytes of the widget data.
+     * </pre>
+     */
+    @Test
+    public void testBackupWidget_writesCorrectData() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+        byte[] widgetBytes = "widget".getBytes();
+
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        String[] widgetData = new String(writtenBytes, StandardCharsets.UTF_8).split("\n");
+        assertThat(widgetData.length).isEqualTo(3);
+        // Metadata header
+        assertThat(widgetData[0]).isEqualTo(Integer.toString(BACKUP_METADATA_VERSION));
+        assertThat(widgetData[1]).isEqualTo(packageInfo.packageName);
+        // Widget data
+        ByteArrayOutputStream expectedBytes = new ByteArrayOutputStream();
+        DataOutputStream stream = new DataOutputStream(expectedBytes);
+        stream.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
+        stream.writeInt(widgetBytes.length);
+        stream.write(widgetBytes);
+        stream.flush();
+        assertThat(widgetData[2]).isEqualTo(expectedBytes.toString());
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_withNullWidgetData_throwsNullPointerException() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+
+        expectThrows(
+                NullPointerException.class,
+                () ->
+                        mBackupWriter.backupWidget(
+                                packageInfo, metadataFile, mFilesDir, /* widgetData */ null));
+
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_withEmptyWidgetData_throwsIllegalArgumentException()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+
+        expectThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mBackupWriter.backupWidget(
+                                packageInfo, metadataFile, mFilesDir, new byte[0]));
+
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupWidget_whenRunPreviouslyWithSameData_producesSameBytesOnSecondRun()
+            throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File metadataFile = createFile(BACKUP_METADATA_FILENAME);
+        byte[] widgetBytes = "widget".getBytes();
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+        byte[] firstRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        // Simulate modifying the metadata file to ensure that file metadata does not change the
+        // backup bytes produced.
+        modifyFileMetadata(metadataFile);
+
+        mBackupWriter.backupWidget(packageInfo, metadataFile, mFilesDir, widgetBytes);
+
+        byte[] secondRunBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ true);
+        assertThat(firstRunBytes).isEqualTo(secondRunBytes);
+        metadataFile.delete();
+    }
+
+    @Test
+    public void testBackupApk_writesCorrectBytesToOutput() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        byte[] apkBytes = "apk".getBytes();
+        File apkFile = createApkFileAndWrite(apkBytes);
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.sourceDir = apkFile.getPath();
+
+        mBackupWriter.backupApk(packageInfo);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        assertThat(writtenBytes).isEqualTo(apkBytes);
+        apkFile.delete();
+    }
+
+    @Test
+    public void testBackupObb_withObbData_writesCorrectBytesToOutput() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File obbDir = createObbDirForPackage(packageInfo.packageName);
+        byte[] obbBytes = "obb".getBytes();
+        File obbFile = createObbFileAndWrite(obbDir, obbBytes);
+
+        mBackupWriter.backupObb(packageInfo);
+
+        byte[] writtenBytes = getWrittenBytes(mBackupDataOutputFile, /* includeTarHeader */ false);
+        assertThat(writtenBytes).isEqualTo(obbBytes);
+        obbFile.delete();
+    }
+
+    @Test
+    public void testBackupObb_withNoObbData_doesNotWriteBytesToOutput() throws Exception {
+        PackageInfo packageInfo =
+                createPackageInfo(TEST_PACKAGE, TEST_PACKAGE_INSTALLER, TEST_PACKAGE_VERSION_CODE);
+        File obbDir = createObbDirForPackage(packageInfo.packageName);
+        // No obb file created.
+
+        mBackupWriter.backupObb(packageInfo);
+
+        assertThat(mBackupDataOutputFile.length()).isEqualTo(0);
+    }
+
+    /**
+     * Creates a test package and registers it with the package manager. Also sets the installer
+     * package name if not {@code null}.
+     */
+    private PackageInfo createPackageInfo(
+            String packageName, @Nullable String installerPackageName, long versionCode) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.setLongVersionCode(versionCode);
+        mShadowPackageManager.addPackage(packageInfo);
+        if (installerPackageName != null) {
+            mShadowPackageManager.setInstallerPackageName(packageName, installerPackageName);
+        }
+        return packageInfo;
+    }
+
+    /**
+     * Reads backup data written to the {@code file} by {@link ShadowBackupDataOutput}. Uses {@link
+     * ShadowBackupDataInput} to parse the data. Follows the format used by {@link
+     * ShadowFullBackup#backupToTar(String, String, String, String, String, FullBackupDataOutput)}.
+     *
+     * @param includeTarHeader If {@code true}, returns the TAR header and data bytes combined.
+     *     Otherwise, only returns the data bytes.
+     */
+    private byte[] getWrittenBytes(File file, boolean includeTarHeader) throws IOException {
+        BackupDataInput input = new BackupDataInput(new FileInputStream(file).getFD());
+        input.readNextHeader();
+        int dataSize = input.getDataSize();
+
+        byte[] bytes;
+        if (includeTarHeader) {
+            bytes = new byte[dataSize + 512];
+            input.readEntityData(bytes, 0, dataSize + 512);
+        } else {
+            input.readEntityData(new byte[512], 0, 512); // skip TAR header
+            bytes = new byte[dataSize];
+            input.readEntityData(bytes, 0, dataSize);
+        }
+
+        return bytes;
+    }
+
+    private File createFile(String fileName) throws IOException {
+        File file = new File(mFilesDir, fileName);
+        file.createNewFile();
+        return file;
+    }
+
+    /**
+     * Sets the last modified time of the {@code file} to the current time to edit the file's
+     * metadata.
+     */
+    private void modifyFileMetadata(File file) throws IOException {
+        Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(System.currentTimeMillis()));
+    }
+
+    private File createApkFileAndWrite(byte[] data) throws IOException {
+        File apkFile = new File(mFilesDir, "apk");
+        apkFile.createNewFile();
+        Files.write(apkFile.toPath(), data);
+        return apkFile;
+    }
+
+    /** Creates an .obb file in the input directory. */
+    private File createObbFileAndWrite(File obbDir, byte[] data) throws IOException {
+        File obbFile = new File(obbDir, "obb");
+        obbFile.createNewFile();
+        Files.write(obbFile.toPath(), data);
+        return obbFile;
+    }
+
+    /**
+     * Creates a package specific obb data directory since the backup method checks for obb data
+     * there. See {@link Environment#buildExternalStorageAppObbDirs(String)}.
+     */
+    private File createObbDirForPackage(String packageName) {
+        ShadowEnvironment.addExternalDir("test");
+        Environment.UserEnvironment userEnv =
+                new Environment.UserEnvironment(UserHandle.USER_SYSTEM);
+        File obbDir =
+                new File(
+                        userEnv.getExternalDirs()[0],
+                        Environment.DIR_ANDROID + "/obb/" + packageName);
+        obbDir.mkdirs();
+        return obbDir;
+    }
+}
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
index ca04008..5812c3c 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
@@ -55,6 +55,11 @@
         return mTransportFlags;
     }
 
+    public ObjectOutputStream getOutputStream() {
+        ensureOutput();
+        return mOutput;
+    }
+
     @Implementation
     public int writeEntityHeader(String key, int dataSize) throws IOException {
         ensureOutput();
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowFullBackup.java b/services/robotests/src/com/android/server/testing/shadows/ShadowFullBackup.java
new file mode 100644
index 0000000..3c913e3
--- /dev/null
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowFullBackup.java
@@ -0,0 +1,70 @@
+package com.android.server.testing.shadows;
+
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackup;
+import android.app.backup.FullBackupDataOutput;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Shadow for {@link FullBackup}. Used to emulate the native method {@link
+ * FullBackup#backupToTar(String, String, String, String, String, FullBackupDataOutput)}. Relies on
+ * the shadow {@link ShadowBackupDataOutput}, which must be included in tests that use this shadow.
+ */
+@Implements(FullBackup.class)
+public class ShadowFullBackup {
+    /**
+     * Reads data from the specified file at {@code path} and writes it to the {@code output}. Does
+     * not match the native implementation, and only partially simulates TAR format. Used solely for
+     * passing backup data for testing purposes.
+     *
+     * <p>Note: Only handles the {@code path} denoting a file and not a directory like the real
+     * implementation.
+     */
+    @Implementation
+    public static int backupToTar(
+            String packageName,
+            String domain,
+            String linkdomain,
+            String rootpath,
+            String path,
+            FullBackupDataOutput output) {
+        BackupDataOutput backupDataOutput = output.getData();
+        try {
+            Path file = Paths.get(path);
+            byte[] data = Files.readAllBytes(file);
+            backupDataOutput.writeEntityHeader("key", data.length);
+
+            // Partially simulate TAR header (not all fields included). We use a 512 byte block for
+            // the header to follow the TAR convention and to have a consistent size block to help
+            // with separating the header from the data.
+            ByteBuffer tarBlock = ByteBuffer.wrap(new byte[512]);
+            String tarPath = "apps/" + packageName + (domain == null ? "" : "/" + domain) + path;
+            tarBlock.put(tarPath.getBytes()); // file path
+            tarBlock.putInt(0x1ff); // file mode
+            tarBlock.putLong(Files.size(file)); // file size
+            tarBlock.putLong(Files.getLastModifiedTime(file).toMillis()); // last modified time
+            tarBlock.putInt(0); // file type
+
+            // Write TAR header directly to the BackupDataOutput's output stream.
+            ShadowBackupDataOutput shadowBackupDataOutput = Shadow.extract(backupDataOutput);
+            ObjectOutputStream outputStream = shadowBackupDataOutput.getOutputStream();
+            outputStream.write(tarBlock.array());
+            outputStream.flush();
+
+            backupDataOutput.writeEntityData(data, data.length);
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+        return 0;
+    }
+}