Merge "Extend XML parser to allow optional requiredFlags attribute for <include /> rules in <full-backup-content> specification"
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index d36a794..d1c957b 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -18,6 +18,7 @@
 
 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;
@@ -343,8 +344,8 @@
             return;
         }
 
-        Map<String, Set<String>> manifestIncludeMap;
-        ArraySet<String> manifestExcludeSet;
+        Map<String, Set<PathWithRequiredFlags>> manifestIncludeMap;
+        ArraySet<PathWithRequiredFlags> manifestExcludeSet;
         try {
             manifestIncludeMap =
                     backupScheme.maybeParseAndGetCanonicalIncludePaths();
@@ -514,14 +515,13 @@
     /**
      * 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.
+     * 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<String>> includeMap,
-                                                         ArraySet<String> filterSet,
-                                                         ArraySet<String> traversalExcludeSet,
-                                                         FullBackupDataOutput data)
-            throws IOException {
+            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,
@@ -530,13 +530,22 @@
         } 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);
+            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
@@ -683,7 +692,7 @@
      * @hide
      */
     protected final void fullBackupFileTree(String packageName, String domain, String startingPath,
-                                            ArraySet<String> manifestExcludes,
+                                            ArraySet<PathWithRequiredFlags> manifestExcludes,
                                             ArraySet<String> systemExcludes,
             FullBackupDataOutput output) {
         // Pull out the domain and set it aside to use when making the tarball.
@@ -714,7 +723,8 @@
                     filePath = file.getCanonicalPath();
 
                     // prune this subtree?
-                    if (manifestExcludes != null && manifestExcludes.contains(filePath)) {
+                    if (manifestExcludes != null
+                            && manifestExcludesContainFilePath(manifestExcludes, filePath)) {
                         continue;
                     }
                     if (systemExcludes != null && systemExcludes.contains(filePath)) {
@@ -750,6 +760,17 @@
         }
     }
 
+    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
@@ -796,8 +817,8 @@
             return false;
         }
 
-        Map<String, Set<String>> includes = null;
-        ArraySet<String> excludes = null;
+        Map<String, Set<PathWithRequiredFlags>> includes = null;
+        ArraySet<PathWithRequiredFlags> excludes = null;
         final String destinationCanonicalPath = destination.getCanonicalPath();
         try {
             includes = bs.maybeParseAndGetCanonicalIncludePaths();
@@ -826,7 +847,7 @@
             // 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()) {
+            for (Set<PathWithRequiredFlags> domainIncludes : includes.values()) {
                 explicitlyIncluded |= isFileSpecifiedInPathList(destination, domainIncludes);
                 if (explicitlyIncluded) {
                     break;
@@ -849,9 +870,10 @@
      * @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) {
+    private boolean isFileSpecifiedInPathList(File file,
+            Collection<PathWithRequiredFlags> canonicalPathList) throws IOException {
+        for (PathWithRequiredFlags canonical : canonicalPathList) {
+            String canonicalPath = canonical.getPath();
             File fileFromList = new File(canonicalPath);
             if (fileFromList.isDirectory()) {
                 if (file.isDirectory()) {
diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java
index a5dd5bd..fb1c2d0 100644
--- a/core/java/android/app/backup/FullBackup.java
+++ b/core/java/android/app/backup/FullBackup.java
@@ -82,6 +82,9 @@
     public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
     public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";
 
+    public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption";
+    public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer";
+
     /**
      * @hide
      */
@@ -224,6 +227,9 @@
 
         private final File EXTERNAL_DIR;
 
+        private final static String TAG_INCLUDE = "include";
+        private final static String TAG_EXCLUDE = "exclude";
+
         final int mFullBackupContent;
         final PackageManager mPackageManager;
         final StorageManager mStorageManager;
@@ -303,15 +309,45 @@
         }
 
         /**
-        * 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.
+         * Represents a path attribute specified in an <include /> rule along with optional
+         * transport flags required from the transport to include file(s) under that path as
+         * specified by requiredFlags attribute. If optional requiredFlags attribute is not
+         * provided, default requiredFlags to 0.
+         * Note: since our parsing codepaths were the same for <include /> and <exclude /> tags,
+         * this structure is also used for <exclude /> tags to preserve that, however you can expect
+         * the getRequiredFlags() to always return 0 for exclude rules.
          */
-        ArraySet<String> mExcludes;
+        public static class PathWithRequiredFlags {
+            private final String mPath;
+            private final int mRequiredFlags;
+
+            public PathWithRequiredFlags(String path, int requiredFlags) {
+                mPath = path;
+                mRequiredFlags = requiredFlags;
+            }
+
+            public String getPath() {
+                return mPath;
+            }
+
+            public int getRequiredFlags() {
+                return mRequiredFlags;
+            }
+        }
+
+        /**
+         * A map of domain -> set of pairs (canonical file; required transport flags) in that
+         * domain that are to be included if the transport has decared the required flags.
+         * We keep track of the domain so that we can go through the file system in order later on.
+         */
+        Map<String, Set<PathWithRequiredFlags>> mIncludes;
+
+        /**
+         * Set that will be populated with pairs (canonical file; requiredFlags=0) for each file or
+         * directory that is to be excluded. Note that for excludes, the requiredFlags attribute is
+         * ignored and the value should be always set to 0.
+         */
+        ArraySet<PathWithRequiredFlags> mExcludes;
 
         BackupScheme(Context context) {
             mFullBackupContent = context.getApplicationInfo().fullBackupContent;
@@ -356,13 +392,14 @@
         }
 
         /**
-         * @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).
+         * @return A mapping of domain -> set of pairs (canonical file; required transport flags)
+         * in that domain that are to be included if the transport has decared the required flags.
+         * 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 {
+        public synchronized Map<String, Set<PathWithRequiredFlags>>
+                maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException {
             if (mIncludes == null) {
                 maybeParseBackupSchemeLocked();
             }
@@ -370,9 +407,10 @@
         }
 
         /**
-         * @return A set of canonical paths that are to be excluded from the backup/restore set.
+         * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the
+         * backup/restore set.
          */
-        public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths()
+        public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths()
                 throws IOException, XmlPullParserException {
             if (mExcludes == null) {
                 maybeParseBackupSchemeLocked();
@@ -382,8 +420,8 @@
 
         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>();
+            mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
+            mExcludes = new ArraySet<PathWithRequiredFlags>();
 
             if (mFullBackupContent == 0) {
                 // android:fullBackupContent="true" which means that we'll do everything.
@@ -415,8 +453,8 @@
 
         @VisibleForTesting
         public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
-                                                   Set<String> excludes,
-                                                   Map<String, Set<String>> includes)
+                                                   Set<PathWithRequiredFlags> excludes,
+                                                   Map<String, Set<PathWithRequiredFlags>> includes)
                 throws IOException, XmlPullParserException {
             int event = parser.getEventType(); // START_DOCUMENT
             while (event != XmlPullParser.START_TAG) {
@@ -441,8 +479,7 @@
                     case XmlPullParser.START_TAG:
                         validateInnerTagContents(parser);
                         final String domainFromXml = parser.getAttributeValue(null, "domain");
-                        final File domainDirectory =
-                                getDirectoryForCriteriaDomain(domainFromXml);
+                        final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml);
                         if (domainDirectory == null) {
                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                 Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
@@ -457,12 +494,23 @@
                             break;
                         }
 
-                        Set<String> activeSet = parseCurrentTagForDomain(
+                        int requiredFlags = 0; // no transport flags are required by default
+                        if (TAG_INCLUDE.equals(parser.getName())) {
+                            // requiredFlags are only supported for <include /> tag, for <exclude />
+                            // we should always leave them as the default = 0
+                            requiredFlags = getRequiredFlagsFromString(
+                                    parser.getAttributeValue(null, "requireFlags"));
+                        }
+
+                        // retrieve the include/exclude set we'll be adding this rule to
+                        Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain(
                                 parser, excludes, includes, domainFromXml);
-                        activeSet.add(canonicalFile.getCanonicalPath());
+                        activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(),
+                                requiredFlags));
                         if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                             Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
-                                    + " for domain \"" + domainFromXml + "\"");
+                                    + " for domain \"" + domainFromXml + "\", requiredFlags + \""
+                                    + requiredFlags + "\"");
                         }
 
                         // Special case journal files (not dirs) for sqlite database. frowny-face.
@@ -472,14 +520,16 @@
                         if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
                             final String canonicalJournalPath =
                                     canonicalFile.getCanonicalPath() + "-journal";
-                            activeSet.add(canonicalJournalPath);
+                            activeSet.add(new PathWithRequiredFlags(canonicalJournalPath,
+                                    requiredFlags));
                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                 Log.v(TAG_XML_PARSER, "...automatically generated "
                                         + canonicalJournalPath + ". Ignore if nonexistent.");
                             }
                             final String canonicalWalPath =
                                     canonicalFile.getCanonicalPath() + "-wal";
-                            activeSet.add(canonicalWalPath);
+                            activeSet.add(new PathWithRequiredFlags(canonicalWalPath,
+                                    requiredFlags));
                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                 Log.v(TAG_XML_PARSER, "...automatically generated "
                                         + canonicalWalPath + ". Ignore if nonexistent.");
@@ -491,7 +541,8 @@
                             !canonicalFile.getCanonicalPath().endsWith(".xml")) {
                             final String canonicalXmlPath =
                                     canonicalFile.getCanonicalPath() + ".xml";
-                            activeSet.add(canonicalXmlPath);
+                            activeSet.add(new PathWithRequiredFlags(canonicalXmlPath,
+                                    requiredFlags));
                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                 Log.v(TAG_XML_PARSER, "...automatically generated "
                                         + canonicalXmlPath + ". Ignore if nonexistent.");
@@ -508,10 +559,12 @@
                     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()) {
+                    for (Map.Entry<String, Set<PathWithRequiredFlags>> entry
+                            : includes.entrySet()) {
                         Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
-                        for (String includeData : entry.getValue()) {
-                            Log.v(TAG_XML_PARSER, "  " + includeData);
+                        for (PathWithRequiredFlags includeData : entry.getValue()) {
+                            Log.v(TAG_XML_PARSER, " path: " + includeData.getPath()
+                                    + " requiredFlags: " + includeData.getRequiredFlags());
                         }
                     }
                 }
@@ -520,8 +573,9 @@
                 if (excludes.isEmpty()) {
                     Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
                 } else {
-                    for (String excludeData : excludes) {
-                        Log.v(TAG_XML_PARSER, "  " + excludeData);
+                    for (PathWithRequiredFlags excludeData : excludes) {
+                        Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath()
+                                + " requiredFlags: " + excludeData.getRequiredFlags());
                     }
                 }
 
@@ -531,20 +585,41 @@
             }
         }
 
-        private Set<String> parseCurrentTagForDomain(XmlPullParser parser,
-                                                     Set<String> excludes,
-                                                     Map<String, Set<String>> includes,
-                                                     String domain)
+        private int getRequiredFlagsFromString(String requiredFlags) {
+            int flags = 0;
+            if (requiredFlags == null || requiredFlags.length() == 0) {
+                // requiredFlags attribute was missing or empty in <include /> tag
+                return flags;
+            }
+            String[] flagsStr = requiredFlags.split("\\|");
+            for (String f : flagsStr) {
+                switch (f) {
+                    case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION:
+                        flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
+                        break;
+                    case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER:
+                        flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER;
+                        break;
+                    default:
+                        Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\"");
+                }
+            }
+            return flags;
+        }
+
+        private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser,
+                Set<PathWithRequiredFlags> excludes,
+                Map<String, Set<PathWithRequiredFlags>> includes, String domain)
                 throws XmlPullParserException {
-            if ("include".equals(parser.getName())) {
+            if (TAG_INCLUDE.equals(parser.getName())) {
                 final String domainToken = getTokenForXmlDomain(domain);
-                Set<String> includeSet = includes.get(domainToken);
+                Set<PathWithRequiredFlags> includeSet = includes.get(domainToken);
                 if (includeSet == null) {
-                    includeSet = new ArraySet<String>();
+                    includeSet = new ArraySet<PathWithRequiredFlags>();
                     includes.put(domainToken, includeSet);
                 }
                 return includeSet;
-            } else if ("exclude".equals(parser.getName())) {
+            } else if (TAG_EXCLUDE.equals(parser.getName())) {
                 return excludes;
             } else {
                 // Unrecognised tag => hard failure.
@@ -589,8 +664,8 @@
         /**
          *
          * @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.
+         * @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) {
@@ -650,15 +725,27 @@
          * 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\".");
+        private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException {
+            if (parser == null) {
+                return;
             }
-            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() + "\"");
+            switch (parser.getName()) {
+                case TAG_INCLUDE:
+                    if (parser.getAttributeCount() > 3) {
+                        throw new XmlPullParserException("At most 3 tag attributes allowed for "
+                                + "\"include\" tag (\"domain\" & \"path\""
+                                + " & optional \"requiredFlags\").");
+                    }
+                    break;
+                case TAG_EXCLUDE:
+                    if (parser.getAttributeCount() > 2) {
+                        throw new XmlPullParserException("At most 2 tag attributes allowed for "
+                                + "\"exclude\" tag (\"domain\" & \"path\".");
+                    }
+                    break;
+                default:
+                    throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
+                            " \"<exclude/>. You provided \"" + parser.getName() + "\"");
             }
         }
     }
diff --git a/core/tests/coretests/src/android/app/backup/FullBackupTest.java b/core/tests/coretests/src/android/app/backup/FullBackupTest.java
index bc6fc15..58ee7a7 100644
--- a/core/tests/coretests/src/android/app/backup/FullBackupTest.java
+++ b/core/tests/coretests/src/android/app/backup/FullBackupTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
 import android.content.Context;
 import android.support.test.filters.LargeTest;
 import android.test.AndroidTestCase;
@@ -43,8 +44,8 @@
     private XmlPullParser mXpp;
     private Context mContext;
 
-    Map<String, Set<String>> includeMap;
-    Set<String> excludesSet;
+    Map<String, Set<PathWithRequiredFlags>> includeMap;
+    Set<PathWithRequiredFlags> excludesSet;
 
     @Override
     public void setUp() throws Exception {
@@ -52,8 +53,8 @@
         mXpp = mFactory.newPullParser();
         mContext = getContext();
 
-        includeMap = new ArrayMap();
-        excludesSet = new ArraySet();
+        includeMap = new ArrayMap<>();
+        excludesSet = new ArraySet<>();
     }
 
     public void testparseBackupSchemeFromXml_onlyInclude() throws Exception {
@@ -68,11 +69,127 @@
         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.FILES_TREE_TOKEN);
+        Set<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
         assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size());
+        PathWithRequiredFlags include = fileDomainIncludes.iterator().next();
         assertEquals("Invalid path parsed for <include/>",
                 new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(),
-                fileDomainIncludes.iterator().next());
+                include.getPath());
+        assertEquals("Invalid requireFlags parsed for <include/>", 0, include.getRequiredFlags());
+    }
+
+    public void testparseBackupSchemeFromXml_onlyIncludeRequireEncryptionFlag() throws Exception {
+        mXpp.setInput(new StringReader(
+                "<full-backup-content>" +
+                        "<include path=\"onlyInclude.txt\" domain=\"file\""
+                        + " requireFlags=\"clientSideEncryption\"/>" +
+                "</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<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size());
+        PathWithRequiredFlags include = fileDomainIncludes.iterator().next();
+        assertEquals("Invalid path parsed for <include/>",
+                new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(),
+                include.getPath());
+        assertEquals("Invalid requireFlags parsed for <include/>",
+                BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED,
+                include.getRequiredFlags());
+    }
+
+    public void testparseBackupSchemeFromXml_onlyIncludeRequireD2DFlag() throws Exception {
+        mXpp.setInput(new StringReader(
+                "<full-backup-content>" +
+                        "<include path=\"onlyInclude.txt\" domain=\"file\""
+                        + " requireFlags=\"deviceToDeviceTransfer\"/>" +
+                "</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<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size());
+        PathWithRequiredFlags include = fileDomainIncludes.iterator().next();
+        assertEquals("Invalid path parsed for <include/>",
+                new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(),
+                include.getPath());
+        assertEquals("Invalid requireFlags parsed for <include/>",
+                BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER,
+                include.getRequiredFlags());
+    }
+
+    public void testparseBackupSchemeFromXml_onlyIncludeRequireEncryptionAndD2DFlags()
+            throws Exception {
+        mXpp.setInput(new StringReader(
+                "<full-backup-content>" +
+                        "<include path=\"onlyInclude.txt\" domain=\"file\""
+                        + " requireFlags=\"clientSideEncryption|deviceToDeviceTransfer\"/>" +
+                "</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<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size());
+        PathWithRequiredFlags include = fileDomainIncludes.iterator().next();
+        assertEquals("Invalid path parsed for <include/>",
+                new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(),
+                include.getPath());
+        assertEquals("Invalid requireFlags parsed for <include/>",
+                BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
+                        | BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER,
+                include.getRequiredFlags());
+    }
+
+    public void testparseBackupSchemeFromXml_onlyIncludeRequireD2DFlagAndIngoreGarbage()
+            throws Exception {
+        mXpp.setInput(new StringReader(
+                "<full-backup-content>" +
+                        "<include path=\"onlyInclude.txt\" domain=\"file\""
+                        + " requireFlags=\"deviceToDeviceTransfer|garbageFlag\"/>" +
+                "</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<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size());
+        PathWithRequiredFlags include = fileDomainIncludes.iterator().next();
+        assertEquals("Invalid path parsed for <include/>",
+                new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(),
+                include.getPath());
+        assertEquals("Invalid requireFlags parsed for <include/>",
+                BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER,
+                include.getRequiredFlags());
+    }
+
+    public void testparseBackupSchemeFromXml_onlyExcludeRequireFlagsNotSupported()
+            throws Exception {
+        mXpp.setInput(new StringReader(
+                "<full-backup-content>" +
+                        "<exclude path=\"onlyExclude.txt\" domain=\"file\""
+                        + " requireFlags=\"deviceToDeviceTransfer\"/>" +
+                "</full-backup-content>"));
+
+        try {
+            FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext);
+            bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap);
+            fail("Having more than 3 attributes in exclude should throw an XmlPullParserException");
+        } catch (XmlPullParserException expected) {}
     }
 
     public void testparseBackupSchemeFromXml_onlyExclude() throws Exception {
@@ -88,7 +205,7 @@
         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());
+                excludesSet.iterator().next().getPath());
     }
 
     public void testparseBackupSchemeFromXml_includeAndExclude() throws Exception {
@@ -101,16 +218,16 @@
         FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext);
         bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap);
 
-        Set<String> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        Set<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_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());
+                fileDomainIncludes.iterator().next().getPath());
 
         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());
+                excludesSet.iterator().next().getPath());
     }
 
     public void testparseBackupSchemeFromXml_lotsOfIncludesAndExcludes() throws Exception {
@@ -126,61 +243,74 @@
                         "<include path=\"include4.xml\" domain=\"sharedpref\"/>" +
                 "</full-backup-content>"));
 
-
         FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext);
         bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap);
 
-        Set<String> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        Set<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_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());
+                fileDomainIncludes.iterator().next().getPath());
 
-        Set<String> databaseDomainIncludes = includeMap.get(FullBackup.DATABASE_TREE_TOKEN);
+        Set<PathWithRequiredFlags> databaseDomainIncludes =
+                includeMap.get(FullBackup.DATABASE_TREE_TOKEN);
+        Set<String> databaseDomainIncludesPaths = new ArraySet<>();
+        for (PathWithRequiredFlags databaseInclude : databaseDomainIncludes) {
+            databaseDomainIncludesPaths.add(databaseInclude.getPath());
+        }
         // Three expected here because of "-journal" and "-wal" files
         assertEquals("Didn't find expected database domain include.",
                 3, databaseDomainIncludes.size());
         assertTrue("Invalid path parsed for <include/>",
-                databaseDomainIncludes.contains(
+                databaseDomainIncludesPaths.contains(
                         new File(mContext.getDatabasePath("foo").getParentFile(), "include2.txt")
                                 .getCanonicalPath()));
         assertTrue("Invalid path parsed for <include/>",
-                databaseDomainIncludes.contains(
+                databaseDomainIncludesPaths.contains(
                         new File(
                                 mContext.getDatabasePath("foo").getParentFile(),
                                 "include2.txt-journal")
                                 .getCanonicalPath()));
         assertTrue("Invalid path parsed for <include/>",
-                databaseDomainIncludes.contains(
+                databaseDomainIncludesPaths.contains(
                         new File(
                                 mContext.getDatabasePath("foo").getParentFile(),
                                 "include2.txt-wal")
                                 .getCanonicalPath()));
 
-        List<String> sharedPrefDomainIncludes = new ArrayList<String>(
+        List<PathWithRequiredFlags> sharedPrefDomainIncludes = new ArrayList<PathWithRequiredFlags>(
                 includeMap.get(FullBackup.SHAREDPREFS_TREE_TOKEN));
-        Collections.sort(sharedPrefDomainIncludes);
+        ArrayList<String> sharedPrefDomainIncludesPaths = new ArrayList<>();
+        for (PathWithRequiredFlags sharedPrefInclude : sharedPrefDomainIncludes) {
+            sharedPrefDomainIncludesPaths.add(sharedPrefInclude.getPath());
+        }
+        // Sets are annoying to iterate over b/c order isn't enforced - convert to an array and
+        // sort lexicographically.
+        Collections.sort(sharedPrefDomainIncludesPaths);
 
         assertEquals("Didn't find expected sharedpref domain include.",
                 3, sharedPrefDomainIncludes.size());
         assertEquals("Invalid path parsed for <include/>",
                 new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include3")
                         .getCanonicalPath(),
-                sharedPrefDomainIncludes.get(0));
+                sharedPrefDomainIncludesPaths.get(0));
         assertEquals("Invalid path parsed for <include/>",
                 new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include3.xml")
                         .getCanonicalPath(),
-                sharedPrefDomainIncludes.get(1));
+                sharedPrefDomainIncludesPaths.get(1));
         assertEquals("Invalid path parsed for <include/>",
                 new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include4.xml")
                         .getCanonicalPath(),
-                sharedPrefDomainIncludes.get(2));
+                sharedPrefDomainIncludesPaths.get(2));
 
 
         assertEquals("Unexpected number of <exclude/>s", 7, 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);
+        ArrayList<String> arrayedSet = new ArrayList<>();
+        for (PathWithRequiredFlags exclude : excludesSet) {
+            arrayedSet.add(exclude.getPath());
+        }
         Collections.sort(arrayedSet);
 
         assertEquals("Invalid path parsed for <exclude/>",
@@ -260,9 +390,10 @@
 
         assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size());
 
-        Set<String> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        Set<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
         assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes);
     }
+
     public void testDoubleDotInPath_isIgnored() throws Exception {
         mXpp.setInput(new StringReader(
                 "<full-backup-content>" +
@@ -274,7 +405,7 @@
 
         assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size());
 
-        Set<String> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
+        Set<PathWithRequiredFlags> fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN);
         assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes);
     }
 
diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk
index d825533..cd8163d 100644
--- a/services/robotests/Android.mk
+++ b/services/robotests/Android.mk
@@ -58,14 +58,14 @@
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
     $(call all-Iaidl-files-under, $(INTERNAL_BACKUP)) \
+    $(call all-java-files-under, ../../core/java/android/app/backup) \
+    $(call all-Iaidl-files-under, ../../core/java/android/app/backup) \
     ../../core/java/android/content/pm/PackageInfo.java \
-    ../../core/java/android/app/backup/BackupAgent.java \
-    ../../core/java/android/app/backup/BackupDataOutput.java \
-    ../../core/java/android/app/backup/FullBackupDataOutput.java \
     ../../core/java/android/app/IBackupAgent.aidl
 
 LOCAL_AIDL_INCLUDES := \
     $(call all-Iaidl-files-under, $(INTERNAL_BACKUP)) \
+    $(call all-Iaidl-files-under, ../../core/java/android/app/backup) \
     ../../core/java/android/app/IBackupAgent.aidl
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
diff --git a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
index d0e6658..60704e7 100644
--- a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
+++ b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java
@@ -112,11 +112,8 @@
         ShadowQueuedWork.class
     }
 )
-@SystemLoaderPackages({"com.android.server.backup"})
+@SystemLoaderPackages({"com.android.server.backup", "android.app.backup"})
 @SystemLoaderClasses({
-    BackupDataOutput.class,
-    FullBackupDataOutput.class,
-    BackupAgent.class,
     IBackupTransport.class,
     IBackupAgent.class,
     PackageInfo.class