summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/server/AppWidgetBackupBridge.java4
-rw-r--r--services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java19
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java283
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java270
-rw-r--r--services/backup/java/com/android/server/backup/utils/FullBackupUtils.java72
-rw-r--r--services/robotests/Android.mk4
-rw-r--r--services/robotests/src/com/android/server/backup/fullbackup/AppMetadataBackupWriterTest.java495
-rw-r--r--services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java5
-rw-r--r--services/robotests/src/com/android/server/testing/shadows/ShadowFullBackup.java70
9 files changed, 974 insertions, 248 deletions
diff --git a/core/java/com/android/server/AppWidgetBackupBridge.java b/core/java/com/android/server/AppWidgetBackupBridge.java
index 2ea2f794dac9..7d82d355e3eb 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 @@ 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)
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
index 30ec8ab711b6..125c2250d7d1 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 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) {
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 000000000000..94365d7dc02d
--- /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 16906f74c8a8..c9f72181bcaf 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 @@ package com.android.server.backup.fullbackup;
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 static com.android.server.backup.BackupManagerService.TAG;
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.BackupRestoreTask;
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 @@ public class FullBackupEngine {
private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
class FullBackupRunner implements Runnable {
-
- 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)
+ 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;
+
+ 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);
+
+ String packageName = mPackage.packageName;
+ boolean isSharedStorage = SHARED_BACKUP_AGENT_PACKAGE.equals(packageName);
+ boolean writeApk =
+ shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage);
- if (mWriteManifest) {
- final boolean writeWidgetData = mWidgetData != null;
+ 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 @@ public class FullBackupEngine {
}
}
}
+
+ /**
+ * 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 @@ public class FullBackupEngine {
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 @@ public class FullBackupEngine {
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 @@ public class FullBackupEngine {
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 a3d56011272f..dbe3cd9225b5 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 @@ 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);
- }
}
diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk
index 3d7fdbdd7436..2691701f79af 100644
--- a/services/robotests/Android.mk
+++ b/services/robotests/Android.mk
@@ -63,7 +63,9 @@ LOCAL_SRC_FILES := \
$(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 000000000000..112e1e385fed
--- /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 ca0400832345..5812c3c85a58 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 @@ public class ShadowBackupDataOutput {
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 000000000000..3c913e317375
--- /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;
+ }
+}