Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 13746906 authored by Annie Meng's avatar Annie Meng
Browse files

[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
parent c18bf578
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server;

import android.annotation.Nullable;

import java.util.List;

/**
@@ -37,6 +39,8 @@ public class AppWidgetBackupBridge {
                : 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)
+12 −7
Original line number Diff line number Diff line
@@ -9,9 +9,9 @@ import static com.android.server.backup.BackupManagerService.OP_TYPE_BACKUP_WAIT

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.os.SELinux;
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 class KeyValueAdbBackupEngine {
        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) {
+283 −0
Original line number Diff line number Diff line
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);
                }
            }
        }
    }
}
+102 −168

File changed.

Preview size limit exceeded, changes collapsed.

+0 −72
Original line number Diff line number Diff line
@@ -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 @@ public class FullBackupUtils {
            }
        }
    }

    /**
     * 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);
    }
}
Loading