Merge "Implement shared-storage full backup/restore"

This commit is contained in:
Christopher Tate
2011-06-07 12:25:06 -07:00
committed by Android (Google) Code Review
12 changed files with 287 additions and 84 deletions

View File

@ -278,7 +278,6 @@ public abstract class BackupAgent extends ContextWrapper {
int token, IBackupManager callbackBinder) throws RemoteException {
long ident = Binder.clearCallingIdentity();
try {
Log.d(TAG, "doRestoreFile() => onRestoreFile()");
BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime);
} catch (IOException e) {
throw new RuntimeException(e);

View File

@ -46,7 +46,7 @@ public class FullBackup {
public static final String SHARED_STORAGE_TOKEN = "shared";
public static final String APPS_PREFIX = "apps/";
public static final String SHARED_PREFIX = "shared/";
public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
@ -61,7 +61,8 @@ public class FullBackup {
String linkdomain, String rootpath, String path, BackupDataOutput output);
static public void restoreToFile(ParcelFileDescriptor data,
long size, int type, long mode, long mtime, File outFile) throws IOException {
long size, int type, long mode, long mtime, File outFile,
boolean doChmod) throws IOException {
if (type == FullBackup.TYPE_DIRECTORY) {
// Canonically a directory has no associated content, so we don't need to read
// anything from the pipe in this case. Just create the directory here and
@ -116,7 +117,7 @@ public class FullBackup {
}
// Now twiddle the state to match the backup, assuming all went well
if (outFile != null) {
if (doChmod && outFile != null) {
try {
Libcore.os.chmod(outFile.getPath(), (int)mode);
} catch (ErrnoException e) {

View File

@ -28,8 +28,6 @@ import libcore.io.OsConstants;
import libcore.io.StructStat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
@ -84,9 +82,10 @@ public class FullBackupAgent extends BackupAgent {
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) {
ParcelFileDescriptor newState) throws IOException {
// Filters, the scan queue, and the set of resulting entities
HashSet<String> filterSet = new HashSet<String>();
String packageName = getPackageName();
// Okay, start with the app's root tree, but exclude all of the canonical subdirs
if (mLibDir != null) {
@ -96,25 +95,28 @@ public class FullBackupAgent extends BackupAgent {
filterSet.add(mDatabaseDir);
filterSet.add(mSharedPrefsDir);
filterSet.add(mFilesDir);
processTree(FullBackup.ROOT_TREE_TOKEN, mMainDir, filterSet, data);
processTree(packageName, FullBackup.ROOT_TREE_TOKEN, mMainDir, filterSet, data);
// Now do the same for the files dir, db dir, and shared prefs dir
filterSet.add(mMainDir);
filterSet.remove(mFilesDir);
processTree(FullBackup.DATA_TREE_TOKEN, mFilesDir, filterSet, data);
processTree(packageName, FullBackup.DATA_TREE_TOKEN, mFilesDir, filterSet, data);
filterSet.add(mFilesDir);
filterSet.remove(mDatabaseDir);
processTree(FullBackup.DATABASE_TREE_TOKEN, mDatabaseDir, filterSet, data);
processTree(packageName, FullBackup.DATABASE_TREE_TOKEN, mDatabaseDir, filterSet, data);
filterSet.add(mDatabaseDir);
filterSet.remove(mSharedPrefsDir);
processTree(FullBackup.SHAREDPREFS_TREE_TOKEN, mSharedPrefsDir, filterSet, data);
processTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, mSharedPrefsDir, filterSet, data);
}
private void processTree(String domain, String rootPath,
// Scan the dir tree (if it actually exists) and process each entry we find. If the
// 'excludes' parameter is non-null, it is consulted each time a new file system entity
// is visited to see whether that entity (and its subtree, if appropriate) should be
// omitted from the backup process.
protected void processTree(String packageName, String domain, String rootPath,
HashSet<String> excludes, BackupDataOutput data) {
// Scan the dir tree (if it actually exists) and process each entry we find
File rootFile = new File(rootPath);
if (rootFile.exists()) {
LinkedList<File> scanQueue = new LinkedList<File>();
@ -125,7 +127,7 @@ public class FullBackupAgent extends BackupAgent {
String filePath = file.getAbsolutePath();
// prune this subtree?
if (excludes.contains(filePath)) {
if (excludes != null && excludes.contains(filePath)) {
continue;
}
@ -149,7 +151,7 @@ public class FullBackupAgent extends BackupAgent {
}
// Finally, back this file up before proceeding
FullBackup.backupToTar(getPackageName(), domain, null, rootPath, filePath, data);
FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, data);
}
}
}
@ -218,6 +220,6 @@ public class FullBackupAgent extends BackupAgent {
if (DEBUG) Log.i(TAG, "[" + domain + " : " + relpath + "] mapped to " + outFile.getPath());
// Now that we've figured out where the data goes, send it on its way
FullBackup.restoreToFile(data, size, type, mode, mtime, outFile);
FullBackup.restoreToFile(data, size, type, mode, mtime, outFile, true);
}
}

View File

@ -1517,11 +1517,12 @@ public class PackageParser {
}
}
// fullBackupAgent is explicitly handled even if allowBackup is false
name = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifestApplication_fullBackupAgent, 0);
if (name != null) {
ai.fullBackupAgentName = buildClassName(pkgName, name, outError);
if (true) {
if (false) {
Log.v(TAG, "android:fullBackupAgent=" + ai.fullBackupAgentName
+ " from " + pkgName + "+" + name);
}

View File

@ -73,6 +73,8 @@ static struct {
static int backupToTar(JNIEnv* env, jobject clazz, jstring packageNameObj,
jstring domainObj, jstring linkdomain,
jstring rootpathObj, jstring pathObj, jobject dataOutputObj) {
int ret;
// Extract the various strings, allowing for null object pointers
const char* packagenamechars = env->GetStringUTFChars(packageNameObj, NULL);
const char* rootchars = env->GetStringUTFChars(rootpathObj, NULL);

View File

@ -503,10 +503,10 @@ int write_tarfile(const String8& packageName, const String8& domain,
needExtended = true;
}
// Non-7bit-clean path also means needing pax extended format
// Non-7bit-clean path or embedded spaces also mean needing pax extended format
if (!needExtended) {
for (size_t i = 0; i < filepath.length(); i++) {
if ((filepath[i] & 0x80) != 0) {
if ((filepath[i] & 0x80) != 0 || filepath[i] == ' ') {
needExtended = true;
break;
}

View File

@ -0,0 +1,33 @@
#
# Copyright (C) 2011 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
LOCAL_PACKAGE_NAME := SharedStorageBackup
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
########################
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright 2011, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.sharedstoragebackup" >
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
<application android:allowClearUserData="false"
android:permission="android.permission.CONFIRM_FULL_BACKUP"
android:fullBackupAgent=".SharedStorageAgent"
android:allowBackup="false" >
</application>
</manifest>

View File

@ -0,0 +1 @@
-keep class com.android.sharedstoragebackup.SharedStorageAgent

View File

@ -0,0 +1,93 @@
package com.android.sharedstoragebackup;
import android.app.backup.FullBackup;
import android.app.backup.FullBackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.util.Slog;
import java.io.File;
import java.io.IOException;
public class SharedStorageAgent extends FullBackupAgent {
static final String TAG = "SharedStorageAgent";
static final boolean DEBUG = true;
StorageVolume[] mVolumes;
@Override
public void onCreate() {
StorageManager mgr = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
if (mgr != null) {
mVolumes = mgr.getVolumeList();
} else {
Slog.e(TAG, "Unable to access Storage Manager");
}
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
// If there are shared-storage volumes available, run the inherited directory-
// hierarchy backup process on them. By convention in the Storage Manager, the
// "primary" shared storage volume is first in the list.
if (mVolumes != null) {
for (int i = 0; i < mVolumes.length; i++) {
StorageVolume v = mVolumes[i];
// Express the contents of volume N this way in the tar stream:
// shared/N/path/to/file
// The restore will then extract to the given volume
String domain = FullBackup.SHARED_PREFIX + i;
processTree(null, domain, v.getPath(), null, data);
}
}
}
/**
* Incremental onRestore() implementation is not used.
*/
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
throws IOException {
}
/**
* Full restore of one file to shared storage
*/
@Override
public void onRestoreFile(ParcelFileDescriptor data, long size,
int type, String domain, String relpath, long mode, long mtime)
throws IOException {
Slog.d(TAG, "Shared restore: [ " + domain + " : " + relpath + "]");
File outFile = null;
// The file path must be in the semantic form [number]/path/to/file...
int slash = relpath.indexOf('/');
if (slash > 0) {
try {
int i = Integer.parseInt(relpath.substring(0, slash));
if (i <= mVolumes.length) {
outFile = new File(mVolumes[i].getPath(), relpath.substring(slash + 1));
if (DEBUG) Slog.i(TAG, " => " + outFile.getAbsolutePath());
} else {
Slog.w(TAG, "Cannot restore data for unavailable volume " + i);
}
} catch (NumberFormatException e) {
if (DEBUG) Slog.w(TAG, "Bad volume number token: " + relpath.substring(0, slash));
}
} else {
if (DEBUG) Slog.i(TAG, "Can't find volume-number token");
}
if (outFile == null) {
Slog.e(TAG, "Skipping data with malformed path " + relpath);
}
FullBackup.restoreToFile(data, size, type, mode, mtime, outFile, false);
}
}

View File

@ -134,6 +134,7 @@ class BackupManagerService extends IBackupManager.Stub {
// Timeout intervals for agent backup & restore operations
static final long TIMEOUT_BACKUP_INTERVAL = 30 * 1000;
static final long TIMEOUT_FULL_BACKUP_INTERVAL = 5 * 60 * 1000;
static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000;
static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000;
// User confirmation timeout for a full backup/restore operation
@ -1691,7 +1692,7 @@ class BackupManagerService extends IBackupManager.Stub {
public void run() {
final List<PackageInfo> packagesToBackup;
Slog.i(TAG, "--- Performing full-dataset restore ---");
Slog.i(TAG, "--- Performing full-dataset backup ---");
sendStartBackup();
// doAllApps supersedes the package set if any
@ -1720,64 +1721,23 @@ class BackupManagerService extends IBackupManager.Stub {
}
}
// Now back up the app data via the agent mechanism
PackageInfo pkg = null;
try {
// Now back up the app data via the agent mechanism
int N = packagesToBackup.size();
for (int i = 0; i < N; i++) {
pkg = packagesToBackup.get(i);
backupOnePackage(pkg);
}
Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName);
IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo,
IApplicationThread.BACKUP_MODE_FULL);
if (agent != null) {
try {
ApplicationInfo app = pkg.applicationInfo;
boolean sendApk = mIncludeApks
&& ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) == 0)
&& ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ||
(app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
sendOnBackupPackage(pkg.packageName);
{
BackupDataOutput output = new BackupDataOutput(
mOutputFile.getFileDescriptor());
if (DEBUG) Slog.d(TAG, "Writing manifest for " + pkg.packageName);
writeAppManifest(pkg, mManifestFile, sendApk);
FullBackup.backupToTar(pkg.packageName, null, null,
mFilesDir.getAbsolutePath(),
mManifestFile.getAbsolutePath(),
output);
}
if (DEBUG) Slog.d(TAG, "Calling doBackup()");
final int token = generateToken();
prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL);
agent.doBackup(null, mOutputFile, null, sendApk,
token, mBackupManagerBinder);
boolean success = waitUntilOperationComplete(token);
if (!success) {
Slog.d(TAG, "Full backup failed on package " + pkg.packageName);
} else {
if (DEBUG) Slog.d(TAG, "Full backup success: " + pkg.packageName);
}
} catch (IOException e) {
Slog.e(TAG, "Error backing up " + pkg.packageName, e);
}
} else {
Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName);
}
tearDown(pkg);
// Finally, shared storage if requested
if (mIncludeShared) {
backupSharedStorage();
}
} catch (RemoteException e) {
Slog.e(TAG, "App died during full backup");
} finally {
if (pkg != null) {
tearDown(pkg);
}
tearDown(pkg);
try {
mOutputFile.close();
} catch (IOException e) {
@ -1796,6 +1756,79 @@ class BackupManagerService extends IBackupManager.Stub {
}
}
private void backupOnePackage(PackageInfo pkg) throws RemoteException {
Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName);
IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo,
IApplicationThread.BACKUP_MODE_FULL);
if (agent != null) {
try {
ApplicationInfo app = pkg.applicationInfo;
boolean sendApk = mIncludeApks
&& ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) == 0)
&& ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ||
(app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
sendOnBackupPackage(pkg.packageName);
{
BackupDataOutput output = new BackupDataOutput(
mOutputFile.getFileDescriptor());
if (DEBUG) Slog.d(TAG, "Writing manifest for " + pkg.packageName);
writeAppManifest(pkg, mManifestFile, sendApk);
FullBackup.backupToTar(pkg.packageName, null, null,
mFilesDir.getAbsolutePath(),
mManifestFile.getAbsolutePath(),
output);
}
if (DEBUG) Slog.d(TAG, "Calling doBackup()");
final int token = generateToken();
prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL);
agent.doBackup(null, mOutputFile, null, sendApk,
token, mBackupManagerBinder);
if (!waitUntilOperationComplete(token)) {
Slog.e(TAG, "Full backup failed on package " + pkg.packageName);
} else {
if (DEBUG) Slog.d(TAG, "Full backup success: " + pkg.packageName);
}
} catch (IOException e) {
Slog.e(TAG, "Error backing up " + pkg.packageName, e);
}
} else {
Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName);
}
tearDown(pkg);
}
private void backupSharedStorage() throws RemoteException {
PackageInfo pkg = null;
try {
pkg = mPackageManager.getPackageInfo("com.android.sharedstoragebackup", 0);
IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo,
IApplicationThread.BACKUP_MODE_FULL);
if (agent != null) {
sendOnBackupPackage("Shared storage");
final int token = generateToken();
prepareOperationTimeout(token, TIMEOUT_SHARED_BACKUP_INTERVAL);
agent.doBackup(null, mOutputFile, null, false, token, mBackupManagerBinder);
if (!waitUntilOperationComplete(token)) {
Slog.e(TAG, "Full backup failed on shared storage");
} else {
if (DEBUG) Slog.d(TAG, "Full shared storage backup success");
}
} else {
Slog.e(TAG, "Could not bind to shared storage backup agent");
}
} catch (NameNotFoundException e) {
Slog.e(TAG, "Shared storage backup package not found");
} finally {
tearDown(pkg);
}
}
private void writeAppManifest(PackageInfo pkg, File manifestFile, boolean withApk)
throws IOException {
// Manifest format. All data are strings ending in LF:
@ -1836,23 +1869,24 @@ class BackupManagerService extends IBackupManager.Stub {
}
private void tearDown(PackageInfo pkg) {
final ApplicationInfo app = pkg.applicationInfo;
try {
// unbind and tidy up even on timeout or failure, just in case
mActivityManager.unbindBackupAgent(app);
if (pkg != null) {
final ApplicationInfo app = pkg.applicationInfo;
if (app != null) {
try {
// unbind and tidy up even on timeout or failure, just in case
mActivityManager.unbindBackupAgent(app);
// The agent was running with a stub Application object, so shut it down.
// !!! We hardcode the confirmation UI's package name here rather than use a
// manifest flag! TODO something less direct.
if (app.uid != Process.SYSTEM_UID
&& !pkg.packageName.equals("com.android.backupconfirm")) {
if (DEBUG) Slog.d(TAG, "Backup complete, killing host process");
mActivityManager.killApplicationProcess(app.processName, app.uid);
} else {
if (DEBUG) Slog.d(TAG, "Not killing after restore: " + app.processName);
// The agent was running with a stub Application object, so shut it down.
if (app.uid != Process.SYSTEM_UID) {
if (DEBUG) Slog.d(TAG, "Backup complete, killing host process");
mActivityManager.killApplicationProcess(app.processName, app.uid);
} else {
if (DEBUG) Slog.d(TAG, "Not killing after restore: " + app.processName);
}
} catch (RemoteException e) {
Slog.d(TAG, "Lost app trying to shut down");
}
}
} catch (RemoteException e) {
Slog.d(TAG, "Lost app trying to shut down");
}
}
@ -1949,6 +1983,7 @@ class BackupManagerService extends IBackupManager.Stub {
// with a whitelist of packages known to be unclearable.
mClearedPackages.add("android");
mClearedPackages.add("com.android.providers.settings");
}
class RestoreFileRunnable implements Runnable {
@ -1988,6 +2023,11 @@ class BackupManagerService extends IBackupManager.Stub {
Slog.i(TAG, "--- Performing full-dataset restore ---");
sendStartRestore();
// Are we able to restore shared-storage data?
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
mPackagePolicies.put("com.android.sharedstoragebackup", RestorePolicy.ACCEPT);
}
try {
byte[] buffer = new byte[32 * 1024];
FileInputStream instream = new FileInputStream(mInputFile.getFileDescriptor());
@ -2707,7 +2747,9 @@ class BackupManagerService extends IBackupManager.Stub {
info.path, 0, FullBackup.SHARED_PREFIX.length())) {
// File in shared storage. !!! TODO: implement this.
info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
info.packageName = "com.android.sharedstoragebackup";
info.domain = FullBackup.SHARED_STORAGE_TOKEN;
if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path);
} else if (FullBackup.APPS_PREFIX.regionMatches(0,
info.path, 0, FullBackup.APPS_PREFIX.length())) {
// App content! Parse out the package name and domain

View File

@ -138,7 +138,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
if (outFile == null) {
Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]");
}
FullBackup.restoreToFile(data, size, type, mode, mtime, outFile);
FullBackup.restoreToFile(data, size, type, mode, mtime, outFile, true);
if (restoredWallpaper) {
WallpaperManagerService wallpaper =