BackupAgentpublic abstract class BackupAgent extends android.content.ContextWrapper Provides the central interface between an
application and Android's data backup infrastructure. An application that wishes
to participate in the backup and restore mechanism will declare a subclass of
{@link android.app.backup.BackupAgent}, implement the
{@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()}
and {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} methods,
and provide the name of its backup agent class in its {@code AndroidManifest.xml} file via
the
<application>
tag's {@code android:backupAgent} attribute.
Developer Guides
For more information about using BackupAgent, read the
Data Backup developer guide.
Basic Operation
When the application makes changes to data that it wishes to keep backed up,
it should call the
{@link android.app.backup.BackupManager#dataChanged() BackupManager.dataChanged()} method.
This notifies the Android Backup Manager that the application needs an opportunity
to update its backup image. The Backup Manager, in turn, schedules a
backup pass to be performed at an opportune time.
Restore operations are typically performed only when applications are first
installed on a device. At that time, the operating system checks to see whether
there is a previously-saved data set available for the application being installed, and if so,
begins an immediate restore pass to deliver the backup data as part of the installation
process.
When a backup or restore pass is run, the application's process is launched
(if not already running), the manifest-declared backup agent class (in the {@code
android:backupAgent} attribute) is instantiated within
that process, and the agent's {@link #onCreate()} method is invoked. This prepares the
agent instance to run the actual backup or restore logic. At this point the
agent's
{@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} or
{@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} method will be
invoked as appropriate for the operation being performed.
A backup data set consists of one or more "entities," flattened binary data
records that are each identified with a key string unique within the data set. Adding a
record to the active data set or updating an existing record is done by simply
writing new entity data under the desired key. Deleting an entity from the data set
is done by writing an entity under that key with header specifying a negative data
size, and no actual entity data.
Helper Classes
An extensible agent based on convenient helper classes is available in
{@link android.app.backup.BackupAgentHelper}. That class is particularly
suited to handling of simple file or {@link android.content.SharedPreferences}
backup and restore. |
Fields Summary |
---|
private static final String | TAG | private static final boolean | DEBUG | public static final int | TYPE_EOF | public static final int | TYPE_FILEDuring a full restore, indicates that the file system object being restored
is an ordinary file. | public static final int | TYPE_DIRECTORYDuring a full restore, indicates that the file system object being restored
is a directory. | public static final int | TYPE_SYMLINK | android.os.Handler | mHandler | private final android.os.IBinder | mBinder |
Constructors Summary |
---|
public BackupAgent()
super(null);
|
Methods Summary |
---|
public void | attach(android.content.Context context)
attachBaseContext(context);
| public final void | fullBackupFile(java.io.File file, FullBackupDataOutput output)Write an entire file as part of a full-backup operation. The file's contents
will be delivered to the backup destination along with the metadata necessary
to place it with the proper location and permissions on the device where the
data is restored.
It is safe to explicitly back up files underneath your application's
{@link #getNoBackupFilesDir()} directory, and they will be restored to that
location correctly.
// Look up where all of our various well-defined dir trees live on this device
String mainDir;
String filesDir;
String nbFilesDir;
String dbDir;
String spDir;
String cacheDir;
String libDir;
String efDir = null;
String filePath;
ApplicationInfo appInfo = getApplicationInfo();
try {
mainDir = new File(appInfo.dataDir).getCanonicalPath();
filesDir = getFilesDir().getCanonicalPath();
nbFilesDir = getNoBackupFilesDir().getCanonicalPath();
dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
cacheDir = getCacheDir().getCanonicalPath();
libDir = (appInfo.nativeLibraryDir == null)
? null
: new File(appInfo.nativeLibraryDir).getCanonicalPath();
// may or may not have external files access to attempt backup/restore there
if (Process.myUid() != Process.SYSTEM_UID) {
File efLocation = getExternalFilesDir(null);
if (efLocation != null) {
efDir = efLocation.getCanonicalPath();
}
}
// Now figure out which well-defined tree the file is placed in, working from
// most to least specific. We also specifically exclude the lib and cache dirs.
filePath = file.getCanonicalPath();
} catch (IOException e) {
Log.w(TAG, "Unable to obtain canonical paths");
return;
}
if (filePath.startsWith(cacheDir)
|| filePath.startsWith(libDir)
|| filePath.startsWith(nbFilesDir)) {
Log.w(TAG, "lib, cache, and no_backup files are not backed up");
return;
}
final String domain;
String rootpath = null;
if (filePath.startsWith(dbDir)) {
domain = FullBackup.DATABASE_TREE_TOKEN;
rootpath = dbDir;
} else if (filePath.startsWith(spDir)) {
domain = FullBackup.SHAREDPREFS_TREE_TOKEN;
rootpath = spDir;
} else if (filePath.startsWith(filesDir)) {
domain = FullBackup.DATA_TREE_TOKEN;
rootpath = filesDir;
} else if (filePath.startsWith(mainDir)) {
domain = FullBackup.ROOT_TREE_TOKEN;
rootpath = mainDir;
} else if ((efDir != null) && filePath.startsWith(efDir)) {
domain = FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
rootpath = efDir;
} else {
Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping");
return;
}
// And now that we know where it lives, semantically, back it up appropriately
Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain
+ " rootpath=" + rootpath);
FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath,
output.getData());
| protected final void | fullBackupFileTree(java.lang.String packageName, java.lang.String domain, java.lang.String rootPath, java.util.HashSet excludes, FullBackupDataOutput output)Scan the dir tree (if it actually exists) and process each entry we find. If the
'excludes' parameter is non-null, it is consulted each time a new file system entity
is visited to see whether that entity (and its subtree, if appropriate) should be
omitted from the backup process.
File rootFile = new File(rootPath);
if (rootFile.exists()) {
LinkedList<File> scanQueue = new LinkedList<File>();
scanQueue.add(rootFile);
while (scanQueue.size() > 0) {
File file = scanQueue.remove(0);
String filePath;
try {
filePath = file.getCanonicalPath();
// prune this subtree?
if (excludes != null && excludes.contains(filePath)) {
continue;
}
// If it's a directory, enqueue its contents for scanning.
StructStat stat = Os.lstat(filePath);
if (OsConstants.S_ISLNK(stat.st_mode)) {
if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file);
continue;
} else if (OsConstants.S_ISDIR(stat.st_mode)) {
File[] contents = file.listFiles();
if (contents != null) {
for (File entry : contents) {
scanQueue.add(0, entry);
}
}
}
} catch (IOException e) {
if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file);
continue;
} catch (ErrnoException e) {
if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e);
continue;
}
// Finally, back this file up before proceeding
FullBackup.backupToTar(packageName, domain, null, rootPath, filePath,
output.getData());
}
}
| android.os.Handler | getHandler()
if (mHandler == null) {
mHandler = new Handler(Looper.getMainLooper());
}
return mHandler;
| public abstract void | onBackup(android.os.ParcelFileDescriptor oldState, BackupDataOutput data, android.os.ParcelFileDescriptor newState)The application is being asked to write any data changed since the last
time it performed a backup operation. The state data recorded during the
last backup pass is provided in the oldState file
descriptor. If oldState is null , no old state
is available and the application should perform a full backup. In both
cases, a representation of the final backup state after this pass should
be written to the file pointed to by the file descriptor wrapped in
newState .
Each entity written to the {@link android.app.backup.BackupDataOutput}
data stream will be transmitted
over the current backup transport and stored in the remote data set under
the key supplied as part of the entity. Writing an entity with a negative
data size instructs the transport to delete whatever entity currently exists
under that key from the remote data set.
| public final android.os.IBinder | onBind()
return mBinder;
| public void | onCreate()Provided as a convenience for agent implementations that need an opportunity
to do one-time initialization before the actual backup or restore operation
is begun.
Agents do not need to override this method.
| public void | onDestroy()Provided as a convenience for agent implementations that need to do some
sort of shutdown process after backup or restore is completed.
Agents do not need to override this method.
| public void | onFullBackup(FullBackupDataOutput data)The application is having its entire file system contents backed up. {@code data}
points to the backup destination, and the app has the opportunity to choose which
files are to be stored. To commit a file as part of the backup, call the
{@link #fullBackupFile(File, FullBackupDataOutput)} helper method. After all file
data is written to the output, the agent returns from this method and the backup
operation concludes.
Certain parts of the app's data are never backed up even if the app explicitly
sends them to the output:
- The contents of the {@link #getCacheDir()} directory
- The contents of the {@link #getNoBackupFilesDir()} directory
- The contents of the app's shared library directory
The default implementation of this method backs up the entirety of the
application's "owned" file system trees to the output other than the few exceptions
listed above. Apps only need to override this method if they need to impose special
limitations on which files are being stored beyond the control that
{@link #getNoBackupFilesDir()} offers.
ApplicationInfo appInfo = getApplicationInfo();
// Note that we don't need to think about the no_backup dir because it's outside
// all of the ones we will be traversing
String rootDir = new File(appInfo.dataDir).getCanonicalPath();
String filesDir = getFilesDir().getCanonicalPath();
String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
String cacheDir = getCacheDir().getCanonicalPath();
String libDir = (appInfo.nativeLibraryDir != null)
? new File(appInfo.nativeLibraryDir).getCanonicalPath()
: null;
// Filters, the scan queue, and the set of resulting entities
HashSet<String> filterSet = new HashSet<String>();
String packageName = getPackageName();
// Okay, start with the app's root tree, but exclude all of the canonical subdirs
if (libDir != null) {
filterSet.add(libDir);
}
filterSet.add(cacheDir);
filterSet.add(databaseDir);
filterSet.add(sharedPrefsDir);
filterSet.add(filesDir);
fullBackupFileTree(packageName, FullBackup.ROOT_TREE_TOKEN, rootDir, filterSet, data);
// Now do the same for the files dir, db dir, and shared prefs dir
filterSet.add(rootDir);
filterSet.remove(filesDir);
fullBackupFileTree(packageName, FullBackup.DATA_TREE_TOKEN, filesDir, filterSet, data);
filterSet.add(filesDir);
filterSet.remove(databaseDir);
fullBackupFileTree(packageName, FullBackup.DATABASE_TREE_TOKEN, databaseDir, filterSet, data);
filterSet.add(databaseDir);
filterSet.remove(sharedPrefsDir);
fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, filterSet, data);
// getExternalFilesDir() location associated with this app. Technically there should
// not be any files here if the app does not properly have permission to access
// external storage, but edge cases happen. fullBackupFileTree() catches
// IOExceptions and similar, and treats them as non-fatal, so we rely on that; and
// we know a priori that processes running as the system UID are not permitted to
// access external storage, so we check for that as well to avoid nastygrams in
// the log.
if (Process.myUid() != Process.SYSTEM_UID) {
File efLocation = getExternalFilesDir(null);
if (efLocation != null) {
fullBackupFileTree(packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN,
efLocation.getCanonicalPath(), null, data);
}
}
| public abstract void | onRestore(BackupDataInput data, int appVersionCode, android.os.ParcelFileDescriptor newState)The application is being restored from backup and should replace any
existing data with the contents of the backup. The backup data is
provided through the data parameter. Once
the restore is finished, the application should write a representation of
the final state to the newState file descriptor.
The application is responsible for properly erasing its old data and
replacing it with the data supplied to this method. No "clear user data"
operation will be performed automatically by the operating system. The
exception to this is in the case of a failed restore attempt: if
onRestore() throws an exception, the OS will assume that the
application's data may now be in an incoherent state, and will clear it
before proceeding.
| public void | onRestoreFile(android.os.ParcelFileDescriptor data, long size, java.io.File destination, int type, long mode, long mtime)Handle the data delivered via the given file descriptor during a full restore
operation. The agent is given the path to the file's original location as well
as its size and metadata.
The file descriptor can only be read for {@code size} bytes; attempting to read
more data has undefined behavior.
The default implementation creates the destination file/directory and populates it
with the data from the file descriptor, then sets the file's access mode and
modification time to match the restore arguments.
FullBackup.restoreFile(data, size, type, mode, mtime, destination);
| protected void | onRestoreFile(android.os.ParcelFileDescriptor data, long size, int type, java.lang.String domain, java.lang.String path, long mode, long mtime)Only specialized platform agents should overload this entry point to support
restores to crazy non-app locations.
String basePath = null;
if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type
+ " domain=" + domain + " relpath=" + path + " mode=" + mode
+ " mtime=" + mtime);
// Parse out the semantic domains into the correct physical location
if (domain.equals(FullBackup.DATA_TREE_TOKEN)) {
basePath = getFilesDir().getCanonicalPath();
} else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) {
basePath = getDatabasePath("foo").getParentFile().getCanonicalPath();
} else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) {
basePath = new File(getApplicationInfo().dataDir).getCanonicalPath();
} else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
} else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) {
basePath = getCacheDir().getCanonicalPath();
} else if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
// make sure we can try to restore here before proceeding
if (Process.myUid() != Process.SYSTEM_UID) {
File efLocation = getExternalFilesDir(null);
if (efLocation != null) {
basePath = getExternalFilesDir(null).getCanonicalPath();
mode = -1; // < 0 is a token to skip attempting a chmod()
}
}
} else if (domain.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
basePath = getNoBackupFilesDir().getCanonicalPath();
} else {
// Not a supported location
Log.i(TAG, "Unrecognized domain " + domain);
}
// Now that we've figured out where the data goes, send it on its way
if (basePath != null) {
// Canonicalize the nominal path and verify that it lies within the stated domain
File outFile = new File(basePath, path);
String outPath = outFile.getCanonicalPath();
if (outPath.startsWith(basePath + File.separatorChar)) {
if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outPath);
onRestoreFile(data, size, outFile, type, mode, mtime);
return;
} else {
// Attempt to restore to a path outside the file's nominal domain.
if (DEBUG) {
Log.e(TAG, "Cross-domain restore attempt: " + outPath);
}
}
}
// Not a supported output location, or bad path: we need to consume the data
// anyway, so just use the default "copy the data out" implementation
// with a null destination.
if (DEBUG) Log.i(TAG, "[ skipping file " + path + "]");
FullBackup.restoreFile(data, size, type, mode, mtime, null);
| public void | onRestoreFinished()The application's restore operation has completed. This method is called after
all available data has been delivered to the application for restore (via either
the {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} or
{@link #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) onRestoreFile()}
callbacks). This provides the app with a stable end-of-restore opportunity to
perform any appropriate post-processing on the data that was just delivered.
| private void | waitForSharedPrefs()
Handler h = getHandler();
final SharedPrefsSynchronizer s = new SharedPrefsSynchronizer();
h.postAtFrontOfQueue(s);
try {
s.mLatch.await();
} catch (InterruptedException e) { /* ignored */ }
|
|