diff options
| author | 2018-09-07 14:52:44 +0000 | |
|---|---|---|
| committer | 2018-09-07 14:52:44 +0000 | |
| commit | 1dbac1a58f6f85100a2e0d24f6091ae9f217a4a7 (patch) | |
| tree | 6b3bbb22d3ef90bb8ed8f3b4fe4f1d4cc1a6d9af | |
| parent | b2fa27cc65affdb2e13f8363cc6336f6a4df764b (diff) | |
| parent | 1374690674f445ecbccdba7c90502079066ac0f1 (diff) | |
Merge "[FBR] Extract app metadata backup to helper"
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; + } +} |