diff options
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 |