diff options
author | Matthew Williams <mjwilliams@google.com> | 2015-04-17 18:22:51 -0700 |
---|---|---|
committer | Matthew Williams <mjwilliams@google.com> | 2015-05-03 16:19:27 -0700 |
commit | 303650c9cdb7cec88e7ec20747b161d9fff10719 (patch) | |
tree | 310f95b0e6ff91830cebfdc015ee158015fc5d27 /core | |
parent | ca030f8ed5fd52f2821d159b9c16d0c514dc0688 (diff) | |
download | frameworks_base-303650c9cdb7cec88e7ec20747b161d9fff10719.zip frameworks_base-303650c9cdb7cec88e7ec20747b161d9fff10719.tar.gz frameworks_base-303650c9cdb7cec88e7ec20747b161d9fff10719.tar.bz2 |
Add full backup criteria to android manifest
BUG: 20010079
Api change: ApplicationInfo now has a fullBackupContent int
where -1 is (off) 0 is (on) and >0 indicates an xml
resource that should be parsed in order for a developer
to indicate exactly which files they want to include/exclude
from the backup set.
dd: https://docs.google.com/document/d/1dnNctwhWOI-_qtZ7I3iNRtrbShmERj2GFTzwV4xXtOk/edit#heading=h.wcfw1q2pbmae
Change-Id: I90273dc0aef5e9a3230c6b074a45e8f5409ed5ce
Diffstat (limited to 'core')
-rw-r--r-- | core/java/android/app/backup/BackupAgent.java | 276 | ||||
-rw-r--r-- | core/java/android/app/backup/FullBackup.java | 401 | ||||
-rw-r--r-- | core/java/android/content/pm/ApplicationInfo.java | 23 | ||||
-rw-r--r-- | core/java/android/content/pm/PackageParser.java | 18 | ||||
-rw-r--r-- | core/res/res/values/attrs_manifest.xml | 6 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 1 | ||||
-rw-r--r-- | core/tests/coretests/src/android/app/backup/FullBackupTest.java | 258 |
7 files changed, 916 insertions, 67 deletions
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index d8556a2..6fca0de 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -33,15 +33,21 @@ 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 java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.util.HashSet; +import java.util.Collection; import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; +import org.xmlpull.v1.XmlPullParserException; + /** * Provides the central interface between an * application and Android's data backup infrastructure. An application that wishes @@ -164,7 +170,6 @@ public abstract class BackupAgent extends ContextWrapper { * to do one-time initialization before the actual backup or restore operation * is begun. * <p> - * Agents do not need to override this method. */ public void onCreate() { } @@ -268,19 +273,41 @@ public abstract class BackupAgent extends ContextWrapper { * 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 ApplicationInfo#fullBackupContent * @see #fullBackupFile(File, FullBackupDataOutput) * @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long) */ public void onFullBackup(FullBackupDataOutput data) throws IOException { - ApplicationInfo appInfo = getApplicationInfo(); + FullBackup.BackupScheme backupScheme = FullBackup.getBackupScheme(this); + if (!backupScheme.isFullBackupContentEnabled()) { + return; + } + + Map<String, Set<String>> manifestIncludeMap; + ArraySet<String> 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(); - // 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 nobackupDir = getNoBackupFilesDir().getCanonicalPath(); @@ -292,34 +319,49 @@ public abstract class BackupAgent extends ContextWrapper { ? 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(); + // 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>(); - // Okay, start with the app's root tree, but exclude all of the canonical subdirs + // Add the directories we always exclude. + traversalExcludeSet.add(cacheDir); + traversalExcludeSet.add(codeCacheDir); + traversalExcludeSet.add(nobackupDir); if (libDir != null) { - filterSet.add(libDir); + traversalExcludeSet.add(libDir); } - filterSet.add(cacheDir); - filterSet.add(codeCacheDir); - filterSet.add(databaseDir); - filterSet.add(sharedPrefsDir); - filterSet.add(filesDir); - filterSet.add(nobackupDir); - 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); + + traversalExcludeSet.add(databaseDir); + traversalExcludeSet.add(sharedPrefsDir); + traversalExcludeSet.add(filesDir); + + // Root dir first. + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.ROOT_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(rootDir); + + // Data dir next. + traversalExcludeSet.remove(filesDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.DATA_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(filesDir); + + // Database directory. + traversalExcludeSet.remove(databaseDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.DATABASE_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(databaseDir); + + // SharedPrefs. + traversalExcludeSet.remove(sharedPrefsDir); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + traversalExcludeSet.add(sharedPrefsDir); // getExternalFilesDir() location associated with this app. Technically there should // not be any files here if the app does not properly have permission to access @@ -331,8 +373,36 @@ public abstract class BackupAgent extends ContextWrapper { if (Process.myUid() != Process.SYSTEM_UID) { File efLocation = getExternalFilesDir(null); if (efLocation != null) { - fullBackupFileTree(packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, - efLocation.getCanonicalPath(), null, data); + applyXmlFiltersAndDoFullBackupForDomain( + packageName, FullBackup.MANAGED_EXTERNAL_TREE_TOKEN, manifestIncludeMap, + manifestExcludeSet, traversalExcludeSet, data); + } + + } + } + + /** + * 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. + */ + private void applyXmlFiltersAndDoFullBackupForDomain(String packageName, String domainToken, + Map<String, Set<String>> includeMap, + ArraySet<String> 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 (String includeFile : includeMap.get(domainToken)) { + fullBackupFileTree(packageName, domainToken, includeFile, filterSet, + traversalExcludeSet, data); } } } @@ -430,21 +500,31 @@ public abstract class BackupAgent extends ContextWrapper { // 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' parameter is non-null, it is consulted each time a new file system entity + * '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 rootPath, - HashSet<String> excludes, FullBackupDataOutput output) { - File rootFile = new File(rootPath); + protected final void fullBackupFileTree(String packageName, String domain, String startingPath, + ArraySet<String> 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); @@ -456,7 +536,10 @@ public abstract class BackupAgent extends ContextWrapper { filePath = file.getCanonicalPath(); // prune this subtree? - if (excludes != null && excludes.contains(filePath)) { + if (manifestExcludes != null && manifestExcludes.contains(filePath)) { + continue; + } + if (systemExcludes != null && systemExcludes.contains(filePath)) { continue; } @@ -475,14 +558,20 @@ public abstract class BackupAgent extends ContextWrapper { } } 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, rootPath, filePath, output); + FullBackup.backupToTar(packageName, domain, null, domainPath, filePath, output); } } } @@ -516,10 +605,91 @@ public abstract class BackupAgent extends ContextWrapper { public void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) 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; + } + Map<String, Set<String>> includes = null; + ArraySet<String> 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; + } + + if (excludes != null && + 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; + } + + 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<String> domainIncludes : includes.values()) { + explicitlyIncluded |= 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; + } + } FullBackup.restoreFile(data, size, type, mode, mtime, destination); } /** + * @return True if the provided file is either directly in the provided list, or the provided + * file is within a directory in the list. + */ + private boolean isFileSpecifiedInPathList(File file, Collection<String> canonicalPathList) + throws IOException { + for (String canonicalPath : canonicalPathList) { + File fileFromList = new File(canonicalPath); + if (fileFromList.isDirectory()) { + if (file.isDirectory()) { + // If they are both directories check exact equals. + return file.equals(fileFromList); + } else { + // O/w we have to check if the file is within the directory from the list. + return file.getCanonicalPath().startsWith(canonicalPath); + } + } else { + if (file.equals(fileFromList)) { + // Need to check the explicit "equals" so we don't end up with substrings. + return true; + } + } + } + return false; + } + + /** * Only specialized platform agents should overload this entry point to support * restores to crazy non-app locations. * @hide @@ -533,31 +703,9 @@ public abstract class BackupAgent extends ContextWrapper { + " 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); + 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 diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index 259884e..7718a36 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -16,16 +16,31 @@ package android.app.backup; -import android.os.ParcelFileDescriptor; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.*; +import android.os.Process; import android.system.ErrnoException; import android.system.Os; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + +import org.xmlpull.v1.XmlPullParser; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.xmlpull.v1.XmlPullParserException; /** * Global constant definitions et cetera related to the full-backup-to-fd * binary format. Nothing in this namespace is part of any API; it's all @@ -35,6 +50,8 @@ import java.io.IOException; */ public class FullBackup { static final String TAG = "FullBackup"; + /** Enable this log tag to get verbose information while parsing the client xml. */ + static final String TAG_XML_PARSER = "BackupXmlParserLogging"; public static final String APK_TREE_TOKEN = "a"; public static final String OBB_TREE_TOKEN = "obb"; @@ -60,6 +77,27 @@ public class FullBackup { static public native int backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output); + private static final Map<String, BackupScheme> kPackageBackupSchemeMap = + new ArrayMap<String, BackupScheme>(); + + static synchronized BackupScheme getBackupScheme(Context context) { + BackupScheme backupSchemeForPackage = + kPackageBackupSchemeMap.get(context.getPackageName()); + if (backupSchemeForPackage == null) { + backupSchemeForPackage = new BackupScheme(context); + kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage); + } + return backupSchemeForPackage; + } + + public static BackupScheme getBackupSchemeForTest(Context context) { + BackupScheme testing = new BackupScheme(context); + testing.mExcludes = new ArraySet(); + testing.mIncludes = new ArrayMap(); + return testing; + } + + /** * Copy data from a socket to the given File location on permanent storage. The * modification time and access mode of the resulting file will be set if desired, @@ -106,6 +144,8 @@ public class FullBackup { if (!parent.exists()) { // in practice this will only be for the default semantic directories, // and using the default mode for those is appropriate. + // This can also happen for the case where a parent directory has been + // excluded, but a file within that directory has been included. parent.mkdirs(); } out = new FileOutputStream(outFile); @@ -154,4 +194,363 @@ public class FullBackup { outFile.setLastModified(mtime); } } + + @VisibleForTesting + public static class BackupScheme { + private final File FILES_DIR; + private final File DATABASE_DIR; + private final File ROOT_DIR; + private final File SHAREDPREF_DIR; + private final File EXTERNAL_DIR; + private final File CACHE_DIR; + private final File NOBACKUP_DIR; + + final int mFullBackupContent; + final PackageManager mPackageManager; + final String mPackageName; + + /** + * Parse out the semantic domains into the correct physical location. + */ + String tokenToDirectoryPath(String domainToken) { + try { + if (domainToken.equals(FullBackup.DATA_TREE_TOKEN)) { + return FILES_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) { + return DATABASE_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) { + return ROOT_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { + return SHAREDPREF_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) { + return CACHE_DIR.getCanonicalPath(); + } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) { + if (EXTERNAL_DIR != null) { + return EXTERNAL_DIR.getCanonicalPath(); + } else { + return null; + } + } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) { + return NOBACKUP_DIR.getCanonicalPath(); + } + // Not a supported location + Log.i(TAG, "Unrecognized domain " + domainToken); + return null; + } catch (IOException e) { + Log.i(TAG, "Error reading directory for domain: " + domainToken); + return null; + } + + } + /** + * A map of domain -> list of canonical file names in that domain that are to be included. + * We keep track of the domain so that we can go through the file system in order later on. + */ + Map<String, Set<String>> mIncludes; + /**e + * List that will be populated with the canonical names of each file or directory that is + * to be excluded. + */ + ArraySet<String> mExcludes; + + BackupScheme(Context context) { + mFullBackupContent = context.getApplicationInfo().fullBackupContent; + mPackageManager = context.getPackageManager(); + mPackageName = context.getPackageName(); + FILES_DIR = context.getFilesDir(); + DATABASE_DIR = context.getDatabasePath("foo").getParentFile(); + ROOT_DIR = new File(context.getApplicationInfo().dataDir); + SHAREDPREF_DIR = context.getSharedPrefsFile("foo").getParentFile(); + CACHE_DIR = context.getCacheDir(); + NOBACKUP_DIR = context.getNoBackupFilesDir(); + if (android.os.Process.myUid() != Process.SYSTEM_UID) { + EXTERNAL_DIR = context.getExternalFilesDir(null); + } else { + EXTERNAL_DIR = null; + } + } + + boolean isFullBackupContentEnabled() { + if (mFullBackupContent < 0) { + // android:fullBackupContent="false", bail. + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\""); + } + return false; + } + return true; + } + + /** + * @return A mapping of domain -> canonical paths within that domain. Each of these paths + * specifies a file that the client has explicitly included in their backup set. If this + * map is empty we will back up the entire data directory (including managed external + * storage). + */ + public synchronized Map<String, Set<String>> maybeParseAndGetCanonicalIncludePaths() + throws IOException, XmlPullParserException { + if (mIncludes == null) { + maybeParseBackupSchemeLocked(); + } + return mIncludes; + } + + /** + * @return A set of canonical paths that are to be excluded from the backup/restore set. + */ + public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths() + throws IOException, XmlPullParserException { + if (mExcludes == null) { + maybeParseBackupSchemeLocked(); + } + return mExcludes; + } + + private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException { + // This not being null is how we know that we've tried to parse the xml already. + mIncludes = new ArrayMap<String, Set<String>>(); + mExcludes = new ArraySet<String>(); + + if (mFullBackupContent == 0) { + // android:fullBackupContent="true" which means that we'll do everything. + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\""); + } + } else { + // android:fullBackupContent="@xml/some_resource". + if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(FullBackup.TAG_XML_PARSER, + "android:fullBackupContent - found xml resource"); + } + XmlResourceParser parser = null; + try { + parser = mPackageManager + .getResourcesForApplication(mPackageName) + .getXml(mFullBackupContent); + parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes); + } catch (PackageManager.NameNotFoundException e) { + // Throw it as an IOException + throw new IOException(e); + } finally { + if (parser != null) { + parser.close(); + } + } + } + } + + @VisibleForTesting + public void parseBackupSchemeFromXmlLocked(XmlPullParser parser, + Set<String> excludes, + Map<String, Set<String>> includes) + throws IOException, XmlPullParserException { + int event = parser.getEventType(); // START_DOCUMENT + while (event != XmlPullParser.START_TAG) { + event = parser.next(); + } + + if (!"full-backup-content".equals(parser.getName())) { + throw new XmlPullParserException("Xml file didn't start with correct tag" + + " (<full-backup-content>). Found \"" + parser.getName() + "\""); + } + + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "\n"); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource."); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, ""); + } + + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { + switch (event) { + case XmlPullParser.START_TAG: + validateInnerTagContents(parser); + final String domainFromXml = parser.getAttributeValue(null, "domain"); + final File domainDirectory = + getDirectoryForCriteriaDomain(domainFromXml); + if (domainDirectory == null) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": " + + "domain=\"" + domainFromXml + "\" invalid; skipping"); + } + break; + } + final File canonicalFile = + extractCanonicalFile(domainDirectory, + parser.getAttributeValue(null, "path")); + if (canonicalFile == null) { + break; + } + + Set<String> activeSet = parseCurrentTagForDomain( + parser, excludes, includes, domainFromXml); + activeSet.add(canonicalFile.getCanonicalPath()); + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath() + + " for domain \"" + domainFromXml + "\""); + } + + // Special case journal files (not dirs) for sqlite database. frowny-face. + // Note that for a restore, the file is never a directory (b/c it doesn't + // exist). We have no way of knowing a priori whether or not to expect a + // dir, so we add the -journal anyway to be safe. + if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) { + final String canonicalJournalPath = + canonicalFile.getCanonicalPath() + "-journal"; + activeSet.add(canonicalJournalPath); + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...automatically generated " + + canonicalJournalPath + ". Ignore if nonexistant."); + } + } + } + } + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "\n"); + Log.v(TAG_XML_PARSER, "Xml resource parsing complete."); + Log.v(TAG_XML_PARSER, "Final tally."); + Log.v(TAG_XML_PARSER, "Includes:"); + if (includes.isEmpty()) { + Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app" + + " data minus excludes)"); + } else { + for (Map.Entry<String, Set<String>> entry : includes.entrySet()) { + Log.v(TAG_XML_PARSER, " domain=" + entry.getKey()); + for (String includeData : entry.getValue()) { + Log.v(TAG_XML_PARSER, " " + includeData); + } + } + } + + Log.v(TAG_XML_PARSER, "Excludes:"); + if (excludes.isEmpty()) { + Log.v(TAG_XML_PARSER, " ...nothing to exclude."); + } else { + for (String excludeData : excludes) { + Log.v(TAG_XML_PARSER, " " + excludeData); + } + } + + Log.v(TAG_XML_PARSER, " "); + Log.v(TAG_XML_PARSER, "===================================================="); + Log.v(TAG_XML_PARSER, "\n"); + } + } + + private Set<String> parseCurrentTagForDomain(XmlPullParser parser, + Set<String> excludes, + Map<String, Set<String>> includes, + String domain) + throws XmlPullParserException { + if ("include".equals(parser.getName())) { + final String domainToken = getTokenForXmlDomain(domain); + Set<String> includeSet = includes.get(domainToken); + if (includeSet == null) { + includeSet = new ArraySet<String>(); + includes.put(domainToken, includeSet); + } + return includeSet; + } else if ("exclude".equals(parser.getName())) { + return excludes; + } else { + // Unrecognised tag => hard failure. + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "Invalid tag found in xml \"" + + parser.getName() + "\"; aborting operation."); + } + throw new XmlPullParserException("Unrecognised tag in backup" + + " criteria xml (" + parser.getName() + ")"); + } + } + + /** + * Map xml specified domain (human-readable, what clients put in their manifest's xml) to + * BackupAgent internal data token. + * @return null if the xml domain was invalid. + */ + private String getTokenForXmlDomain(String xmlDomain) { + if ("root".equals(xmlDomain)) { + return FullBackup.ROOT_TREE_TOKEN; + } else if ("file".equals(xmlDomain)) { + return FullBackup.DATA_TREE_TOKEN; + } else if ("database".equals(xmlDomain)) { + return FullBackup.DATABASE_TREE_TOKEN; + } else if ("sharedpref".equals(xmlDomain)) { + return FullBackup.SHAREDPREFS_TREE_TOKEN; + } else if ("external".equals(xmlDomain)) { + return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN; + } else { + return null; + } + } + + /** + * + * @param domain Directory where the specified file should exist. Not null. + * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may be + * null. + * @return The canonical path of the file specified or null if no such file exists. + */ + private File extractCanonicalFile(File domain, String filePathFromXml) { + if (filePathFromXml == null) { + // Allow things like <include domain="sharedpref"/> + filePathFromXml = ""; + } + if (filePathFromXml.contains("..")) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml + + "\", but the \"..\" path is not permitted; skipping."); + } + return null; + } + if (filePathFromXml.contains("//")) { + if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { + Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml + + "\", which contains the invalid \"//\" sequence; skipping."); + } + return null; + } + return new File(domain, filePathFromXml); + } + + /** + * @param domain parsed from xml. Not sanitised before calling this function so may be null. + * @return The directory relevant to the domain specified. + */ + private File getDirectoryForCriteriaDomain(String domain) { + if (TextUtils.isEmpty(domain)) { + return null; + } + if ("file".equals(domain)) { + return FILES_DIR; + } else if ("database".equals(domain)) { + return DATABASE_DIR; + } else if ("root".equals(domain)) { + return ROOT_DIR; + } else if ("sharedpref".equals(domain)) { + return SHAREDPREF_DIR; + } else if ("external".equals(domain)) { + return EXTERNAL_DIR; + } else { + return null; + } + } + + /** + * Let's be strict about the type of xml the client can write. If we see anything untoward, + * throw an XmlPullParserException. + */ + private void validateInnerTagContents(XmlPullParser parser) + throws XmlPullParserException { + if (parser.getAttributeCount() > 2) { + throw new XmlPullParserException("At most 2 tag attributes allowed for \"" + + parser.getName() + "\" tag (\"domain\" & \"path\"."); + } + if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) { + throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" + + " \"<exclude/>. You provided \"" + parser.getName() + "\""); + } + } + } } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 6c32873..707ef30 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -96,6 +96,21 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public String backupAgentName; /** + * An optional attribute that indicates the app supports automatic backup of app data. + * <p>0 is the default and means the app's entire data folder + managed external storage will + * be backed up; + * Any negative value indicates the app does not support full-data backup, though it may still + * want to participate via the traditional key/value backup API; + * A positive number specifies an xml resource in which the application has defined its backup + * include/exclude criteria. + * <p>If android:allowBackup is set to false, this attribute is ignored. + * + * @see {@link android.content.Context#getNoBackupFilesDir} + * @see {@link #FLAG_ALLOW_BACKUP} + */ + public int fullBackupContent = 0; + + /** * The default extra UI options for activities in this application. * Set from the {@link android.R.attr#uiOptions} attribute in the * activity's manifest. @@ -686,6 +701,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "uiOptions=0x" + Integer.toHexString(uiOptions)); } pw.println(prefix + "supportsRtl=" + (hasRtlSupport() ? "true" : "false")); + if (fullBackupContent > 0) { + pw.println(prefix + "fullBackupContent=@xml/" + fullBackupContent); + } else { + pw.println(prefix + "fullBackupContent=" + (fullBackupContent < 0 ? "false" : "true")); + } super.dumpBack(pw, prefix); } @@ -763,6 +783,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uiOptions = orig.uiOptions; backupAgentName = orig.backupAgentName; hardwareAccelerated = orig.hardwareAccelerated; + fullBackupContent = orig.fullBackupContent; } @@ -816,6 +837,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(descriptionRes); dest.writeInt(uiOptions); dest.writeInt(hardwareAccelerated ? 1 : 0); + dest.writeInt(fullBackupContent); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -868,6 +890,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { descriptionRes = source.readInt(); uiOptions = source.readInt(); hardwareAccelerated = source.readInt() != 0; + fullBackupContent = source.readInt(); } /** diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 9596c42..acc27c3 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2421,8 +2421,8 @@ public class PackageParser { if (allowBackup) { ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; - // backupAgent, killAfterRestore, and restoreAnyVersion are only relevant - // if backup is possible for the given application. + // backupAgent, killAfterRestore, fullBackupContent and restoreAnyVersion are only + // relevant if backup is possible for the given application. String backupAgent = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_backupAgent, Configuration.NATIVE_CONFIG_VERSION); @@ -2449,6 +2449,20 @@ public class PackageParser { ai.flags |= ApplicationInfo.FLAG_FULL_BACKUP_ONLY; } } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupContent); + if (v != null && (ai.fullBackupContent = v.resourceId) == 0) { + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent specified as boolean=" + + (v.data == 0 ? "false" : "true")); + } + // "false" => -1, "true" => 0 + ai.fullBackupContent = (v.data == 0 ? -1 : 0); + } + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent=" + ai.fullBackupContent + " for " + pkgName); + } } TypedValue v = sa.peekValue( diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 4631427..59c6e4f 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -842,6 +842,11 @@ via adb. The default value of this attribute is <code>true</code>. --> <attr name="allowBackup" format="boolean" /> + <!-- Applications will set this in their manifest to opt-in to or out of full app data back-up + and restore. Alternatively they can set it to an xml resource within their app that will + be parsed by the BackupAgent to selectively backup files indicated within that xml. --> + <attr name="fullBackupContent" format="reference|boolean" /> + <!-- Indicates that even though the application provides a <code>BackupAgent</code>, only full-data streaming backup operations are to be performed to save the app's data. This lets the app rely on full-data backups while still participating in @@ -1189,6 +1194,7 @@ <attr name="backupAgent" /> <attr name="allowBackup" /> <attr name="fullBackupOnly" /> + <attr name="fullBackupContent" /> <attr name="killAfterRestore" /> <attr name="restoreNeedsApplication" /> <attr name="restoreAnyVersion" /> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 79b81a7..1a5977f 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2620,6 +2620,7 @@ <public type="attr" name="overflowTintMode" /> <public type="attr" name="navigationTint" /> <public type="attr" name="navigationTintMode" /> + <public type="attr" name="fullBackupContent" /> <public type="style" name="Widget.Material.Button.Colored" /> diff --git a/core/tests/coretests/src/android/app/backup/FullBackupTest.java b/core/tests/coretests/src/android/app/backup/FullBackupTest.java new file mode 100644 index 0000000..8c9c63c --- /dev/null +++ b/core/tests/coretests/src/android/app/backup/FullBackupTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2015 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.util.ArrayMap; +import android.util.ArraySet; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.File; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class FullBackupTest extends AndroidTestCase { + private XmlPullParserFactory mFactory; + private XmlPullParser mXpp; + private Context mContext; + + Map<String, Set<String>> includeMap; + Set<String> excludesSet; + + @Override + public void setUp() throws Exception { + mFactory = XmlPullParserFactory.newInstance(); + mXpp = mFactory.newPullParser(); + mContext = getContext(); + + includeMap = new ArrayMap(); + excludesSet = new ArraySet(); + } + + public void testparseBackupSchemeFromXml_onlyInclude() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<include path=\"onlyInclude.txt\" domain=\"file\"/>" + + "</full-backup-content>")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Excluding files when there was no <exclude/> tag.", 0, excludesSet.size()); + assertEquals("Unexpected number of <include/>s", 1, includeMap.size()); + + Set<String> fileDomainIncludes = includeMap.get(FullBackup.DATA_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + assertEquals("Invalid path parsed for <include/>", + new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), + fileDomainIncludes.iterator().next()); + } + + public void testparseBackupSchemeFromXml_onlyExclude() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<exclude path=\"onlyExclude.txt\" domain=\"file\"/>" + + "</full-backup-content>")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Including files when there was no <include/> tag.", 0, includeMap.size()); + assertEquals("Unexpected number of <exclude/>s", 1, excludesSet.size()); + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getFilesDir(), "onlyExclude.txt").getCanonicalPath(), + excludesSet.iterator().next()); + } + + public void testparseBackupSchemeFromXml_includeAndExclude() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<exclude path=\"exclude.txt\" domain=\"file\"/>" + + "<include path=\"include.txt\" domain=\"file\"/>" + + "</full-backup-content>")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + Set<String> fileDomainIncludes = includeMap.get(FullBackup.DATA_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + assertEquals("Invalid path parsed for <include/>", + new File(mContext.getFilesDir(), "include.txt").getCanonicalPath(), + fileDomainIncludes.iterator().next()); + + assertEquals("Unexpected number of <exclude/>s", 1, excludesSet.size()); + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getFilesDir(), "exclude.txt").getCanonicalPath(), + excludesSet.iterator().next()); + } + + public void testparseBackupSchemeFromXml_lotsOfIncludesAndExcludes() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<exclude path=\"exclude1.txt\" domain=\"file\"/>" + + "<include path=\"include1.txt\" domain=\"file\"/>" + + "<exclude path=\"exclude2.txt\" domain=\"database\"/>" + + "<include path=\"include2.txt\" domain=\"database\"/>" + + "<exclude path=\"exclude3.txt\" domain=\"sharedpref\"/>" + + "<include path=\"include3.txt\" domain=\"sharedpref\"/>" + + "</full-backup-content>")); + + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + Set<String> fileDomainIncludes = includeMap.get(FullBackup.DATA_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + assertEquals("Invalid path parsed for <include/>", + new File(mContext.getFilesDir(), "include1.txt").getCanonicalPath(), + fileDomainIncludes.iterator().next()); + + Set<String> databaseDomainIncludes = includeMap.get(FullBackup.DATABASE_TREE_TOKEN); + assertEquals("Didn't find expected database domain include.", + 2, databaseDomainIncludes.size()); // two expected here because of "-journal" file + assertTrue("Invalid path parsed for <include/>", + databaseDomainIncludes.contains( + new File(mContext.getDatabasePath("foo").getParentFile(), "include2.txt") + .getCanonicalPath())); + assertTrue("Invalid path parsed for <include/>", + databaseDomainIncludes.contains( + new File( + mContext.getDatabasePath("foo").getParentFile(), + "include2.txt-journal") + .getCanonicalPath())); + + Set<String> sharedPrefDomainIncludes = includeMap.get(FullBackup.SHAREDPREFS_TREE_TOKEN); + assertEquals("Didn't find expected sharedpref domain include.", + 1, sharedPrefDomainIncludes.size()); + assertEquals("Invalid path parsed for <include/>", + new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include3.txt") + .getCanonicalPath(), + sharedPrefDomainIncludes.iterator().next()); + + + assertEquals("Unexpected number of <exclude/>s", 4, excludesSet.size()); + // Sets are annoying to iterate over b/c order isn't enforced - convert to an array and + // sort lexicographically. + List<String> arrayedSet = new ArrayList<String>(excludesSet); + Collections.sort(arrayedSet); + + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getDatabasePath("foo").getParentFile(), "exclude2.txt") + .getCanonicalPath(), + arrayedSet.get(0)); + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getDatabasePath("foo").getParentFile(), "exclude2.txt-journal") + .getCanonicalPath(), + arrayedSet.get(1)); + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getFilesDir(), "exclude1.txt").getCanonicalPath(), + arrayedSet.get(2)); + assertEquals("Invalid path parsed for <exclude/>", + new File(mContext.getSharedPrefsFile("foo").getParentFile(), "exclude3.txt") + .getCanonicalPath(), + arrayedSet.get(3)); + } + + public void testParseBackupSchemeFromXml_invalidXmlFails() throws Exception { + // Invalid root tag. + mXpp.setInput(new StringReader( + "<full-weird-tag>" + + "<exclude path=\"invalidRootTag.txt\" domain=\"file\"/>" + + "</ffull-weird-tag>" )); + + try { + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + fail("Invalid root xml tag should throw an XmlPullParserException"); + } catch (XmlPullParserException expected) {} + + // Invalid exclude tag. + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<excluded path=\"invalidExcludeTag.txt\" domain=\"file\"/>" + + "</full-backup-conten>t" )); + try { + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + fail("Misspelled xml exclude tag should throw an XmlPullParserException"); + } catch (XmlPullParserException expected) {} + + // Just for good measure - invalid include tag. + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<yinclude path=\"invalidIncludeTag.txt\" domain=\"file\"/>" + + "</full-backup-conten>t" )); + try { + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + fail("Misspelled xml exclude tag should throw an XmlPullParserException"); + } catch (XmlPullParserException expected) {} + + } + + public void testInvalidPath_doesNotBackup() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<exclude path=\"..\" domain=\"file\"/>" + // Invalid use of ".." dir. + "</full-backup-content>" )); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size()); + + Set<String> fileDomainIncludes = includeMap.get(FullBackup.DATA_TREE_TOKEN); + assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes); + } + public void testDoubleDotInPath_isIgnored() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<include path=\"..\" domain=\"file\"/>" + // Invalid use of ".." dir. + "</full-backup-content>" )); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size()); + + Set<String> fileDomainIncludes = includeMap.get(FullBackup.DATA_TREE_TOKEN); + assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes); + } + + public void testDoubleSlashInPath_isIgnored() throws Exception { + mXpp.setInput(new StringReader( + "<full-backup-content>" + + "<exclude path=\"//hello.txt\" domain=\"file\"/>" + // Invalid use of "//" + "</full-backup-content>" )); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Didn't throw away invalid path containing \"//\".", 0, excludesSet.size()); + } +} |