/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.backup;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.IBackupAgent;
import android.app.QueuedWork;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupAnnotations.OperationType;
import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructStat;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.server.backup.Flags;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* 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.
*
*
For more information about using BackupAgent, read the * Data Backup developer guide.
* 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. *
* Threading *
* The constructor, as well as {@link #onCreate()} and {@link #onDestroy()} lifecycle callbacks run * on the main thread (UI thread) of the application that implements the BackupAgent. * The data-handling callbacks: * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()}, * {@link #onFullBackup(FullBackupDataOutput)}, * {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()}, * {@link #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) onRestoreFile()}, * {@link #onRestoreFinished()}, and {@link #onQuotaExceeded(long, long) onQuotaExceeded()} * run on binder pool threads. * * @see android.app.backup.BackupManager * @see android.app.backup.BackupAgentHelper * @see android.app.backup.BackupDataInput * @see android.app.backup.BackupDataOutput */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; private static final boolean DEBUG = false; private static final int DEFAULT_BACKUP_DESTINATION = BackupDestination.CLOUD; /** @hide */ public static final int RESULT_SUCCESS = 0; /** @hide */ public static final int RESULT_ERROR = -1; /** @hide */ public static final int TYPE_EOF = 0; /** * During a full restore, indicates that the file system object being restored * is an ordinary file. */ public static final int TYPE_FILE = 1; /** * During a full restore, indicates that the file system object being restored * is a directory. */ public static final int TYPE_DIRECTORY = 2; /** @hide */ public static final int TYPE_SYMLINK = 3; /** * Flag for {@link BackupDataOutput#getTransportFlags()} and * {@link FullBackupDataOutput#getTransportFlags()} only. * *
The transport has client-side encryption enabled. i.e., the user's backup has been * encrypted with a key known only to the device, and not to the remote storage solution. Even * if an attacker had root access to the remote storage provider they should not be able to * decrypt the user's backup data. */ public static final int FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED = 1; /** * Flag for {@link BackupDataOutput#getTransportFlags()} and * {@link FullBackupDataOutput#getTransportFlags()} only. * *
The transport is for a device-to-device transfer. There is no third party or intermediate * storage. The user's backup data is sent directly to another device over e.g., USB or WiFi. */ public static final int FLAG_DEVICE_TO_DEVICE_TRANSFER = 2; /** * Flag for {@link RestoreSet#backupTransportFlags} to indicate if restore should be skipped * for apps that have already been launched. * * @hide */ public static final int FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS = 1 << 2; /** * Flag for {@link BackupDataOutput#getTransportFlags()} and * {@link FullBackupDataOutput#getTransportFlags()} only. * *
Used for internal testing only. Do not check this flag in production code. * * @hide */ public static final int FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED = 1 << 31; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED, FLAG_DEVICE_TO_DEVICE_TRANSFER, FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED }) public @interface BackupTransportFlags {} Handler mHandler = null; @Nullable private volatile BackupRestoreEventLogger mLogger = null; @Nullable private UserHandle mUser; // This field is written from the main thread (in onCreate), and read in a Binder thread (in // onFullBackup that is called from system_server via Binder). @BackupDestination private volatile int mBackupDestination = DEFAULT_BACKUP_DESTINATION; Handler getHandler() { if (mHandler == null) { mHandler = new Handler(Looper.getMainLooper()); } return mHandler; } class SharedPrefsSynchronizer implements Runnable { public final CountDownLatch mLatch = new CountDownLatch(1); @Override public void run() { QueuedWork.waitToFinish(); mLatch.countDown(); } }; // Syncing shared preferences deferred writes needs to happen on the main looper thread private void waitForSharedPrefs() { Handler h = getHandler(); final SharedPrefsSynchronizer s = new SharedPrefsSynchronizer(); h.postAtFrontOfQueue(s); try { s.mLatch.await(); } catch (InterruptedException e) { /* ignored */ } } /** * Get a logger to record app-specific backup and restore events that are happening during a * backup or restore operation. * *
The logger instance had been created by the system with the correct {@link * BackupRestoreEventLogger.OperationType} that corresponds to the operation the {@code * BackupAgent} is currently handling. * * @hide */ @Nullable public BackupRestoreEventLogger getBackupRestoreEventLogger() { return mLogger; } public BackupAgent() { super(null); } /** * 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. *
*/ public void onCreate() { } /** @hide */ public void onCreate(UserHandle user) { mUser = user; onCreate(); } /** * @deprecated Use {@link BackupAgent#onCreate(UserHandle, int, int)} instead. * * @hide */ @Deprecated public void onCreate(UserHandle user, @BackupDestination int backupDestination) { mBackupDestination = backupDestination; onCreate(user); } /** * @hide */ public void onCreate(UserHandle user, @BackupDestination int backupDestination, @OperationType int operationType) { mBackupDestination = backupDestination; mLogger = new BackupRestoreEventLogger(operationType); onCreate(user, backupDestination); } /** * 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 onDestroy() {
}
/**
* 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.
*
* @param oldState An open, read-only ParcelFileDescriptor pointing to the
* last backup state provided by the application. May be
* null
, in which case no prior state is being
* provided and the application should perform a full backup.
* @param data A structured wrapper around an open, read/write
* file descriptor pointing to the backup data destination.
* Typically the application will use backup helper classes to
* write to this file.
* @param newState An open, read/write ParcelFileDescriptor pointing to an
* empty file. The application should record the final backup
* state here after writing the requested data to the data
* output stream.
*/
public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException;
/**
* 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.
*
* @param data A structured wrapper around an open, read-only
* file descriptor pointing to a full snapshot of the
* application's data. The application should consume every
* entity represented in this data stream.
* @param appVersionCode The value of the {@code
* android:versionCode} manifest attribute,
* from the application that backed up this particular data set. This
* makes it possible for an application's agent to distinguish among any
* possible older data versions when asked to perform the restore
* operation.
* @param newState An open, read/write ParcelFileDescriptor pointing to an
* empty file. The application should record the final backup
* state here after restoring its data from the Certain parts of the app's data are never backed up even if the app explicitly
* sends them to the output:
*
* 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.
* Alternatively they can provide an xml resource to specify what data to include or exclude.
*
*
* @param data A structured wrapper pointing to the backup destination.
* @throws IOException
*
* @see Context#getNoBackupFilesDir()
* @see #fullBackupFile(File, FullBackupDataOutput)
* @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long)
*/
public void onFullBackup(FullBackupDataOutput data) throws IOException {
FullBackup.BackupScheme backupScheme = FullBackup.getBackupScheme(this,
mBackupDestination);
if (!backupScheme.isFullBackupEnabled(data.getTransportFlags())) {
return;
}
IncludeExcludeRules includeExcludeRules;
try {
includeExcludeRules = getIncludeExcludeRules(backupScheme);
} catch (IOException | XmlPullParserException e) {
if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
Log.v(FullBackup.TAG_XML_PARSER,
"Exception trying to parse fullBackupContent xml file!"
+ " Aborting full backup.", e);
}
return;
}
Map The {@code quotaBytes} value is the total data size currently permitted for this
* application. If desired, the application can use this as a hint for determining
* how much data to store. For example, a messaging application might choose to
* store only the newest messages, dropping enough older content to stay under
* the quota.
*
* Note that the maximum quota for the application can change over
* time. In particular, in the future the quota may grow. Applications that adapt
* to the quota when deciding what data to store should be aware of this and implement
* their data storage mechanisms in a way that can take advantage of additional
* quota.
*
* @param backupDataBytes The amount of data measured while initializing the backup
* operation, if the total exceeds the app's alloted quota. If initial measurement
* suggested that the data would fit but then too much data was actually submitted
* as part of the operation, then this value is the amount of data that had been
* streamed into the transport at the time the quota was reached.
* @param quotaBytes The maximum data size that the transport currently permits
* this application to store as a backup.
*/
public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
}
private int getBackupUserId() {
return mUser == null ? super.getUserId() : mUser.getIdentifier();
}
/**
* Check whether the xml yielded any Attempting to back up files in directories that are ignored by
* the backup system will have no effect. For example, if the app calls this method
* with a file inside the {@link #getNoBackupFilesDir()} directory, it will be ignored.
* See {@link #onFullBackup(FullBackupDataOutput)} for details on what directories
* are excluded from backups.
*
* @param file The file to be backed up. The file must exist and be readable by
* the caller.
* @param output The destination to which the backed-up file data will be sent.
*/
public final void fullBackupFile(File file, FullBackupDataOutput output) {
// Look up where all of our various well-defined dir trees live on this device
final String rootDir;
final String filesDir;
final String nbFilesDir;
final String dbDir;
final String spDir;
final String cacheDir;
final String codeCacheDir;
final String deviceRootDir;
final String deviceFilesDir;
final String deviceNbFilesDir;
final String deviceDbDir;
final String deviceSpDir;
final String deviceCacheDir;
final String deviceCodeCacheDir;
final String libDir;
String efDir = null;
String filePath;
ApplicationInfo appInfo = getApplicationInfo();
try {
// System apps have control over where their default storage context
// is pointed, so we're always explicit when building paths.
final Context ceContext = createCredentialProtectedStorageContext();
rootDir = ceContext.getDataDir().getCanonicalPath();
filesDir = ceContext.getFilesDir().getCanonicalPath();
nbFilesDir = ceContext.getNoBackupFilesDir().getCanonicalPath();
dbDir = ceContext.getDatabasePath("foo").getParentFile().getCanonicalPath();
spDir = ceContext.getSharedPreferencesPath("foo").getParentFile().getCanonicalPath();
cacheDir = ceContext.getCacheDir().getCanonicalPath();
codeCacheDir = ceContext.getCodeCacheDir().getCanonicalPath();
final Context deContext = createDeviceProtectedStorageContext();
deviceRootDir = deContext.getDataDir().getCanonicalPath();
deviceFilesDir = deContext.getFilesDir().getCanonicalPath();
deviceNbFilesDir = deContext.getNoBackupFilesDir().getCanonicalPath();
deviceDbDir = deContext.getDatabasePath("foo").getParentFile().getCanonicalPath();
deviceSpDir = deContext.getSharedPreferencesPath("foo").getParentFile()
.getCanonicalPath();
deviceCacheDir = deContext.getCacheDir().getCanonicalPath();
deviceCodeCacheDir = deContext.getCodeCacheDir().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, cache,
// and code_cache dirs.
filePath = file.getCanonicalPath();
} catch (IOException e) {
Log.w(TAG, "Unable to obtain canonical paths");
return;
}
if (filePath.startsWith(cacheDir)
|| filePath.startsWith(codeCacheDir)
|| filePath.startsWith(nbFilesDir)
|| filePath.startsWith(deviceCacheDir)
|| filePath.startsWith(deviceCodeCacheDir)
|| filePath.startsWith(deviceNbFilesDir)
|| filePath.startsWith(libDir)) {
Log.w(TAG, "lib, cache, code_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.FILES_TREE_TOKEN;
rootpath = filesDir;
} else if (filePath.startsWith(rootDir)) {
domain = FullBackup.ROOT_TREE_TOKEN;
rootpath = rootDir;
} else if (filePath.startsWith(deviceDbDir)) {
domain = FullBackup.DEVICE_DATABASE_TREE_TOKEN;
rootpath = deviceDbDir;
} else if (filePath.startsWith(deviceSpDir)) {
domain = FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
rootpath = deviceSpDir;
} else if (filePath.startsWith(deviceFilesDir)) {
domain = FullBackup.DEVICE_FILES_TREE_TOKEN;
rootpath = deviceFilesDir;
} else if (filePath.startsWith(deviceRootDir)) {
domain = FullBackup.DEVICE_ROOT_TREE_TOKEN;
rootpath = deviceRootDir;
} 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
// In the measurement case, backupToTar() updates the size in output and returns
// without transmitting any file data.
if (DEBUG) Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain
+ " rootpath=" + rootpath);
FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, output);
}
/**
* Scan the dir tree (if it actually exists) and process each entry we find. If the
* 'excludes' parameters are non-null, they are 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.
*
* @param systemExcludes An optional list of excludes.
* @hide
*/
protected final void fullBackupFileTree(String packageName, String domain, String startingPath,
Set
* 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.
*
* @param data A read-only file descriptor from which the agent can read {@code size}
* bytes of file data.
* @param size The number of bytes of file content to be restored to the given
* destination. If the file system object being restored is a directory, {@code size}
* will be zero.
* @param destination The File on disk to be restored with the given data.
* @param type The kind of file system object being restored. This will be either
* {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}.
* @param mode The access mode to be assigned to the destination after its data is
* written. This is in the standard format used by {@code chmod()}.
* @param mtime The modification time of the file when it was backed up, suitable to
* be assigned to the file after its data is written.
* @throws IOException
*/
public void onRestoreFile(ParcelFileDescriptor data, long size,
File destination, int type, long mode, long mtime)
throws IOException {
final boolean accept = isFileEligibleForRestore(destination);
// If we don't accept the file, consume the bytes from the pipe anyway.
FullBackup.restoreFile(data, size, type, mode, mtime, accept ? destination : null);
}
private boolean isFileEligibleForRestore(File destination) throws IOException {
FullBackup.BackupScheme bs = FullBackup.getBackupScheme(this, mBackupDestination);
if (!bs.isFullRestoreEnabled()) {
if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
Log.v(FullBackup.TAG_XML_PARSER,
"onRestoreFile \"" + destination.getCanonicalPath()
+ "\" : fullBackupContent not enabled for " + getPackageName());
}
return false;
}
Mapdata
stream.
* When a full-backup dataset is being restored, this will be null
.
*/
public abstract void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException;
/**
* New version of {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}
* that handles a long app version code. Default implementation casts the version code to
* an int and calls {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}.
*/
public void onRestore(BackupDataInput data, long appVersionCode,
ParcelFileDescriptor newState)
throws IOException {
onRestore(data, (int) appVersionCode, newState);
}
/**
* New version of {@link #onRestore(BackupDataInput, long, android.os.ParcelFileDescriptor)}
* that has a list of keys to be excluded from the restore. Key/value pairs for which the key
* is present in {@code excludedKeys} have already been excluded from the restore data by the
* system. The list is passed to the agent to make it aware of what data has been removed (in
* case it has any application-level consequences) as well as the data that should be removed
* by the agent itself.
*
* The default implementation calls {@link #onRestore(BackupDataInput, long,
* android.os.ParcelFileDescriptor)}.
*
* @param excludedKeys A list of keys to be excluded from restore.
*
* @hide
*/
public void onRestore(BackupDataInput data, long appVersionCode,
ParcelFileDescriptor newState,
Set
*
*
* domainToken
.
* If so, perform a {@link #fullBackupFileTree} which backs up the file or recurses if the path
* is a directory, but only if all the required flags of the include rule are satisfied by
* the transport.
*/
private void applyXmlFiltersAndDoFullBackupForDomain(String packageName, String domainToken,
Map> in) {
if (mLogger != null) {
in.complete(mLogger.getLoggingResults());
} else {
in.complete(Collections.emptyList());
}
}
@Override
public void getOperationType(
AndroidFuture