diff options
author | Christopher Tate <ctate@google.com> | 2011-05-18 16:28:19 -0700 |
---|---|---|
committer | Christopher Tate <ctate@google.com> | 2011-06-01 15:09:55 -0700 |
commit | 75a99709accef8cf221fd436d646727e7c8dd1f1 (patch) | |
tree | 9ce16dbf95890e8dad57d63724a6cdb3d36d6fb9 | |
parent | 2978cef0a77550ea3a364ffbf42fc43f2029070e (diff) | |
download | frameworks_base-75a99709accef8cf221fd436d646727e7c8dd1f1.zip frameworks_base-75a99709accef8cf221fd436d646727e7c8dd1f1.tar.gz frameworks_base-75a99709accef8cf221fd436d646727e7c8dd1f1.tar.bz2 |
Restore from a previous full backup's tarfile
Usage: adb restore [tarfilename]
Restores app data [and installs the apps if necessary from the backup
file] captured in a previous invocation of 'adb backup'. The user
must explicitly acknowledge the action on-device before it is allowed
to proceed; this prevents any "invisible" pushes of content from the
host to the device.
Known issues:
* The settings databases and wallpaper are saved/restored, but lots
of other system state is not yet captured in the full backup. This
means that for practical purposes this is usable for 3rd party
apps at present but not for full-system cloning/imaging.
Change-Id: I0c748b645845e7c9178e30bf142857861a64efd3
16 files changed, 1416 insertions, 94 deletions
diff --git a/cmds/bu/src/com/android/commands/bu/Backup.java b/cmds/bu/src/com/android/commands/bu/Backup.java index f3f0432..e81f799 100644 --- a/cmds/bu/src/com/android/commands/bu/Backup.java +++ b/cmds/bu/src/com/android/commands/bu/Backup.java @@ -34,11 +34,12 @@ public final class Backup { IBackupManager mBackupManager; public static void main(String[] args) { + Log.d(TAG, "Beginning: " + args[0]); mArgs = args; try { new Backup().run(); } catch (Exception e) { - Log.e(TAG, "Error running backup", e); + Log.e(TAG, "Error running backup/restore", e); } Log.d(TAG, "Finished."); } @@ -46,7 +47,7 @@ public final class Backup { public void run() { mBackupManager = IBackupManager.Stub.asInterface(ServiceManager.getService("backup")); if (mBackupManager == null) { - System.err.println("ERROR: could not contact backup manager"); + Log.e(TAG, "Can't obtain Backup Manager binder"); return; } @@ -56,7 +57,7 @@ public final class Backup { } else if (arg.equals("restore")) { doFullRestore(); } else { - System.err.println("ERROR: invalid operation '" + arg + "'"); + Log.e(TAG, "Invalid operation '" + arg + "'"); } } @@ -80,7 +81,6 @@ public final class Backup { } else if ("-all".equals(arg)) { doEverything = true; } else { - System.err.println("WARNING: unknown backup flag " + arg); Log.w(TAG, "Unknown backup flag " + arg); continue; } @@ -91,13 +91,10 @@ public final class Backup { } if (doEverything && packages.size() > 0) { - System.err.println("WARNING: -all used with explicit backup package set"); Log.w(TAG, "-all passed for backup along with specific package names"); } if (!doEverything && !saveShared && packages.size() == 0) { - System.err.println( - "ERROR: no packages supplied for backup and neither -shared nor -all given"); Log.e(TAG, "no backup packages supplied and neither -shared nor -all given"); return; } @@ -108,13 +105,22 @@ public final class Backup { mBackupManager.fullBackup(fd, saveApks, saveShared, doEverything, packages.toArray(packArray)); } catch (IOException e) { - System.err.println("ERROR: cannot dup System.out"); + Log.e(TAG, "Can't dup out"); } catch (RemoteException e) { - System.err.println("ERROR: unable to invoke backup manager service"); + Log.e(TAG, "Unable to invoke backup manager for backup"); } } private void doFullRestore() { + // No arguments to restore + try { + ParcelFileDescriptor fd = ParcelFileDescriptor.dup(FileDescriptor.in); + mBackupManager.fullRestore(fd); + } catch (IOException e) { + Log.e(TAG, "Can't dup System.in"); + } catch (RemoteException e) { + Log.e(TAG, "Unable to invoke backup manager for restore"); + } } private String nextArg() { diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 85e59b3..955cef2 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1980,7 +1980,8 @@ public final class ActivityThread { BackupAgent agent = null; String classname = data.appInfo.backupAgentName; - if (data.backupMode == IApplicationThread.BACKUP_MODE_FULL) { + if (data.backupMode == IApplicationThread.BACKUP_MODE_FULL + || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL) { classname = "android.app.backup.FullBackupAgent"; if ((data.appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { // system packages can supply their own full-backup agent @@ -2011,7 +2012,8 @@ public final class ActivityThread { // If this is during restore, fail silently; otherwise go // ahead and let the user see the crash. Slog.e(TAG, "Agent threw during creation: " + e); - if (data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE) { + if (data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE + && data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE_FULL) { throw e; } // falling through with 'binder' still null @@ -3658,12 +3660,16 @@ public final class ActivityThread { Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; - List<ProviderInfo> providers = data.providers; - if (providers != null) { - installContentProviders(app, providers); - // For process that contains content providers, we want to - // ensure that the JIT is enabled "at some point". - mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000); + // don't bring up providers in restricted mode; they may depend on the + // app's custom Application class + if (!data.restrictedBackupMode){ + List<ProviderInfo> providers = data.providers; + if (providers != null) { + installContentProviders(app, providers); + // For process that contains content providers, we want to + // ensure that the JIT is enabled "at some point". + mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000); + } } try { diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 8c31559..05a68a8 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -68,6 +68,7 @@ public interface IApplicationThread extends IInterface { static final int BACKUP_MODE_INCREMENTAL = 0; static final int BACKUP_MODE_FULL = 1; static final int BACKUP_MODE_RESTORE = 2; + static final int BACKUP_MODE_RESTORE_FULL = 3; void scheduleCreateBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo, int backupMode) throws RemoteException; void scheduleDestroyBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo) diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index 52fc623..8af78fa 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -79,4 +79,23 @@ oneway interface IBackupAgent { */ void doRestore(in ParcelFileDescriptor data, int appVersionCode, in ParcelFileDescriptor newState, int token, IBackupManager callbackBinder); + + /** + * Restore a single "file" to the application. The file was typically obtained from + * a full-backup dataset. The agent reads 'size' bytes of file content + * from the provided file descriptor. + * + * @param data Read-only pipe delivering the file content itself. + * + * @param size Size of the file being restored. + * @param type Type of file system entity, e.g. FullBackup.TYPE_DIRECTORY. + * @param domain Name of the file's semantic domain to which the 'path' argument is a + * relative path. e.g. FullBackup.DATABASE_TREE_TOKEN. + * @param path Relative path of the file within its semantic domain. + * @param mode Access mode of the file system entity, e.g. 0660. + * @param mtime Last modification time of the file system entity. + */ + void doRestoreFile(in ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime, + int token, IBackupManager callbackBinder); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index dc60e24..17f8adb 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -179,10 +179,18 @@ public abstract class BackupAgent extends ContextWrapper { throws IOException; /** + * @hide + */ + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + // empty stub implementation + } + + /** * Package-private, used only for dispatching an extra step during full backup */ void onSaveApk(BackupDataOutput data) { - if (DEBUG) Log.v(TAG, "--- base onSaveApk() ---"); } // ----- Core implementation ----- @@ -203,6 +211,7 @@ public abstract class BackupAgent extends ContextWrapper { private class BackupServiceBinder extends IBackupAgent.Stub { private static final String TAG = "BackupServiceBinder"; + @Override public void doBackup(ParcelFileDescriptor oldState, ParcelFileDescriptor data, ParcelFileDescriptor newState, @@ -236,6 +245,7 @@ public abstract class BackupAgent extends ContextWrapper { } } + @Override public void doRestore(ParcelFileDescriptor data, int appVersionCode, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder) throws RemoteException { @@ -261,5 +271,25 @@ public abstract class BackupAgent extends ContextWrapper { } } } + + @Override + public void doRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime, + 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); + } finally { + Binder.restoreCallingIdentity(ident); + try { + callbackBinder.opComplete(token); + } catch (RemoteException e) { + // we'll time out anyway, so we're safe + } + } + } } } diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index 9850566..dfb0dd7 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -16,6 +16,17 @@ package android.app.backup; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import libcore.io.ErrnoException; +import libcore.io.Libcore; + /** * Global constant definitions et cetera related to the full-backup-to-fd * binary format. @@ -23,18 +34,95 @@ package android.app.backup; * @hide */ public class FullBackup { - public static String APK_TREE_TOKEN = "a"; - public static String OBB_TREE_TOKEN = "obb"; - public static String ROOT_TREE_TOKEN = "r"; - public static String DATA_TREE_TOKEN = "f"; - public static String DATABASE_TREE_TOKEN = "db"; - public static String SHAREDPREFS_TREE_TOKEN = "sp"; - public static String CACHE_TREE_TOKEN = "c"; - - public static String FULL_BACKUP_INTENT_ACTION = "fullback"; - public static String FULL_RESTORE_INTENT_ACTION = "fullrest"; - public static String CONF_TOKEN_INTENT_EXTRA = "conftoken"; + static final String TAG = "FullBackup"; + + public static final String APK_TREE_TOKEN = "a"; + public static final String OBB_TREE_TOKEN = "obb"; + public static final String ROOT_TREE_TOKEN = "r"; + public static final String DATA_TREE_TOKEN = "f"; + public static final String DATABASE_TREE_TOKEN = "db"; + public static final String SHAREDPREFS_TREE_TOKEN = "sp"; + public static final String CACHE_TREE_TOKEN = "c"; + 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 FULL_BACKUP_INTENT_ACTION = "fullback"; + public static final String FULL_RESTORE_INTENT_ACTION = "fullrest"; + public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken"; + + public static final int TYPE_EOF = 0; + public static final int TYPE_FILE = 1; + public static final int TYPE_DIRECTORY = 2; + public static final int TYPE_SYMLINK = 3; static public native int backupToTar(String packageName, String domain, 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 { + 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 + // drop down to the final metadata adjustment. + if (outFile != null) outFile.mkdirs(); + } else { + FileOutputStream out = null; + + // Pull the data from the pipe, copying it to the output file, until we're done + try { + if (outFile != null) { + File parent = outFile.getParentFile(); + if (!parent.exists()) { + // in practice this will only be for the default semantic directories, + // and using the default mode for those is appropriate. + // TODO: support the edge case of apps that have adjusted the + // permissions on these core directories + parent.mkdirs(); + } + out = new FileOutputStream(outFile); + } + } catch (IOException e) { + Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e); + } + + byte[] buffer = new byte[32 * 1024]; + final long origSize = size; + FileInputStream in = new FileInputStream(data.getFileDescriptor()); + while (size > 0) { + int toRead = (size > buffer.length) ? buffer.length : (int)size; + int got = in.read(buffer, 0, toRead); + if (got <= 0) { + Log.w(TAG, "Incomplete read: expected " + size + " but got " + + (origSize - size)); + break; + } + if (out != null) { + try { + out.write(buffer, 0, got); + } catch (IOException e) { + // Problem writing to the file. Quit copying data and delete + // the file, but of course keep consuming the input stream. + Log.e(TAG, "Unable to write to file " + outFile.getPath(), e); + out.close(); + out = null; + outFile.delete(); + } + } + size -= got; + } + if (out != null) out.close(); + } + + // Now twiddle the state to match the backup, assuming all went well + if (outFile != null) { + try { + Libcore.os.chmod(outFile.getPath(), (int)mode); + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } + outFile.setLastModified(mtime); + } + } } diff --git a/core/java/android/app/backup/FullBackupAgent.java b/core/java/android/app/backup/FullBackupAgent.java index f0a1f2a..4dca593 100644 --- a/core/java/android/app/backup/FullBackupAgent.java +++ b/core/java/android/app/backup/FullBackupAgent.java @@ -28,6 +28,9 @@ 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; @@ -53,8 +56,12 @@ public class FullBackupAgent extends BackupAgent { private String mCacheDir; private String mLibDir; + private File NULL_FILE; + @Override public void onCreate() { + NULL_FILE = new File("/dev/null"); + mPm = getPackageManager(); try { ApplicationInfo appInfo = mPm.getApplicationInfo(getPackageName(), 0); @@ -177,7 +184,40 @@ public class FullBackupAgent extends BackupAgent { } } + /** + * Dummy -- We're never used for restore of an incremental dataset + */ + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + } + + /** + * Restore the described file from the given pipe. + */ @Override - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) { + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String relpath, long mode, long mtime) + throws IOException { + String basePath = null; + File outFile = null; + + if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type + + " domain=" + domain + " relpath=" + relpath + " mode=" + mode + + " mtime=" + mtime); + + // Parse out the semantic domains into the correct physical location + if (domain.equals(FullBackup.DATA_TREE_TOKEN)) basePath = mFilesDir; + else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) basePath = mDatabaseDir; + else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) basePath = mMainDir; + else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) basePath = mSharedPrefsDir; + + // Not a supported output location? We need to consume the data + // anyway, so send it to /dev/null + outFile = (basePath != null) ? new File(basePath, relpath) : null; + 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); } } diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 94e31a8..bac874e 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -147,6 +147,14 @@ interface IBackupManager { boolean allApps, in String[] packageNames); /** + * Restore device content from the data stream passed through the given socket. The + * data stream must be in the format emitted by fullBackup(). + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void fullRestore(in ParcelFileDescriptor fd); + + /** * Confirm that the requested full backup/restore operation can proceed. The system will * not actually perform the operation described to fullBackup() / fullRestore() unless the * UI calls back into the Backup Manager to confirm, passing the correct token. At diff --git a/libs/utils/BackupHelpers.cpp b/libs/utils/BackupHelpers.cpp index e15875f..f933199 100644 --- a/libs/utils/BackupHelpers.cpp +++ b/libs/utils/BackupHelpers.cpp @@ -503,6 +503,16 @@ int write_tarfile(const String8& packageName, const String8& domain, needExtended = true; } + // Non-7bit-clean path also means needing pax extended format + if (!needExtended) { + for (size_t i = 0; i < filepath.length(); i++) { + if ((filepath[i] & 0x80) != 0) { + needExtended = true; + break; + } + } + } + int err = 0; struct stat64 s; if (lstat64(filepath.string(), &s) != 0) { diff --git a/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java b/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java index 4b42067..52bfc28 100644 --- a/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java +++ b/packages/BackupRestoreConfirmation/src/com/android/backupconfirm/BackupRestoreConfirmation.java @@ -98,6 +98,8 @@ public class BackupRestoreConfirmation extends Activity { break; case MSG_RESTORE_PACKAGE: { + String name = (String) msg.obj; + mStatusView.setText(name); } break; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index 45bb2b6..0c4ef7d 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -55,6 +55,7 @@ import android.util.Log; */ public class SettingsBackupAgent extends BackupAgentHelper { private static final boolean DEBUG = false; + private static final boolean DEBUG_BACKUP = DEBUG || true; private static final String KEY_SYSTEM = "system"; private static final String KEY_SECURE = "secure"; @@ -75,6 +76,9 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final int STATE_WIFI_CONFIG = 4; private static final int STATE_SIZE = 5; // The number of state items + // Versioning of the 'full backup' format + private static final int FULL_BACKUP_VERSION = 1; + private static String[] sortedSystemKeys = null; private static String[] sortedSecureKeys = null; @@ -109,6 +113,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static String mWifiConfigFile; public void onCreate() { + if (DEBUG_BACKUP) Log.d(TAG, "onCreate() invoked"); + mSettingsHelper = new SettingsHelper(this); super.onCreate(); @@ -151,26 +157,32 @@ public class SettingsBackupAgent extends BackupAgentHelper { // representation of the backed-up settings. String root = getFilesDir().getAbsolutePath(); File stage = new File(root, STAGE_FILE); - FileOutputStream filestream = new FileOutputStream(stage); - BufferedOutputStream bufstream = new BufferedOutputStream(filestream); - DataOutputStream out = new DataOutputStream(bufstream); - - out.writeInt(systemSettingsData.length); - out.write(systemSettingsData); - out.writeInt(secureSettingsData.length); - out.write(secureSettingsData); - out.writeInt(locale.length); - out.write(locale); - out.writeInt(wifiSupplicantData.length); - out.write(wifiSupplicantData); - out.writeInt(wifiConfigData.length); - out.write(wifiConfigData); - - out.flush(); // also flushes downstream - - // now we're set to emit the tar stream - FullBackup.backupToTar(getPackageName(), FullBackup.DATA_TREE_TOKEN, null, - root, stage.getAbsolutePath(), data); + try { + FileOutputStream filestream = new FileOutputStream(stage); + BufferedOutputStream bufstream = new BufferedOutputStream(filestream); + DataOutputStream out = new DataOutputStream(bufstream); + + out.writeInt(FULL_BACKUP_VERSION); + + out.writeInt(systemSettingsData.length); + out.write(systemSettingsData); + out.writeInt(secureSettingsData.length); + out.write(secureSettingsData); + out.writeInt(locale.length); + out.write(locale); + out.writeInt(wifiSupplicantData.length); + out.write(wifiSupplicantData); + out.writeInt(wifiConfigData.length); + out.write(wifiConfigData); + + out.flush(); // also flushes downstream + + // now we're set to emit the tar stream + FullBackup.backupToTar(getPackageName(), FullBackup.DATA_TREE_TOKEN, null, + root, stage.getAbsolutePath(), data); + } finally { + stage.delete(); + } } } @@ -199,7 +211,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { } else if (KEY_LOCALE.equals(key)) { byte[] localeData = new byte[size]; data.readEntityData(localeData, 0, size); - mSettingsHelper.setLocaleData(localeData); + mSettingsHelper.setLocaleData(localeData, size); } else if (KEY_WIFI_CONFIG.equals(key)) { restoreFileData(mWifiConfigFile, data); } else { @@ -208,6 +220,70 @@ public class SettingsBackupAgent extends BackupAgentHelper { } } + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String relpath, long mode, long mtime) + throws IOException { + if (DEBUG_BACKUP) Log.d(TAG, "onRestoreFile() invoked"); + // Our data is actually a blob of flattened settings data identical to that + // produced during incremental backups. Just unpack and apply it all in + // turn. + FileInputStream instream = new FileInputStream(data.getFileDescriptor()); + DataInputStream in = new DataInputStream(instream); + + int version = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, "Flattened data version " + version); + if (version == FULL_BACKUP_VERSION) { + // system settings data first + int nBytes = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of settings data"); + byte[] buffer = new byte[nBytes]; + in.read(buffer, 0, nBytes); + restoreSettings(buffer, nBytes, Settings.System.CONTENT_URI); + + // secure settings + nBytes = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of secure settings data"); + if (nBytes > buffer.length) buffer = new byte[nBytes]; + in.read(buffer, 0, nBytes); + restoreSettings(buffer, nBytes, Settings.Secure.CONTENT_URI); + + // locale + nBytes = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of locale data"); + if (nBytes > buffer.length) buffer = new byte[nBytes]; + in.read(buffer, 0, nBytes); + mSettingsHelper.setLocaleData(buffer, nBytes); + + // wifi supplicant + nBytes = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of wifi supplicant data"); + if (nBytes > buffer.length) buffer = new byte[nBytes]; + in.read(buffer, 0, nBytes); + int retainedWifiState = enableWifi(false); + restoreWifiSupplicant(FILE_WIFI_SUPPLICANT, buffer, nBytes); + FileUtils.setPermissions(FILE_WIFI_SUPPLICANT, + FileUtils.S_IRUSR | FileUtils.S_IWUSR | + FileUtils.S_IRGRP | FileUtils.S_IWGRP, + Process.myUid(), Process.WIFI_UID); + // retain the previous WIFI state. + enableWifi(retainedWifiState == WifiManager.WIFI_STATE_ENABLED || + retainedWifiState == WifiManager.WIFI_STATE_ENABLING); + + // wifi config + nBytes = in.readInt(); + if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of wifi config data"); + if (nBytes > buffer.length) buffer = new byte[nBytes]; + in.read(buffer, 0, nBytes); + restoreFileData(mWifiConfigFile, buffer, nBytes); + + if (DEBUG_BACKUP) Log.d(TAG, "Full restore complete."); + } else { + data.close(); + throw new IOException("Invalid file schema"); + } + } + private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException { long[] stateChecksums = new long[STATE_SIZE]; @@ -287,6 +363,17 @@ public class SettingsBackupAgent extends BackupAgentHelper { } private void restoreSettings(BackupDataInput data, Uri contentUri) { + byte[] settings = new byte[data.getDataSize()]; + try { + data.readEntityData(settings, 0, settings.length); + } catch (IOException ioe) { + Log.e(TAG, "Couldn't read entity data"); + return; + } + restoreSettings(settings, settings.length, contentUri); + } + + private void restoreSettings(byte[] settings, int bytes, Uri contentUri) { if (DEBUG) Log.i(TAG, "restoreSettings: " + contentUri); String[] whitelist = null; if (contentUri.equals(Settings.Secure.CONTENT_URI)) { @@ -296,15 +383,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { } ContentValues cv = new ContentValues(2); - byte[] settings = new byte[data.getDataSize()]; - try { - data.readEntityData(settings, 0, settings.length); - } catch (IOException ioe) { - Log.e(TAG, "Couldn't read entity data"); - return; - } int pos = 0; - while (pos < settings.length) { + while (pos < bytes) { int length = readInt(settings, pos); pos += 4; String settingName = length > 0? new String(settings, pos, length) : null; @@ -451,13 +531,16 @@ public class SettingsBackupAgent extends BackupAgentHelper { private void restoreFileData(String filename, BackupDataInput data) { byte[] bytes = new byte[data.getDataSize()]; if (bytes.length <= 0) return; + restoreFileData(filename, bytes, bytes.length); + } + + private void restoreFileData(String filename, byte[] bytes, int size) { try { - data.readEntityData(bytes, 0, bytes.length); File file = new File(filename); if (file.exists()) file.delete(); OutputStream os = new BufferedOutputStream(new FileOutputStream(filename, true)); - os.write(bytes); + os.write(bytes, 0, size); os.close(); } catch (IOException ioe) { Log.w(TAG, "Couldn't restore " + filename); @@ -506,15 +589,18 @@ public class SettingsBackupAgent extends BackupAgentHelper { private void restoreWifiSupplicant(String filename, BackupDataInput data) { byte[] bytes = new byte[data.getDataSize()]; if (bytes.length <= 0) return; + restoreWifiSupplicant(filename, bytes, bytes.length); + } + + private void restoreWifiSupplicant(String filename, byte[] bytes, int size) { try { - data.readEntityData(bytes, 0, bytes.length); File supplicantFile = new File(FILE_WIFI_SUPPLICANT); if (supplicantFile.exists()) supplicantFile.delete(); copyWifiSupplicantTemplate(); OutputStream os = new BufferedOutputStream(new FileOutputStream(filename, true)); os.write("\n".getBytes()); - os.write(bytes); + os.write(bytes, 0, size); os.close(); } catch (IOException ioe) { Log.w(TAG, "Couldn't restore " + filename); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 0e75fbc..3e7d86a 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -147,7 +147,7 @@ public class SettingsHelper { * "ll" is the language code and "cc" is the country code. * @param data the locale string in bytes. */ - void setLocaleData(byte[] data) { + void setLocaleData(byte[] data, int size) { // Check if locale was set by the user: Configuration conf = mContext.getResources().getConfiguration(); Locale loc = conf.locale; @@ -157,9 +157,9 @@ public class SettingsHelper { if (conf.userSetLocale) return; // Don't change if user set it in the SetupWizard final String[] availableLocales = mContext.getAssets().getLocales(); - String localeCode = new String(data); + String localeCode = new String(data, 0, size); String language = new String(data, 0, 2); - String country = data.length > 4 ? new String(data, 3, 2) : ""; + String country = size > 4 ? new String(data, 3, 2) : ""; loc = null; for (int i = 0; i < availableLocales.length; i++) { if (availableLocales[i].equals(localeCode)) { diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index 455b4c2..cd58b9b 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -39,6 +39,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -46,6 +47,7 @@ import android.content.pm.Signature; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -74,12 +76,16 @@ import com.android.server.PackageManagerBackupAgent.Metadata; import java.io.EOFException; import java.io.File; import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.io.RandomAccessFile; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -398,6 +404,13 @@ class BackupManagerService extends IBackupManager.Stub { break; } + case MSG_RUN_FULL_RESTORE: + { + FullRestoreParams params = (FullRestoreParams)msg.obj; + (new PerformFullRestoreTask(params.fd, params.observer, params.latch)).run(); + break; + } + case MSG_RUN_CLEAR: { ClearParams params = (ClearParams)msg.obj; @@ -1236,7 +1249,7 @@ class BackupManagerService extends IBackupManager.Stub { Slog.d(TAG, "awaiting agent for " + app); // success; wait for the agent to arrive - // only wait 10 seconds for the clear data to happen + // only wait 10 seconds for the bind to happen long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; while (mConnecting && mConnectedAgent == null && (System.currentTimeMillis() < timeoutMark)) { @@ -1677,6 +1690,7 @@ class BackupManagerService extends IBackupManager.Stub { public void run() { final List<PackageInfo> packagesToBackup; + Slog.i(TAG, "--- Performing full-dataset restore ---"); sendStartBackup(); // doAllApps supersedes the package set if any @@ -1779,7 +1793,9 @@ class BackupManagerService extends IBackupManager.Stub { // Version 1: // package name // package's versionCode - // boolean: "1" if archive includes .apk, "0" otherwise + // 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); @@ -1788,6 +1804,11 @@ class BackupManagerService extends IBackupManager.Stub { printer.println(Integer.toString(BACKUP_MANIFEST_VERSION)); printer.println(pkg.packageName); printer.println(Integer.toString(pkg.versionCode)); + printer.println(Integer.toString(Build.VERSION.SDK_INT)); + + String installerName = mPackageManager.getInstallerPackageName(pkg.packageName); + printer.println((installerName != null) ? installerName : ""); + printer.println(withApk ? "1" : "0"); if (pkg.signatures == null) { printer.println("0"); @@ -1861,6 +1882,896 @@ class BackupManagerService extends IBackupManager.Stub { } + // ----- Full restore from a file/socket ----- + + // Description of a file in the restore datastream + static class FileMetadata { + String packageName; // name of the owning app + String installerPackageName; // name of the market-type app that installed the owner + int type; // e.g. FullBackup.TYPE_DIRECTORY + String domain; // e.g. FullBackup.DATABASE_TREE_TOKEN + String path; // subpath within the semantic domain + long mode; // e.g. 0666 (actually int) + long mtime; // last mod time, UTC time_t (actually int) + long size; // bytes of content + } + + enum RestorePolicy { + IGNORE, + ACCEPT, + ACCEPT_IF_APK + } + + class PerformFullRestoreTask implements Runnable { + ParcelFileDescriptor mInputFile; + IFullBackupRestoreObserver mObserver; + AtomicBoolean mLatchObject; + IBackupAgent mAgent; + String mAgentPackage; + ApplicationInfo mTargetApp; + ParcelFileDescriptor[] mPipes = null; + + // possible handling states for a given package in the restore dataset + final HashMap<String, RestorePolicy> mPackagePolicies + = new HashMap<String, RestorePolicy>(); + + // installer package names for each encountered app, derived from the manifests + final HashMap<String, String> mPackageInstallers = new HashMap<String, String>(); + + // Signatures for a given package found in its manifest file + final HashMap<String, Signature[]> mManifestSignatures + = new HashMap<String, Signature[]>(); + + // Packages we've already wiped data on when restoring their first file + final HashSet<String> mClearedPackages = new HashSet<String>(); + + PerformFullRestoreTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer, + AtomicBoolean latch) { + mInputFile = fd; + mObserver = observer; + mLatchObject = latch; + mAgent = null; + mAgentPackage = null; + mTargetApp = null; + + // Which packages we've already wiped data on. We prepopulate this + // with a whitelist of packages known to be unclearable. + mClearedPackages.add("android"); + mClearedPackages.add("com.android.backupconfirm"); + mClearedPackages.add("com.android.providers.settings"); + } + + class RestoreFileRunnable implements Runnable { + IBackupAgent mAgent; + FileMetadata mInfo; + ParcelFileDescriptor mSocket; + int mToken; + + RestoreFileRunnable(IBackupAgent agent, FileMetadata info, + ParcelFileDescriptor socket, int token) throws IOException { + mAgent = agent; + mInfo = info; + mToken = token; + + // This class is used strictly for process-local binder invocations. The + // semantics of ParcelFileDescriptor differ in this case; in particular, we + // do not automatically get a 'dup'ed descriptor that we can can continue + // to use asynchronously from the caller. So, we make sure to dup it ourselves + // before proceeding to do the restore. + mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); + } + + @Override + public void run() { + try { + mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type, + mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime, + mToken, mBackupManagerBinder); + } catch (RemoteException e) { + // never happens; this is used strictly for local binder calls + } + } + } + + @Override + public void run() { + Slog.i(TAG, "--- Performing full-dataset restore ---"); + sendStartRestore(); + + try { + byte[] buffer = new byte[32 * 1024]; + FileInputStream instream = new FileInputStream(mInputFile.getFileDescriptor()); + + boolean didRestore; + do { + didRestore = restoreOneFile(instream, buffer); + } while (didRestore); + + if (DEBUG) Slog.v(TAG, "Done consuming input tarfile"); + } finally { + tearDownPipes(); + tearDownAgent(mTargetApp); + + try { + mInputFile.close(); + } catch (IOException e) { + /* nothing we can do about this */ + } + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + synchronized (mLatchObject) { + mLatchObject.set(true); + mLatchObject.notifyAll(); + } + sendEndRestore(); + mWakelock.release(); + if (DEBUG) Slog.d(TAG, "Full restore pass complete."); + } + } + + boolean restoreOneFile(InputStream instream, byte[] buffer) { + FileMetadata info; + try { + info = readTarHeaders(instream); + if (info != null) { + if (DEBUG) { + dumpFileMetadata(info); + } + + final String pkg = info.packageName; + if (!pkg.equals(mAgentPackage)) { + // okay, change in package; set up our various + // bookkeeping if we haven't seen it yet + if (!mPackagePolicies.containsKey(pkg)) { + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + + // Clean up the previous agent relationship if necessary, + // and let the observer know we're considering a new app. + if (mAgent != null) { + if (DEBUG) Slog.d(TAG, "Saw new package; tearing down old one"); + tearDownPipes(); + tearDownAgent(mTargetApp); + mTargetApp = null; + mAgentPackage = null; + } + } + + if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { + mPackagePolicies.put(pkg, readAppManifest(info, instream)); + mPackageInstallers.put(pkg, info.installerPackageName); + // We've read only the manifest content itself at this point, + // so consume the footer before looping around to the next + // input file + skipTarPadding(info.size, instream); + sendOnRestorePackage(pkg); + } else { + // Non-manifest, so it's actual file data. Is this a package + // we're ignoring? + boolean okay = true; + RestorePolicy policy = mPackagePolicies.get(pkg); + switch (policy) { + case IGNORE: + okay = false; + break; + + case ACCEPT_IF_APK: + // If we're in accept-if-apk state, then the first file we + // see MUST be the apk. + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "APK file; installing"); + // Try to install the app. + String installerName = mPackageInstallers.get(pkg); + okay = installApk(info, installerName, instream); + // good to go; promote to ACCEPT + mPackagePolicies.put(pkg, (okay) + ? RestorePolicy.ACCEPT + : RestorePolicy.IGNORE); + // At this point we've consumed this file entry + // ourselves, so just strip the tar footer and + // go on to the next file in the input stream + skipTarPadding(info.size, instream); + return true; + } else { + // File data before (or without) the apk. We can't + // handle it coherently in this case so ignore it. + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + okay = false; + } + break; + + case ACCEPT: + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "apk present but ACCEPT"); + // we can take the data without the apk, so we + // *want* to do so. skip the apk by declaring this + // one file not-okay without changing the restore + // policy for the package. + okay = false; + } + break; + + default: + // Something has gone dreadfully wrong when determining + // the restore policy from the manifest. Ignore the + // rest of this package's data. + Slog.e(TAG, "Invalid policy from manifest"); + okay = false; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + break; + } + + // If the policy is satisfied, go ahead and set up to pipe the + // data to the agent. + if (DEBUG && okay && mAgent != null) { + Slog.i(TAG, "Reusing existing agent instance"); + } + if (okay && mAgent == null) { + if (DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg); + + try { + mTargetApp = mPackageManager.getApplicationInfo(pkg, 0); + + // If we haven't sent any data to this app yet, we probably + // need to clear it first. Check that. + if (!mClearedPackages.contains(pkg)) { + // apps with their own full backup agents are + // responsible for coherently managing a full + // restore. + if (mTargetApp.fullBackupAgentName == null) { + if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore"); + clearApplicationDataSynchronous(pkg); + } else { + if (DEBUG) Slog.d(TAG, "full backup agent (" + + mTargetApp.fullBackupAgentName + ") => no clear"); + } + mClearedPackages.add(pkg); + } else { + if (DEBUG) Slog.d(TAG, "We've initialized this app already; no clear required"); + } + + // All set; now set up the IPC and launch the agent + setUpPipes(); + mAgent = bindToAgentSynchronous(mTargetApp, + IApplicationThread.BACKUP_MODE_RESTORE_FULL); + mAgentPackage = pkg; + } catch (IOException e) { + // fall through to error handling + } catch (NameNotFoundException e) { + // fall through to error handling + } + + if (mAgent == null) { + if (DEBUG) Slog.d(TAG, "Unable to create agent for " + pkg); + okay = false; + tearDownPipes(); + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Sanity check: make sure we never give data to the wrong app. This + // should never happen but a little paranoia here won't go amiss. + if (okay && !pkg.equals(mAgentPackage)) { + Slog.e(TAG, "Restoring data for " + pkg + + " but agent is for " + mAgentPackage); + okay = false; + } + + // At this point we have an agent ready to handle the full + // restore data as well as a pipe for sending data to + // that agent. Tell the agent to start reading from the + // pipe. + if (okay) { + boolean agentSuccess = true; + long toCopy = info.size; + final int token = generateToken(); + try { + if (DEBUG) Slog.d(TAG, "Invoking agent to restore file " + + info.path); + prepareOperationTimeout(token, + TIMEOUT_FULL_BACKUP_INTERVAL); + // fire up the app's agent listening on the socket. If + // the agent is running in the system process we can't + // just invoke it asynchronously, so we provide a thread + // for it here. + if (mTargetApp.processName.equals("system")) { + Slog.d(TAG, "system process agent - spinning a thread"); + RestoreFileRunnable runner = new RestoreFileRunnable( + mAgent, info, mPipes[0], token); + new Thread(runner).start(); + } else { + mAgent.doRestoreFile(mPipes[0], info.size, info.type, + info.domain, info.path, info.mode, info.mtime, + token, mBackupManagerBinder); + } + } catch (IOException e) { + // couldn't dup the socket for a process-local restore + Slog.d(TAG, "Couldn't establish restore"); + agentSuccess = false; + okay = false; + } catch (RemoteException e) { + // whoops, remote agent went away. We'll eat the content + // ourselves, then, and not copy it over. + Slog.e(TAG, "Agent crashed during full restore"); + agentSuccess = false; + okay = false; + } + + // Copy over the data if the agent is still good + if (okay) { + boolean pipeOkay = true; + FileOutputStream pipe = new FileOutputStream( + mPipes[1].getFileDescriptor()); + if (DEBUG) Slog.d(TAG, "Piping data to agent"); + while (toCopy > 0) { + int toRead = (toCopy > buffer.length) + ? buffer.length : (int)toCopy; + int nRead = instream.read(buffer, 0, toRead); + if (nRead <= 0) break; + toCopy -= nRead; + + // send it to the output pipe as long as things + // are still good + if (pipeOkay) { + try { + pipe.write(buffer, 0, nRead); + } catch (IOException e) { + Slog.e(TAG, + "Failed to write to restore pipe", e); + pipeOkay = false; + } + } + } + + // done sending that file! Now we just need to consume + // the delta from info.size to the end of block. + skipTarPadding(info.size, instream); + + // and now that we've sent it all, wait for the remote + // side to acknowledge receipt + agentSuccess = waitUntilOperationComplete(token); + } + + // okay, if the remote end failed at any point, deal with + // it by ignoring the rest of the restore on it + if (!agentSuccess) { + mBackupHandler.removeMessages(MSG_TIMEOUT); + tearDownPipes(); + tearDownAgent(mTargetApp); + mAgent = null; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Problems setting up the agent communication, or an already- + // ignored package: skip to the next tar stream entry by + // reading and discarding this file. + if (!okay) { + if (DEBUG) Slog.d(TAG, "[discarding file content]"); + long bytesToConsume = (info.size + 511) & ~511; + while (bytesToConsume > 0) { + int toRead = (bytesToConsume > buffer.length) + ? buffer.length : (int)bytesToConsume; + long nRead = instream.read(buffer, 0, toRead); + if (nRead <= 0) break; + bytesToConsume -= nRead; + } + } + } + } + } catch (IOException e) { + Slog.w(TAG, "io exception on restore socket read", e); + // treat as EOF + info = null; + } + + return (info != null); + } + + void setUpPipes() throws IOException { + mPipes = ParcelFileDescriptor.createPipe(); + } + + void tearDownPipes() { + if (mPipes != null) { + if (mPipes[0] != null) { + try { + mPipes[0].close(); + mPipes[0] = null; + mPipes[1].close(); + mPipes[1] = null; + } catch (IOException e) { + Slog.w(TAG, "Couldn't close agent pipes", e); + } + } + mPipes = null; + } + } + + void tearDownAgent(ApplicationInfo app) { + if (mAgent != 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 + && !app.packageName.equals("com.android.backupconfirm")) { + if (DEBUG) Slog.d(TAG, "Killing host process"); + mActivityManager.killApplicationProcess(app.processName, app.uid); + } else { + if (DEBUG) Slog.d(TAG, "Not killing after full restore"); + } + } catch (RemoteException e) { + Slog.d(TAG, "Lost app trying to shut down"); + } + mAgent = null; + } + } + + class RestoreInstallObserver extends IPackageInstallObserver.Stub { + final AtomicBoolean mDone = new AtomicBoolean(); + int mResult; + + public void reset() { + synchronized (mDone) { + mDone.set(false); + } + } + + public void waitForCompletion() { + synchronized (mDone) { + while (mDone.get() == false) { + try { + mDone.wait(); + } catch (InterruptedException e) { } + } + } + } + + int getResult() { + return mResult; + } + + @Override + public void packageInstalled(String packageName, int returnCode) + throws RemoteException { + synchronized (mDone) { + mResult = returnCode; + mDone.set(true); + mDone.notifyAll(); + } + } + } + final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); + + boolean installApk(FileMetadata info, String installerPackage, InputStream instream) { + boolean okay = true; + + if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName); + + // The file content is an .apk file. Copy it out to a staging location and + // attempt to install it. + File apkFile = new File(mDataDir, info.packageName); + try { + FileOutputStream apkStream = new FileOutputStream(apkFile); + byte[] buffer = new byte[32 * 1024]; + long size = info.size; + while (size > 0) { + long toRead = (buffer.length < size) ? buffer.length : size; + int didRead = instream.read(buffer, 0, (int)toRead); + apkStream.write(buffer, 0, didRead); + size -= didRead; + } + apkStream.close(); + + // make sure the installer can read it + apkFile.setReadable(true, false); + + // Now install it + Uri packageUri = Uri.fromFile(apkFile); + mInstallObserver.reset(); + mPackageManager.installPackage(packageUri, mInstallObserver, + PackageManager.INSTALL_REPLACE_EXISTING, installerPackage); + mInstallObserver.waitForCompletion(); + + if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) { + // The only time we continue to accept install of data even if the + // apk install failed is if we had already determined that we could + // accept the data regardless. + if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) { + okay = false; + } + } + } catch (IOException e) { + Slog.e(TAG, "Unable to transcribe restored apk for install"); + okay = false; + } finally { + apkFile.delete(); + } + + return okay; + } + + // Given an actual file content size, consume the post-content padding mandated + // by the tar format. + void skipTarPadding(long size, InputStream instream) throws IOException { + long partial = (size + 512) % 512; + if (partial > 0) { + byte[] buffer = new byte[512]; + instream.read(buffer, 0, 512 - (int)partial); + } + } + + // Returns a policy constant; takes a buffer arg to reduce memory churn + RestorePolicy readAppManifest(FileMetadata info, InputStream instream) + throws IOException { + // Fail on suspiciously large manifest files + if (info.size > 64 * 1024) { + throw new IOException("Restore manifest too big; corrupt? size=" + info.size); + } + byte[] buffer = new byte[(int) info.size]; + int nRead = 0; + while (nRead < info.size) { + nRead += instream.read(buffer, nRead, (int)info.size - nRead); + } + + RestorePolicy policy = RestorePolicy.IGNORE; + String[] str = new String[1]; + int offset = 0; + + try { + offset = extractLine(buffer, offset, str); + int version = Integer.parseInt(str[0]); + if (version == BACKUP_MANIFEST_VERSION) { + offset = extractLine(buffer, offset, str); + String manifestPackage = str[0]; + // TODO: handle <original-package> + if (manifestPackage.equals(info.packageName)) { + offset = extractLine(buffer, offset, str); + version = Integer.parseInt(str[0]); // app version + offset = extractLine(buffer, offset, str); + int platformVersion = Integer.parseInt(str[0]); + offset = extractLine(buffer, offset, str); + info.installerPackageName = (str[0].length() > 0) ? str[0] : null; + offset = extractLine(buffer, offset, str); + boolean hasApk = str[0].equals("1"); + offset = extractLine(buffer, offset, str); + int numSigs = Integer.parseInt(str[0]); + Signature[] sigs = null; + if (numSigs > 0) { + sigs = new Signature[numSigs]; + for (int i = 0; i < numSigs; i++) { + offset = extractLine(buffer, offset, str); + sigs[i] = new Signature(str[0]); + } + + // Okay, got the manifest info we need... + try { + // Verify signatures against any installed version; if they + // don't match, then we fall though and ignore the data. The + // signatureMatch() method explicitly ignores the signature + // check for packages installed on the system partition, because + // such packages are signed with the platform cert instead of + // the app developer's cert, so they're different on every + // device. + PackageInfo pkgInfo = mPackageManager.getPackageInfo( + info.packageName, PackageManager.GET_SIGNATURES); + if (signaturesMatch(sigs, pkgInfo)) { + if (pkgInfo.versionCode >= version) { + Slog.i(TAG, "Sig + version match; taking data"); + policy = RestorePolicy.ACCEPT; + } else { + // The data is from a newer version of the app than + // is presently installed. That means we can only + // use it if the matching apk is also supplied. + Slog.d(TAG, "Data version " + version + + " is newer than installed version " + + pkgInfo.versionCode + " - requiring apk"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + } + } catch (NameNotFoundException e) { + // Okay, the target app isn't installed. We can process + // the restore properly only if the dataset provides the + // apk file and we can successfully install it. + if (DEBUG) Slog.i(TAG, "Package " + info.packageName + + " not installed; requiring apk in dataset"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + + if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) { + Slog.i(TAG, "Cannot restore package " + info.packageName + + " without the matching .apk"); + } + } else { + Slog.i(TAG, "Missing signature on backed-up package " + + info.packageName); + } + } else { + Slog.i(TAG, "Expected package " + info.packageName + + " but restore manifest claims " + manifestPackage); + } + } else { + Slog.i(TAG, "Unknown restore manifest version " + version + + " for package " + info.packageName); + } + } catch (NumberFormatException e) { + Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); + } + + return policy; + } + + // Builds a line from a byte buffer starting at 'offset', and returns + // the index of the next unconsumed data in the buffer. + int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException { + final int end = buffer.length; + if (offset >= end) throw new IOException("Incomplete data"); + + int pos; + for (pos = offset; pos < end; pos++) { + byte c = buffer[pos]; + // at LF we declare end of line, and return the next char as the + // starting point for the next time through + if (c == '\n') { + break; + } + } + outStr[0] = new String(buffer, offset, pos - offset); + pos++; // may be pointing an extra byte past the end but that's okay + return pos; + } + + void dumpFileMetadata(FileMetadata info) { + if (DEBUG) { + StringBuilder b = new StringBuilder(128); + + // mode string + b.append((info.type == FullBackup.TYPE_DIRECTORY) ? 'd' : '-'); + b.append(((info.mode & 0400) != 0) ? 'r' : '-'); + b.append(((info.mode & 0200) != 0) ? 'w' : '-'); + b.append(((info.mode & 0100) != 0) ? 'x' : '-'); + b.append(((info.mode & 0040) != 0) ? 'r' : '-'); + b.append(((info.mode & 0020) != 0) ? 'w' : '-'); + b.append(((info.mode & 0010) != 0) ? 'x' : '-'); + b.append(((info.mode & 0004) != 0) ? 'r' : '-'); + b.append(((info.mode & 0002) != 0) ? 'w' : '-'); + b.append(((info.mode & 0001) != 0) ? 'x' : '-'); + b.append(String.format(" %9d ", info.size)); + + Date stamp = new Date(info.mtime); + b.append(new SimpleDateFormat("MMM dd kk:mm:ss ").format(stamp)); + + b.append(info.packageName); + b.append(" :: "); + b.append(info.domain); + b.append(" :: "); + b.append(info.path); + + Slog.i(TAG, b.toString()); + } + } + // Consume a tar file header block [sequence] and accumulate the relevant metadata + FileMetadata readTarHeaders(InputStream instream) throws IOException { + byte[] block = new byte[512]; + FileMetadata info = null; + + boolean gotHeader = readTarHeader(instream, block); + if (gotHeader) { + // okay, presume we're okay, and extract the various metadata + info = new FileMetadata(); + info.size = extractRadix(block, 124, 12, 8); + info.mtime = extractRadix(block, 136, 12, 8); + info.mode = extractRadix(block, 100, 8, 8); + + info.path = extractString(block, 345, 155); // prefix + String path = extractString(block, 0, 100); + if (path.length() > 0) { + if (info.path.length() > 0) info.path += '/'; + info.path += path; + } + + // tar link indicator field: 1 byte at offset 156 in the header. + int typeChar = block[156]; + if (typeChar == 'x') { + // pax extended header, so we need to read that + gotHeader = readPaxExtendedHeader(instream, info); + if (gotHeader) { + // and after a pax extended header comes another real header -- read + // that to find the real file type + gotHeader = readTarHeader(instream, block); + } + if (!gotHeader) throw new IOException("Bad or missing pax header"); + + typeChar = block[156]; + } + + switch (typeChar) { + case '0': info.type = FullBackup.TYPE_FILE; break; + case '5': info.type = FullBackup.TYPE_DIRECTORY; break; + case 0: { + // presume EOF + return null; + } + default: { + Slog.e(TAG, "Unknown tar entity type: " + typeChar); + throw new IOException("Unknown entity type " + typeChar); + } + } + + // Parse out the path + // + // first: apps/shared/unrecognized + if (FullBackup.SHARED_PREFIX.regionMatches(0, + info.path, 0, FullBackup.SHARED_PREFIX.length())) { + // File in shared storage. !!! TODO: implement this. + info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); + info.domain = FullBackup.SHARED_STORAGE_TOKEN; + } else if (FullBackup.APPS_PREFIX.regionMatches(0, + info.path, 0, FullBackup.APPS_PREFIX.length())) { + // App content! Parse out the package name and domain + + // strip the apps/ prefix + info.path = info.path.substring(FullBackup.APPS_PREFIX.length()); + + // extract the package name + int slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in " + info.path); + info.packageName = info.path.substring(0, slash); + info.path = info.path.substring(slash+1); + + // if it's a manifest we're done, otherwise parse out the domains + if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) { + slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path); + info.domain = info.path.substring(0, slash); + // validate that it's one of the domains we understand + if (!info.domain.equals(FullBackup.APK_TREE_TOKEN) + && !info.domain.equals(FullBackup.DATA_TREE_TOKEN) + && !info.domain.equals(FullBackup.DATABASE_TREE_TOKEN) + && !info.domain.equals(FullBackup.ROOT_TREE_TOKEN) + && !info.domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN) + && !info.domain.equals(FullBackup.OBB_TREE_TOKEN) + && !info.domain.equals(FullBackup.CACHE_TREE_TOKEN)) { + throw new IOException("Unrecognized domain " + info.domain); + } + + info.path = info.path.substring(slash + 1); + } + } + } + return info; + } + + boolean readTarHeader(InputStream instream, byte[] block) throws IOException { + int nRead = instream.read(block, 0, 512); + if (nRead > 0 && nRead != 512) { + // if we read only a partial block, then things are + // clearly screwed up. terminate the restore. + throw new IOException("Partial header block: " + nRead); + } + return (nRead > 0); + } + + // overwrites 'info' fields based on the pax extended header + boolean readPaxExtendedHeader(InputStream instream, FileMetadata info) + throws IOException { + // We should never see a pax extended header larger than this + if (info.size > 32*1024) { + Slog.w(TAG, "Suspiciously large pax header size " + info.size + + " - aborting"); + throw new IOException("Sanity failure: pax header size " + info.size); + } + + // read whole blocks, not just the content size + int numBlocks = (int)((info.size + 511) >> 9); + byte[] data = new byte[numBlocks * 512]; + int nRead = instream.read(data); + if (nRead != data.length) { + return false; + } + + final int contentSize = (int) info.size; + int offset = 0; + do { + // extract the line at 'offset' + int eol = offset+1; + while (eol < contentSize && data[eol] != ' ') eol++; + if (eol >= contentSize) { + // error: we just hit EOD looking for the end of the size field + throw new IOException("Invalid pax data"); + } + // eol points to the space between the count and the key + int linelen = (int) extractRadix(data, offset, eol - offset, 10); + int key = eol + 1; // start of key=value + eol = offset + linelen - 1; // trailing LF + int value; + for (value = key+1; data[value] != '=' && value <= eol; value++); + if (value > eol) { + throw new IOException("Invalid pax declaration"); + } + + // pax requires that key/value strings be in UTF-8 + String keyStr = new String(data, key, value-key, "UTF-8"); + // -1 to strip the trailing LF + String valStr = new String(data, value+1, eol-value-1, "UTF-8"); + + if ("path".equals(keyStr)) { + info.path = valStr; + } else if ("size".equals(keyStr)) { + info.size = Long.parseLong(valStr); + } else { + if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key); + } + + offset += linelen; + } while (offset < contentSize); + + return true; + } + + long extractRadix(byte[] data, int offset, int maxChars, int radix) + throws IOException { + long value = 0; + final int end = offset + maxChars; + for (int i = offset; i < end; i++) { + final byte b = data[i]; + if (b == 0 || b == ' ') break; + if (b < '0' || b > ('0' + radix - 1)) { + throw new IOException("Invalid number in header"); + } + value = radix * value + (b - '0'); + } + return value; + } + + String extractString(byte[] data, int offset, int maxChars) throws IOException { + final int end = offset + maxChars; + int eos = offset; + // tar string fields can end with either NUL or SPC + while (eos < end && data[eos] != 0 && data[eos] != ' ') eos++; + return new String(data, offset, eos-offset, "US-ASCII"); + } + + void sendStartRestore() { + if (mObserver != null) { + try { + mObserver.onStartRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: startRestore"); + mObserver = null; + } + } + } + + void sendOnRestorePackage(String name) { + if (mObserver != null) { + try { + // TODO: use a more user-friendly name string + mObserver.onRestorePackage(name); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: restorePackage"); + mObserver = null; + } + } + } + + void sendEndRestore() { + if (mObserver != null) { + try { + mObserver.onEndRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: endRestore"); + mObserver = null; + } + } + } + } + // ----- Restore handling ----- private boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) { @@ -2583,43 +3494,97 @@ class BackupManagerService extends IBackupManager.Stub { mFullConfirmations.put(token, params); } - // start up the confirmation UI, making sure the screen lights up - if (DEBUG) Slog.d(TAG, "Starting confirmation UI, token=" + token); - try { - Intent confIntent = new Intent(FullBackup.FULL_BACKUP_INTENT_ACTION); - confIntent.setClassName("com.android.backupconfirm", - "com.android.backupconfirm.BackupRestoreConfirmation"); - confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); - confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(confIntent); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Unable to launch full backup confirmation", e); + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full backup confirmation"); mFullConfirmations.delete(token); return; } + + // make sure the screen is lit for the user interaction mPowerManager.userActivity(SystemClock.uptimeMillis(), false); // start the confirmation countdown - if (DEBUG) Slog.d(TAG, "Posting conf timeout msg after " - + TIMEOUT_FULL_CONFIRMATION + " millis"); - Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, - token, 0, params); - mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); + startConfirmationTimeout(token, params); // wait for the backup to be performed if (DEBUG) Slog.d(TAG, "Waiting for full backup completion..."); waitForCompletion(params); - if (DEBUG) Slog.d(TAG, "...Full backup operation complete!"); } finally { - Binder.restoreCallingIdentity(oldId); try { fd.close(); } catch (IOException e) { // just eat it } + Binder.restoreCallingIdentity(oldId); + } + if (DEBUG) Slog.d(TAG, "Full backup done; returning to caller"); + } + + public void fullRestore(ParcelFileDescriptor fd) { + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup"); + Slog.i(TAG, "Beginning full restore..."); + + long oldId = Binder.clearCallingIdentity(); + + try { + FullRestoreParams params = new FullRestoreParams(fd); + final int token = generateToken(); + synchronized (mFullConfirmations) { + mFullConfirmations.put(token, params); + } + + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full restore confirmation"); + mFullConfirmations.delete(token); + return; + } + + // make sure the screen is lit for the user interaction + mPowerManager.userActivity(SystemClock.uptimeMillis(), false); + + // start the confirmation countdown + startConfirmationTimeout(token, params); + + // wait for the restore to be performed + if (DEBUG) Slog.d(TAG, "Waiting for full restore completion..."); + waitForCompletion(params); + } finally { + try { + fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Error trying to close fd after full restore: " + e); + } + Binder.restoreCallingIdentity(oldId); + Slog.i(TAG, "Full restore completed"); } } + boolean startConfirmationUi(int token, String action) { + try { + Intent confIntent = new Intent(action); + confIntent.setClassName("com.android.backupconfirm", + "com.android.backupconfirm.BackupRestoreConfirmation"); + confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); + confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(confIntent); + } catch (ActivityNotFoundException e) { + return false; + } + return true; + } + + void startConfirmationTimeout(int token, FullParams params) { + if (DEBUG) Slog.d(TAG, "Posting conf timeout msg after " + + TIMEOUT_FULL_CONFIRMATION + " millis"); + Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, + token, 0, params); + mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); + } + void waitForCompletion(FullParams params) { synchronized (params.latch) { while (params.latch.get() == false) { @@ -2661,9 +3626,10 @@ class BackupManagerService extends IBackupManager.Stub { if (allow) { params.observer = observer; final int verb = params instanceof FullBackupParams - ? MSG_RUN_FULL_BACKUP + ? MSG_RUN_FULL_BACKUP : MSG_RUN_FULL_RESTORE; + if (DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb); mWakelock.acquire(); Message msg = mBackupHandler.obtainMessage(verb, params); mBackupHandler.sendMessage(msg); diff --git a/services/java/com/android/server/SystemBackupAgent.java b/services/java/com/android/server/SystemBackupAgent.java index 54555bb..99c8af6 100644 --- a/services/java/com/android/server/SystemBackupAgent.java +++ b/services/java/com/android/server/SystemBackupAgent.java @@ -37,16 +37,25 @@ import java.io.IOException; public class SystemBackupAgent extends BackupAgentHelper { private static final String TAG = "SystemBackupAgent"; - // These paths must match what the WallpaperManagerService uses + // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME + // are also used in the full-backup file format, so must not change unless steps are + // taken to support the legacy backed-up datasets. + private static final String WALLPAPER_IMAGE_FILENAME = "wallpaper"; + private static final String WALLPAPER_INFO_FILENAME = "wallpaper_info.xml"; + private static final String WALLPAPER_IMAGE_DIR = "/data/data/com.android.settings/files"; - private static final String WALLPAPER_IMAGE = WALLPAPER_IMAGE_DIR + "/wallpaper"; + private static final String WALLPAPER_IMAGE = WALLPAPER_IMAGE_DIR + "/" + WALLPAPER_IMAGE_FILENAME; + private static final String WALLPAPER_INFO_DIR = "/data/system"; - private static final String WALLPAPER_INFO = WALLPAPER_INFO_DIR + "/wallpaper_info.xml"; + private static final String WALLPAPER_INFO = WALLPAPER_INFO_DIR + "/" + WALLPAPER_INFO_FILENAME; + @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { if (oldState == null) { + // Ah, it's a full backup dataset, being restored piecemeal. Just + // pop over to the full restore handling and we're done. runFullBackup(data); return; } @@ -66,11 +75,18 @@ public class SystemBackupAgent extends BackupAgentHelper { } private void runFullBackup(BackupDataOutput output) { - // Back up the data files directly - FullBackup.backupToTar(getPackageName(), null, null, - WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output); - FullBackup.backupToTar(getPackageName(), null, null, + fullWallpaperBackup(output); + } + + private void fullWallpaperBackup(BackupDataOutput output) { + // Back up the data files directly. We do them in this specific order -- + // info file followed by image -- because then we need take no special + // steps during restore; the restore will happen properly when the individual + // files are restored piecemeal. + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, WALLPAPER_INFO_DIR, WALLPAPER_INFO, output); + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, + WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output); } @Override @@ -96,4 +112,46 @@ public class SystemBackupAgent extends BackupAgentHelper { (new File(WALLPAPER_INFO)).delete(); } } + + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + Slog.i(TAG, "Restoring file domain=" + domain + " path=" + path); + + // Bits to indicate postprocessing we may need to perform + boolean restoredWallpaper = false; + + File outFile = null; + // Various domain+files we understand a priori + if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { + if (path.equals(WALLPAPER_INFO_FILENAME)) { + outFile = new File(WALLPAPER_INFO); + restoredWallpaper = true; + } else if (path.equals(WALLPAPER_IMAGE_FILENAME)) { + outFile = new File(WALLPAPER_IMAGE); + restoredWallpaper = true; + } + } + + try { + if (outFile == null) { + Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]"); + } + FullBackup.restoreToFile(data, size, type, mode, mtime, outFile); + + if (restoredWallpaper) { + WallpaperManagerService wallpaper = + (WallpaperManagerService)ServiceManager.getService( + Context.WALLPAPER_SERVICE); + wallpaper.settingsRestored(); + } + } catch (IOException e) { + if (restoredWallpaper) { + // Make sure we wind up in a good state + (new File(WALLPAPER_IMAGE)).delete(); + (new File(WALLPAPER_INFO)).delete(); + } + } + } } diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index 262e5ce..b463e56 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -3676,6 +3676,7 @@ public final class ActivityManagerService extends ActivityManagerNative boolean isRestrictedBackupMode = false; if (mBackupTarget != null && mBackupAppName.equals(processName)) { isRestrictedBackupMode = (mBackupTarget.backupMode == BackupRecord.RESTORE) + || (mBackupTarget.backupMode == BackupRecord.RESTORE_FULL) || (mBackupTarget.backupMode == BackupRecord.BACKUP_FULL); } diff --git a/services/java/com/android/server/am/BackupRecord.java b/services/java/com/android/server/am/BackupRecord.java index 6590b91..7e73106 100644 --- a/services/java/com/android/server/am/BackupRecord.java +++ b/services/java/com/android/server/am/BackupRecord.java @@ -26,6 +26,7 @@ class BackupRecord { public static final int BACKUP_NORMAL = 0; public static final int BACKUP_FULL = 1; public static final int RESTORE = 2; + public static final int RESTORE_FULL = 3; final BatteryStatsImpl.Uid.Pkg.Serv stats; String stringName; // cached toString() output |