| /* |
| * 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.app.IBackupAgent; |
| import android.app.QueuedWork; |
| 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.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.system.StructStat; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import libcore.io.IoUtils; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.LinkedList; |
| import java.util.Map; |
| 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 <code> |
| * <a href="{@docRoot}guide/topics/manifest/application-element.html"><application></a></code> |
| * tag's {@code android:backupAgent} attribute. |
| * |
| * <div class="special reference"> |
| * <h3>Developer Guides</h3> |
| * <p>For more information about using BackupAgent, read the |
| * <a href="{@docRoot}guide/topics/data/backup.html">Data Backup</a> developer guide.</p></div> |
| * |
| * <h3>Basic Operation</h3> |
| * <p> |
| * 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. |
| * <p> |
| * 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. |
| * <p> |
| * 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. |
| * <p> |
| * 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. |
| * <p> |
| * <b>Helper Classes</b> |
| * <p> |
| * 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. |
| * <p> |
| * <b>Threading</b> |
| * <p> |
| * 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; |
| |
| /** @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. |
| * |
| * <p>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. |
| * |
| * <p>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 BackupDataOutput#getTransportFlags()} and |
| * {@link FullBackupDataOutput#getTransportFlags()} only. |
| * |
| * <p>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; |
| |
| Handler mHandler = null; |
| |
| 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 */ } |
| } |
| |
| |
| 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. |
| * <p> |
| */ |
| public void onCreate() { |
| } |
| |
| /** |
| * Provided as a convenience for agent implementations that need to do some |
| * sort of shutdown process after backup or restore is completed. |
| * <p> |
| * 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 <code>oldState</code> file |
| * descriptor. If <code>oldState</code> is <code>null</code>, 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 |
| * <code>newState</code>. |
| * <p> |
| * Each entity written to the {@link android.app.backup.BackupDataOutput} |
| * <code>data</code> 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 |
| * <code>null</code>, 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 <code>data</code> |
| * 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 <code>data</code> parameter. Once |
| * the restore is finished, the application should write a representation of |
| * the final state to the <code>newState</code> file descriptor. |
| * <p> |
| * 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 <a |
| * href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code |
| * android:versionCode}</a> 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 <code>data</code> stream. |
| * When a full-backup dataset is being restored, this will be <code>null</code>. |
| */ |
| 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); |
| } |
| |
| /** |
| * 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. |
| * |
| * <p>Certain parts of the app's data are never backed up even if the app explicitly |
| * sends them to the output: |
| * |
| * <ul> |
| * <li>The contents of the {@link #getCacheDir()} directory</li> |
| * <li>The contents of the {@link #getCodeCacheDir()} directory</li> |
| * <li>The contents of the {@link #getNoBackupFilesDir()} directory</li> |
| * <li>The contents of the app's shared library directory</li> |
| * </ul> |
| * |
| * <p>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); |
| if (!backupScheme.isFullBackupContentEnabled()) { |
| return; |
| } |
| |
| Map<String, Set<PathWithRequiredFlags>> manifestIncludeMap; |
| ArraySet<PathWithRequiredFlags> manifestExcludeSet; |
| try { |
| manifestIncludeMap = |
| backupScheme.maybeParseAndGetCanonicalIncludePaths(); |
| manifestExcludeSet = backupScheme.maybeParseAndGetCanonicalExcludePaths(); |
| } 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; |
| } |
| |
| final String packageName = getPackageName(); |
| final ApplicationInfo appInfo = getApplicationInfo(); |
| |
| // System apps have control over where their default storage context |
| // is pointed, so we're always explicit when building paths. |
| final Context ceContext = createCredentialProtectedStorageContext(); |
| final String rootDir = ceContext.getDataDir().getCanonicalPath(); |
| final String filesDir = ceContext.getFilesDir().getCanonicalPath(); |
| final String noBackupDir = ceContext.getNoBackupFilesDir().getCanonicalPath(); |
| final String databaseDir = ceContext.getDatabasePath("foo").getParentFile() |
| .getCanonicalPath(); |
| final String sharedPrefsDir = ceContext.getSharedPreferencesPath("foo").getParentFile() |
| .getCanonicalPath(); |
| final String cacheDir = ceContext.getCacheDir().getCanonicalPath(); |
| final String codeCacheDir = ceContext.getCodeCacheDir().getCanonicalPath(); |
| |
| final Context deContext = createDeviceProtectedStorageContext(); |
| final String deviceRootDir = deContext.getDataDir().getCanonicalPath(); |
| final String deviceFilesDir = deContext.getFilesDir().getCanonicalPath(); |
| final String deviceNoBackupDir = deContext.getNoBackupFilesDir().getCanonicalPath(); |
| final String deviceDatabaseDir = deContext.getDatabasePath("foo").getParentFile() |
| .getCanonicalPath(); |
| final String deviceSharedPrefsDir = deContext.getSharedPreferencesPath("foo") |
| .getParentFile().getCanonicalPath(); |
| final String deviceCacheDir = deContext.getCacheDir().getCanonicalPath(); |
| final String deviceCodeCacheDir = deContext.getCodeCacheDir().getCanonicalPath(); |
| |
| final String libDir = (appInfo.nativeLibraryDir != null) |
| ? new File(appInfo.nativeLibraryDir).getCanonicalPath() |
| : null; |
| |
| // Maintain a set of excluded directories so that as we traverse the tree we know we're not |
| // going places we don't expect, and so the manifest includes can't take precedence over |
| // what the framework decides is not to be included. |
| final ArraySet<String> traversalExcludeSet = new ArraySet<String>(); |
| |
| // Add the directories we always exclude. |
| traversalExcludeSet.add(filesDir); |
| traversalExcludeSet.add(noBackupDir); |
| traversalExcludeSet.add(databaseDir); |
| traversalExcludeSet.add(sharedPrefsDir); |
| traversalExcludeSet.add(cacheDir); |
| traversalExcludeSet.add(codeCacheDir); |
| |
| traversalExcludeSet.add(deviceFilesDir); |
| traversalExcludeSet.add(deviceNoBackupDir); |
| traversalExcludeSet.add(deviceDatabaseDir); |
| traversalExcludeSet.add(deviceSharedPrefsDir); |
| traversalExcludeSet.add(deviceCacheDir); |
| traversalExcludeSet.add(deviceCodeCacheDir); |
| |
| if (libDir != null) { |
| traversalExcludeSet.add(libDir); |
| } |
| |
| // Root dir first. |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(rootDir); |
| |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.DEVICE_ROOT_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(deviceRootDir); |
| |
| // Data dir next. |
| traversalExcludeSet.remove(filesDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.FILES_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(filesDir); |
| |
| traversalExcludeSet.remove(deviceFilesDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.DEVICE_FILES_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(deviceFilesDir); |
| |
| // Database directory. |
| traversalExcludeSet.remove(databaseDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.DATABASE_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(databaseDir); |
| |
| traversalExcludeSet.remove(deviceDatabaseDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.DEVICE_DATABASE_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(deviceDatabaseDir); |
| |
| // SharedPrefs. |
| traversalExcludeSet.remove(sharedPrefsDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(sharedPrefsDir); |
| |
| traversalExcludeSet.remove(deviceSharedPrefsDir); |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| traversalExcludeSet.add(deviceSharedPrefsDir); |
| |
| // 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) { |
| applyXmlFiltersAndDoFullBackupForDomain( |
| packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, manifestIncludeMap, |
| manifestExcludeSet, traversalExcludeSet, data); |
| } |
| |
| } |
| } |
| |
| /** |
| * Notification that the application's current backup operation causes it to exceed |
| * the maximum size permitted by the transport. The ongoing backup operation is |
| * halted and rolled back: any data that had been stored by a previous backup operation |
| * is still intact. Typically the quota-exceeded state will be detected before any data |
| * is actually transmitted over the network. |
| * |
| * <p>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. |
| * |
| * <p class="note">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) { |
| } |
| |
| /** |
| * Check whether the xml yielded any <include/> tag for the provided <code>domainToken</code>. |
| * 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<String, Set<PathWithRequiredFlags>> includeMap, |
| ArraySet<PathWithRequiredFlags> filterSet, ArraySet<String> traversalExcludeSet, |
| FullBackupDataOutput data) throws IOException { |
| if (includeMap == null || includeMap.size() == 0) { |
| // Do entire sub-tree for the provided token. |
| fullBackupFileTree(packageName, domainToken, |
| FullBackup.getBackupScheme(this).tokenToDirectoryPath(domainToken), |
| filterSet, traversalExcludeSet, data); |
| } else if (includeMap.get(domainToken) != null) { |
| // This will be null if the xml parsing didn't yield any rules for |
| // this domain (there may still be rules for other domains). |
| for (PathWithRequiredFlags includeFile : includeMap.get(domainToken)) { |
| if (areIncludeRequiredTransportFlagsSatisfied(includeFile.getRequiredFlags(), |
| data.getTransportFlags())) { |
| fullBackupFileTree(packageName, domainToken, includeFile.getPath(), filterSet, |
| traversalExcludeSet, data); |
| } |
| } |
| } |
| } |
| |
| private boolean areIncludeRequiredTransportFlagsSatisfied(int includeFlags, |
| int transportFlags) { |
| // all bits that are set in includeFlags must also be set in transportFlags |
| return (transportFlags & includeFlags) == includeFlags; |
| } |
| |
| /** |
| * 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. |
| * |
| * <p class="note">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, |
| ArraySet<PathWithRequiredFlags> manifestExcludes, |
| ArraySet<String> systemExcludes, |
| FullBackupDataOutput output) { |
| // Pull out the domain and set it aside to use when making the tarball. |
| String domainPath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain); |
| if (domainPath == null) { |
| // Should never happen. |
| return; |
| } |
| |
| File rootFile = new File(startingPath); |
| if (rootFile.exists()) { |
| LinkedList<File> scanQueue = new LinkedList<File>(); |
| scanQueue.add(rootFile); |
| |
| while (scanQueue.size() > 0) { |
| File file = scanQueue.remove(0); |
| String filePath; |
| try { |
| // Ignore things that aren't "real" files or dirs |
| StructStat stat = Os.lstat(file.getPath()); |
| if (!OsConstants.S_ISREG(stat.st_mode) |
| && !OsConstants.S_ISDIR(stat.st_mode)) { |
| if (DEBUG) Log.i(TAG, "Not a file/dir (skipping)!: " + file); |
| continue; |
| } |
| |
| // For all other verification, look at the canonicalized path |
| filePath = file.getCanonicalPath(); |
| |
| // prune this subtree? |
| if (manifestExcludes != null |
| && manifestExcludesContainFilePath(manifestExcludes, filePath)) { |
| continue; |
| } |
| if (systemExcludes != null && systemExcludes.contains(filePath)) { |
| continue; |
| } |
| |
| // If it's a directory, enqueue its contents for scanning. |
| 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); |
| if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { |
| Log.v(FullBackup.TAG_XML_PARSER, "Error canonicalizing path of " + file); |
| } |
| continue; |
| } catch (ErrnoException e) { |
| if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); |
| if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { |
| Log.v(FullBackup.TAG_XML_PARSER, "Error scanning file " + file + " : " + e); |
| } |
| continue; |
| } |
| |
| // Finally, back this file up (or measure it) before proceeding |
| FullBackup.backupToTar(packageName, domain, null, domainPath, filePath, output); |
| } |
| } |
| } |
| |
| private boolean manifestExcludesContainFilePath( |
| ArraySet<PathWithRequiredFlags> manifestExcludes, String filePath) { |
| for (PathWithRequiredFlags exclude : manifestExcludes) { |
| String excludePath = exclude.getPath(); |
| if (excludePath != null && excludePath.equals(filePath)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * 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. |
| * <p> |
| * The file descriptor can only be read for {@code size} bytes; attempting to read |
| * more data has undefined behavior. |
| * <p> |
| * 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); |
| if (!bs.isFullBackupContentEnabled()) { |
| 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; |
| } |
| |
| Map<String, Set<PathWithRequiredFlags>> includes = null; |
| ArraySet<PathWithRequiredFlags> excludes = null; |
| final String destinationCanonicalPath = destination.getCanonicalPath(); |
| try { |
| includes = bs.maybeParseAndGetCanonicalIncludePaths(); |
| excludes = bs.maybeParseAndGetCanonicalExcludePaths(); |
| } catch (XmlPullParserException e) { |
| if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { |
| Log.v(FullBackup.TAG_XML_PARSER, |
| "onRestoreFile \"" + destinationCanonicalPath |
| + "\" : Exception trying to parse fullBackupContent xml file!" |
| + " Aborting onRestoreFile.", e); |
| } |
| return false; |
| } |
| |
| if (excludes != null && |
| BackupUtils.isFileSpecifiedInPathList(destination, excludes)) { |
| if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { |
| Log.v(FullBackup.TAG_XML_PARSER, |
| "onRestoreFile: \"" + destinationCanonicalPath + "\": listed in" |
| + " excludes; skipping."); |
| } |
| return false; |
| } |
| |
| if (includes != null && !includes.isEmpty()) { |
| // Rather than figure out the <include/> domain based on the path (a lot of code, and |
| // it's a small list), we'll go through and look for it. |
| boolean explicitlyIncluded = false; |
| for (Set<PathWithRequiredFlags> domainIncludes : includes.values()) { |
| explicitlyIncluded |= |
| BackupUtils.isFileSpecifiedInPathList(destination, domainIncludes); |
| if (explicitlyIncluded) { |
| break; |
| } |
| } |
| if (!explicitlyIncluded) { |
| if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { |
| Log.v(FullBackup.TAG_XML_PARSER, |
| "onRestoreFile: Trying to restore \"" |
| + destinationCanonicalPath + "\" but it isn't specified" |
| + " in the included files; skipping."); |
| } |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Only specialized platform agents should overload this entry point to support |
| * restores to crazy non-app locations. |
| * @hide |
| */ |
| protected void onRestoreFile(ParcelFileDescriptor data, long size, |
| int type, String domain, String path, long mode, long mtime) |
| throws IOException { |
| String basePath = null; |
| |
| if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type |
| + " domain=" + domain + " relpath=" + path + " mode=" + mode |
| + " mtime=" + mtime); |
| |
| basePath = FullBackup.getBackupScheme(this).tokenToDirectoryPath(domain); |
| if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { |
| mode = -1; // < 0 is a token to skip attempting a chmod() |
| } |
| |
| // 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); |
| } |
| |
| /** |
| * 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. |
| * |
| * @see #onRestore(BackupDataInput, int, ParcelFileDescriptor) |
| * @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) |
| */ |
| public void onRestoreFinished() { |
| } |
| |
| // ----- Core implementation ----- |
| |
| /** @hide */ |
| public final IBinder onBind() { |
| return mBinder; |
| } |
| |
| private final IBinder mBinder = new BackupServiceBinder().asBinder(); |
| |
| /** @hide */ |
| public void attach(Context context) { |
| attachBaseContext(context); |
| } |
| |
| // ----- IBackupService binder interface ----- |
| private class BackupServiceBinder extends IBackupAgent.Stub { |
| private static final String TAG = "BackupServiceBinder"; |
| |
| @Override |
| public void doBackup(ParcelFileDescriptor oldState, |
| ParcelFileDescriptor data, |
| ParcelFileDescriptor newState, |
| long quotaBytes, int token, IBackupManager callbackBinder, int transportFlags) |
| throws RemoteException { |
| // Ensure that we're running with the app's normal permission level |
| long ident = Binder.clearCallingIdentity(); |
| |
| if (DEBUG) Log.v(TAG, "doBackup() invoked"); |
| BackupDataOutput output = new BackupDataOutput( |
| data.getFileDescriptor(), quotaBytes, transportFlags); |
| |
| try { |
| BackupAgent.this.onBackup(oldState, output, newState); |
| } catch (IOException ex) { |
| Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw new RuntimeException(ex); |
| } catch (RuntimeException ex) { |
| Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw ex; |
| } finally { |
| // Ensure that any SharedPreferences writes have landed after the backup, |
| // in case the app code has side effects (since apps cannot provide this |
| // guarantee themselves). |
| waitForSharedPrefs(); |
| |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, 0); |
| } catch (RemoteException e) { |
| // we'll time out anyway, so we're safe |
| } |
| |
| // Don't close the fd out from under the system service if this was local |
| if (Binder.getCallingPid() != Process.myPid()) { |
| IoUtils.closeQuietly(oldState); |
| IoUtils.closeQuietly(data); |
| IoUtils.closeQuietly(newState); |
| } |
| } |
| } |
| |
| @Override |
| public void doRestore(ParcelFileDescriptor data, long appVersionCode, |
| ParcelFileDescriptor newState, |
| int token, IBackupManager callbackBinder) throws RemoteException { |
| // Ensure that we're running with the app's normal permission level |
| long ident = Binder.clearCallingIdentity(); |
| |
| if (DEBUG) Log.v(TAG, "doRestore() invoked"); |
| |
| // Ensure that any side-effect SharedPreferences writes have landed *before* |
| // we may be about to rewrite the file out from underneath |
| waitForSharedPrefs(); |
| |
| BackupDataInput input = new BackupDataInput(data.getFileDescriptor()); |
| try { |
| BackupAgent.this.onRestore(input, appVersionCode, newState); |
| } catch (IOException ex) { |
| Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw new RuntimeException(ex); |
| } catch (RuntimeException ex) { |
| Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw ex; |
| } finally { |
| // And bring live SharedPreferences instances up to date |
| reloadSharedPreferences(); |
| |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, 0); |
| } catch (RemoteException e) { |
| // we'll time out anyway, so we're safe |
| } |
| |
| if (Binder.getCallingPid() != Process.myPid()) { |
| IoUtils.closeQuietly(data); |
| IoUtils.closeQuietly(newState); |
| } |
| } |
| } |
| |
| @Override |
| public void doFullBackup(ParcelFileDescriptor data, |
| long quotaBytes, int token, IBackupManager callbackBinder, int transportFlags) { |
| // Ensure that we're running with the app's normal permission level |
| long ident = Binder.clearCallingIdentity(); |
| |
| if (DEBUG) Log.v(TAG, "doFullBackup() invoked"); |
| |
| // Ensure that any SharedPreferences writes have landed *before* |
| // we potentially try to back up the underlying files directly. |
| waitForSharedPrefs(); |
| |
| try { |
| BackupAgent.this.onFullBackup(new FullBackupDataOutput( |
| data, quotaBytes, transportFlags)); |
| } catch (IOException ex) { |
| Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw new RuntimeException(ex); |
| } catch (RuntimeException ex) { |
| Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw ex; |
| } finally { |
| // ... and then again after, as in the doBackup() case |
| waitForSharedPrefs(); |
| |
| // Send the EOD marker indicating that there is no more data |
| // forthcoming from this agent. |
| try { |
| FileOutputStream out = new FileOutputStream(data.getFileDescriptor()); |
| byte[] buf = new byte[4]; |
| out.write(buf); |
| } catch (IOException e) { |
| Log.e(TAG, "Unable to finalize backup stream!"); |
| } |
| |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, 0); |
| } catch (RemoteException e) { |
| // we'll time out anyway, so we're safe |
| } |
| |
| if (Binder.getCallingPid() != Process.myPid()) { |
| IoUtils.closeQuietly(data); |
| } |
| } |
| } |
| |
| public void doMeasureFullBackup(long quotaBytes, int token, IBackupManager callbackBinder, |
| int transportFlags) { |
| // Ensure that we're running with the app's normal permission level |
| final long ident = Binder.clearCallingIdentity(); |
| FullBackupDataOutput measureOutput = |
| new FullBackupDataOutput(quotaBytes, transportFlags); |
| |
| waitForSharedPrefs(); |
| try { |
| BackupAgent.this.onFullBackup(measureOutput); |
| } catch (IOException ex) { |
| Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw new RuntimeException(ex); |
| } catch (RuntimeException ex) { |
| Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex); |
| throw ex; |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, measureOutput.getSize()); |
| } catch (RemoteException e) { |
| // timeout, so we're safe |
| } |
| } |
| } |
| |
| @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 { |
| BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime); |
| } catch (IOException e) { |
| Log.d(TAG, "onRestoreFile (" + BackupAgent.this.getClass().getName() + ") threw", e); |
| throw new RuntimeException(e); |
| } finally { |
| // Ensure that any side-effect SharedPreferences writes have landed |
| waitForSharedPrefs(); |
| // And bring live SharedPreferences instances up to date |
| reloadSharedPreferences(); |
| |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, 0); |
| } catch (RemoteException e) { |
| // we'll time out anyway, so we're safe |
| } |
| |
| if (Binder.getCallingPid() != Process.myPid()) { |
| IoUtils.closeQuietly(data); |
| } |
| } |
| } |
| |
| @Override |
| public void doRestoreFinished(int token, IBackupManager callbackBinder) { |
| long ident = Binder.clearCallingIdentity(); |
| try { |
| BackupAgent.this.onRestoreFinished(); |
| } catch (Exception e) { |
| Log.d(TAG, "onRestoreFinished (" + BackupAgent.this.getClass().getName() + ") threw", e); |
| throw e; |
| } finally { |
| // Ensure that any side-effect SharedPreferences writes have landed |
| waitForSharedPrefs(); |
| |
| Binder.restoreCallingIdentity(ident); |
| try { |
| callbackBinder.opComplete(token, 0); |
| } catch (RemoteException e) { |
| // we'll time out anyway, so we're safe |
| } |
| } |
| } |
| |
| @Override |
| public void fail(String message) { |
| getHandler().post(new FailRunnable(message)); |
| } |
| |
| @Override |
| public void doQuotaExceeded(long backupDataBytes, long quotaBytes) { |
| long ident = Binder.clearCallingIdentity(); |
| try { |
| BackupAgent.this.onQuotaExceeded(backupDataBytes, quotaBytes); |
| } catch (Exception e) { |
| Log.d(TAG, "onQuotaExceeded(" + BackupAgent.this.getClass().getName() + ") threw", |
| e); |
| throw e; |
| } finally { |
| waitForSharedPrefs(); |
| Binder.restoreCallingIdentity(ident); |
| } |
| } |
| } |
| |
| static class FailRunnable implements Runnable { |
| private String mMessage; |
| |
| FailRunnable(String message) { |
| mMessage = message; |
| } |
| |
| @Override |
| public void run() { |
| throw new IllegalStateException(mMessage); |
| } |
| } |
| } |