Merge "[FBR] Extract app metadata backup to helper"
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private final PackageManager mPackageManager;
|
||||
private final PackageInfo mPackage;
|
||||
private final IBackupAgent mAgent;
|
||||
private final ParcelFileDescriptor mPipe;
|
||||
private final int mToken;
|
||||
private final boolean mIncludeApks;
|
||||
private final File mFilesDir;
|
||||
|
||||
PackageInfo mPackage;
|
||||
byte[] mWidgetData;
|
||||
IBackupAgent mAgent;
|
||||
ParcelFileDescriptor mPipe;
|
||||
int mToken;
|
||||
boolean mSendApk;
|
||||
boolean mWriteManifest;
|
||||
|
||||
FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe,
|
||||
int token, boolean sendApk, boolean writeManifest, byte[] widgetData)
|
||||
FullBackupRunner(
|
||||
PackageInfo packageInfo,
|
||||
IBackupAgent agent,
|
||||
ParcelFileDescriptor pipe,
|
||||
int token,
|
||||
boolean includeApks)
|
||||
throws IOException {
|
||||
mPackage = pack;
|
||||
mWidgetData = widgetData;
|
||||
mPackageManager = backupManagerService.getPackageManager();
|
||||
mPackage = packageInfo;
|
||||
mAgent = agent;
|
||||
mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor());
|
||||
mToken = token;
|
||||
mSendApk = sendApk;
|
||||
mWriteManifest = writeManifest;
|
||||
mIncludeApks = includeApks;
|
||||
mFilesDir = new File("/data/system");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
FullBackupDataOutput output = new FullBackupDataOutput(
|
||||
mPipe, -1, mTransportFlags);
|
||||
FullBackupDataOutput output =
|
||||
new FullBackupDataOutput(mPipe, /* quota */ -1, mTransportFlags);
|
||||
AppMetadataBackupWriter appMetadataBackupWriter =
|
||||
new AppMetadataBackupWriter(output, mPackageManager);
|
||||
|
||||
if (mWriteManifest) {
|
||||
final boolean writeWidgetData = mWidgetData != null;
|
||||
String packageName = mPackage.packageName;
|
||||
boolean isSharedStorage = SHARED_BACKUP_AGENT_PACKAGE.equals(packageName);
|
||||
boolean writeApk =
|
||||
shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage);
|
||||
|
||||
if (!isSharedStorage) {
|
||||
if (MORE_DEBUG) {
|
||||
Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
|
||||
Slog.d(TAG, "Writing manifest for " + packageName);
|
||||
}
|
||||
FullBackupUtils
|
||||
.writeAppManifest(mPackage, backupManagerService.getPackageManager(),
|
||||
mManifestFile, mSendApk,
|
||||
writeWidgetData);
|
||||
FullBackup.backupToTar(mPackage.packageName, null, null,
|
||||
mFilesDir.getAbsolutePath(),
|
||||
mManifestFile.getAbsolutePath(),
|
||||
output);
|
||||
mManifestFile.delete();
|
||||
|
||||
// We only need to write a metadata file if we have widget data to stash
|
||||
if (writeWidgetData) {
|
||||
writeMetadata(mPackage, mMetadataFile, mWidgetData);
|
||||
FullBackup.backupToTar(mPackage.packageName, null, null,
|
||||
mFilesDir.getAbsolutePath(),
|
||||
mMetadataFile.getAbsolutePath(),
|
||||
output);
|
||||
mMetadataFile.delete();
|
||||
File manifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
|
||||
appMetadataBackupWriter.backupManifest(
|
||||
mPackage, manifestFile, mFilesDir, writeApk);
|
||||
manifestFile.delete();
|
||||
|
||||
// Write widget data.
|
||||
// TODO: http://b/22388012
|
||||
byte[] widgetData =
|
||||
AppWidgetBackupBridge.getWidgetState(
|
||||
packageName, UserHandle.USER_SYSTEM);
|
||||
if (widgetData != null && widgetData.length > 0) {
|
||||
File metadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
|
||||
appMetadataBackupWriter.backupWidget(
|
||||
mPackage, metadataFile, mFilesDir, widgetData);
|
||||
metadataFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
if (mSendApk) {
|
||||
writeApkToBackup(mPackage, output);
|
||||
// TODO(b/113807190): Look into removing, only used for 'adb backup'.
|
||||
if (writeApk) {
|
||||
appMetadataBackupWriter.backupApk(mPackage);
|
||||
appMetadataBackupWriter.backupObb(mPackage);
|
||||
}
|
||||
|
||||
final boolean isSharedStorage =
|
||||
mPackage.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
|
||||
final long timeout = isSharedStorage ?
|
||||
mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis() :
|
||||
mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
|
||||
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName);
|
||||
Slog.d(TAG, "Calling doFullBackup() on " + packageName);
|
||||
}
|
||||
backupManagerService
|
||||
.prepareOperationTimeout(mToken,
|
||||
timeout,
|
||||
mTimeoutMonitor /* in parent class */,
|
||||
OP_TYPE_BACKUP_WAIT);
|
||||
mAgent.doFullBackup(mPipe, mQuota, mToken,
|
||||
backupManagerService.getBackupManagerBinder(), mTransportFlags);
|
||||
|
||||
long timeout =
|
||||
isSharedStorage
|
||||
? mAgentTimeoutParameters.getSharedBackupAgentTimeoutMillis()
|
||||
: mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
|
||||
backupManagerService.prepareOperationTimeout(
|
||||
mToken,
|
||||
timeout,
|
||||
mTimeoutMonitor /* in parent class */,
|
||||
OP_TYPE_BACKUP_WAIT);
|
||||
mAgent.doFullBackup(
|
||||
mPipe,
|
||||
mQuota,
|
||||
mToken,
|
||||
backupManagerService.getBackupManagerBinder(),
|
||||
mTransportFlags);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Error running full backup for " + mPackage.packageName);
|
||||
} catch (RemoteException e) {
|
||||
@@ -162,12 +162,33 @@ 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) \
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user