FullBackup.java revision 39cf42c9273bc496a89476a0b637874fa21940a2
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.app.backup;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.XmlResourceParser;
22import android.os.ParcelFileDescriptor;
23import android.os.Process;
24import android.os.storage.StorageManager;
25import android.os.storage.StorageVolume;
26import android.system.ErrnoException;
27import android.system.Os;
28import android.text.TextUtils;
29import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.Log;
32
33import com.android.internal.annotations.VisibleForTesting;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.util.Map;
43import java.util.Set;
44
45/**
46 * Global constant definitions et cetera related to the full-backup-to-fd
47 * binary format.  Nothing in this namespace is part of any API; it's all
48 * hidden details of the current implementation gathered into one location.
49 *
50 * @hide
51 */
52public class FullBackup {
53    static final String TAG = "FullBackup";
54    /** Enable this log tag to get verbose information while parsing the client xml. */
55    static final String TAG_XML_PARSER = "BackupXmlParserLogging";
56
57    public static final String APK_TREE_TOKEN = "a";
58    public static final String OBB_TREE_TOKEN = "obb";
59    public static final String KEY_VALUE_DATA_TOKEN = "k";
60
61    public static final String ROOT_TREE_TOKEN = "r";
62    public static final String FILES_TREE_TOKEN = "f";
63    public static final String NO_BACKUP_TREE_TOKEN = "nb";
64    public static final String DATABASE_TREE_TOKEN = "db";
65    public static final String SHAREDPREFS_TREE_TOKEN = "sp";
66    public static final String CACHE_TREE_TOKEN = "c";
67
68    public static final String DEVICE_ROOT_TREE_TOKEN = "d_r";
69    public static final String DEVICE_FILES_TREE_TOKEN = "d_f";
70    public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb";
71    public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db";
72    public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp";
73    public static final String DEVICE_CACHE_TREE_TOKEN = "d_c";
74
75    public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";
76    public static final String SHARED_STORAGE_TOKEN = "shared";
77
78    public static final String APPS_PREFIX = "apps/";
79    public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
80
81    public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
82    public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
83    public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";
84
85    public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption";
86    public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer";
87    public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION =
88            "fakeClientSideEncryption";
89
90    /**
91     * @hide
92     */
93    static public native int backupToTar(String packageName, String domain,
94            String linkdomain, String rootpath, String path, FullBackupDataOutput output);
95
96    private static final Map<String, BackupScheme> kPackageBackupSchemeMap =
97            new ArrayMap<String, BackupScheme>();
98
99    static synchronized BackupScheme getBackupScheme(Context context) {
100        BackupScheme backupSchemeForPackage =
101                kPackageBackupSchemeMap.get(context.getPackageName());
102        if (backupSchemeForPackage == null) {
103            backupSchemeForPackage = new BackupScheme(context);
104            kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage);
105        }
106        return backupSchemeForPackage;
107    }
108
109    public static BackupScheme getBackupSchemeForTest(Context context) {
110        BackupScheme testing = new BackupScheme(context);
111        testing.mExcludes = new ArraySet();
112        testing.mIncludes = new ArrayMap();
113        return testing;
114    }
115
116
117    /**
118     * Copy data from a socket to the given File location on permanent storage.  The
119     * modification time and access mode of the resulting file will be set if desired,
120     * although group/all rwx modes will be stripped: the restored file will not be
121     * accessible from outside the target application even if the original file was.
122     * If the {@code type} parameter indicates that the result should be a directory,
123     * the socket parameter may be {@code null}; even if it is valid, no data will be
124     * read from it in this case.
125     * <p>
126     * If the {@code mode} argument is negative, then the resulting output file will not
127     * have its access mode or last modification time reset as part of this operation.
128     *
129     * @param data Socket supplying the data to be copied to the output file.  If the
130     *    output is a directory, this may be {@code null}.
131     * @param size Number of bytes of data to copy from the socket to the file.  At least
132     *    this much data must be available through the {@code data} parameter.
133     * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
134     *    or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
135     * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
136     *    the output file or directory.  group/all rwx modes are stripped even if set
137     *    in this parameter.  If this parameter is negative then neither
138     *    the mode nor the mtime values will be applied to the restored file.
139     * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
140     *    last modification time of the output file.  if the {@code mode} parameter is
141     *    negative then this parameter will be ignored.
142     * @param outFile Location within the filesystem to place the data.  This must point
143     *    to a location that is writeable by the caller, preferably using an absolute path.
144     * @throws IOException
145     */
146    static public void restoreFile(ParcelFileDescriptor data,
147            long size, int type, long mode, long mtime, File outFile) throws IOException {
148        if (type == BackupAgent.TYPE_DIRECTORY) {
149            // Canonically a directory has no associated content, so we don't need to read
150            // anything from the pipe in this case.  Just create the directory here and
151            // drop down to the final metadata adjustment.
152            if (outFile != null) outFile.mkdirs();
153        } else {
154            FileOutputStream out = null;
155
156            // Pull the data from the pipe, copying it to the output file, until we're done
157            try {
158                if (outFile != null) {
159                    File parent = outFile.getParentFile();
160                    if (!parent.exists()) {
161                        // in practice this will only be for the default semantic directories,
162                        // and using the default mode for those is appropriate.
163                        // This can also happen for the case where a parent directory has been
164                        // excluded, but a file within that directory has been included.
165                        parent.mkdirs();
166                    }
167                    out = new FileOutputStream(outFile);
168                }
169            } catch (IOException e) {
170                Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
171            }
172
173            byte[] buffer = new byte[32 * 1024];
174            final long origSize = size;
175            FileInputStream in = new FileInputStream(data.getFileDescriptor());
176            while (size > 0) {
177                int toRead = (size > buffer.length) ? buffer.length : (int)size;
178                int got = in.read(buffer, 0, toRead);
179                if (got <= 0) {
180                    Log.w(TAG, "Incomplete read: expected " + size + " but got "
181                            + (origSize - size));
182                    break;
183                }
184                if (out != null) {
185                    try {
186                        out.write(buffer, 0, got);
187                    } catch (IOException e) {
188                        // Problem writing to the file.  Quit copying data and delete
189                        // the file, but of course keep consuming the input stream.
190                        Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
191                        out.close();
192                        out = null;
193                        outFile.delete();
194                    }
195                }
196                size -= got;
197            }
198            if (out != null) out.close();
199        }
200
201        // Now twiddle the state to match the backup, assuming all went well
202        if (mode >= 0 && outFile != null) {
203            try {
204                // explicitly prevent emplacement of files accessible by outside apps
205                mode &= 0700;
206                Os.chmod(outFile.getPath(), (int)mode);
207            } catch (ErrnoException e) {
208                e.rethrowAsIOException();
209            }
210            outFile.setLastModified(mtime);
211        }
212    }
213
214    @VisibleForTesting
215    public static class BackupScheme {
216        private final File FILES_DIR;
217        private final File DATABASE_DIR;
218        private final File ROOT_DIR;
219        private final File SHAREDPREF_DIR;
220        private final File CACHE_DIR;
221        private final File NOBACKUP_DIR;
222
223        private final File DEVICE_FILES_DIR;
224        private final File DEVICE_DATABASE_DIR;
225        private final File DEVICE_ROOT_DIR;
226        private final File DEVICE_SHAREDPREF_DIR;
227        private final File DEVICE_CACHE_DIR;
228        private final File DEVICE_NOBACKUP_DIR;
229
230        private final File EXTERNAL_DIR;
231
232        private final static String TAG_INCLUDE = "include";
233        private final static String TAG_EXCLUDE = "exclude";
234
235        final int mFullBackupContent;
236        final PackageManager mPackageManager;
237        final StorageManager mStorageManager;
238        final String mPackageName;
239
240        // lazy initialized, only when needed
241        private StorageVolume[] mVolumes = null;
242
243        /**
244         * Parse out the semantic domains into the correct physical location.
245         */
246        String tokenToDirectoryPath(String domainToken) {
247            try {
248                if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
249                    return FILES_DIR.getCanonicalPath();
250                } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
251                    return DATABASE_DIR.getCanonicalPath();
252                } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
253                    return ROOT_DIR.getCanonicalPath();
254                } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
255                    return SHAREDPREF_DIR.getCanonicalPath();
256                } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
257                    return CACHE_DIR.getCanonicalPath();
258                } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
259                    return NOBACKUP_DIR.getCanonicalPath();
260                } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) {
261                    return DEVICE_FILES_DIR.getCanonicalPath();
262                } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) {
263                    return DEVICE_DATABASE_DIR.getCanonicalPath();
264                } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) {
265                    return DEVICE_ROOT_DIR.getCanonicalPath();
266                } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) {
267                    return DEVICE_SHAREDPREF_DIR.getCanonicalPath();
268                } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) {
269                    return DEVICE_CACHE_DIR.getCanonicalPath();
270                } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) {
271                    return DEVICE_NOBACKUP_DIR.getCanonicalPath();
272                } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
273                    if (EXTERNAL_DIR != null) {
274                        return EXTERNAL_DIR.getCanonicalPath();
275                    } else {
276                        return null;
277                    }
278                } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
279                    return sharedDomainToPath(domainToken);
280                }
281                // Not a supported location
282                Log.i(TAG, "Unrecognized domain " + domainToken);
283                return null;
284            } catch (Exception e) {
285                Log.i(TAG, "Error reading directory for domain: " + domainToken);
286                return null;
287            }
288
289        }
290
291        private String sharedDomainToPath(String domain) throws IOException {
292            // already known to start with SHARED_PREFIX, so we just look after that
293            final String volume = domain.substring(FullBackup.SHARED_PREFIX.length());
294            final StorageVolume[] volumes = getVolumeList();
295            final int volNum = Integer.parseInt(volume);
296            if (volNum < mVolumes.length) {
297                return volumes[volNum].getPathFile().getCanonicalPath();
298            }
299            return null;
300        }
301
302        private StorageVolume[] getVolumeList() {
303            if (mStorageManager != null) {
304                if (mVolumes == null) {
305                    mVolumes = mStorageManager.getVolumeList();
306                }
307            } else {
308                Log.e(TAG, "Unable to access Storage Manager");
309            }
310            return mVolumes;
311        }
312
313        /**
314         * Represents a path attribute specified in an <include /> rule along with optional
315         * transport flags required from the transport to include file(s) under that path as
316         * specified by requiredFlags attribute. If optional requiredFlags attribute is not
317         * provided, default requiredFlags to 0.
318         * Note: since our parsing codepaths were the same for <include /> and <exclude /> tags,
319         * this structure is also used for <exclude /> tags to preserve that, however you can expect
320         * the getRequiredFlags() to always return 0 for exclude rules.
321         */
322        public static class PathWithRequiredFlags {
323            private final String mPath;
324            private final int mRequiredFlags;
325
326            public PathWithRequiredFlags(String path, int requiredFlags) {
327                mPath = path;
328                mRequiredFlags = requiredFlags;
329            }
330
331            public String getPath() {
332                return mPath;
333            }
334
335            public int getRequiredFlags() {
336                return mRequiredFlags;
337            }
338        }
339
340        /**
341         * A map of domain -> set of pairs (canonical file; required transport flags) in that
342         * domain that are to be included if the transport has decared the required flags.
343         * We keep track of the domain so that we can go through the file system in order later on.
344         */
345        Map<String, Set<PathWithRequiredFlags>> mIncludes;
346
347        /**
348         * Set that will be populated with pairs (canonical file; requiredFlags=0) for each file or
349         * directory that is to be excluded. Note that for excludes, the requiredFlags attribute is
350         * ignored and the value should be always set to 0.
351         */
352        ArraySet<PathWithRequiredFlags> mExcludes;
353
354        BackupScheme(Context context) {
355            mFullBackupContent = context.getApplicationInfo().fullBackupContent;
356            mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
357            mPackageManager = context.getPackageManager();
358            mPackageName = context.getPackageName();
359
360            // System apps have control over where their default storage context
361            // is pointed, so we're always explicit when building paths.
362            final Context ceContext = context.createCredentialProtectedStorageContext();
363            FILES_DIR = ceContext.getFilesDir();
364            DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile();
365            ROOT_DIR = ceContext.getDataDir();
366            SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile();
367            CACHE_DIR = ceContext.getCacheDir();
368            NOBACKUP_DIR = ceContext.getNoBackupFilesDir();
369
370            final Context deContext = context.createDeviceProtectedStorageContext();
371            DEVICE_FILES_DIR = deContext.getFilesDir();
372            DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile();
373            DEVICE_ROOT_DIR = deContext.getDataDir();
374            DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile();
375            DEVICE_CACHE_DIR = deContext.getCacheDir();
376            DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir();
377
378            if (android.os.Process.myUid() != Process.SYSTEM_UID) {
379                EXTERNAL_DIR = context.getExternalFilesDir(null);
380            } else {
381                EXTERNAL_DIR = null;
382            }
383        }
384
385        boolean isFullBackupContentEnabled() {
386            if (mFullBackupContent < 0) {
387                // android:fullBackupContent="false", bail.
388                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
389                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
390                }
391                return false;
392            }
393            return true;
394        }
395
396        /**
397         * @return A mapping of domain -> set of pairs (canonical file; required transport flags)
398         * in that domain that are to be included if the transport has decared the required flags.
399         * Each of these paths specifies a file that the client has explicitly included in their
400         * backup set. If this map is empty we will back up the entire data directory (including
401         * managed external storage).
402         */
403        public synchronized Map<String, Set<PathWithRequiredFlags>>
404                maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException {
405            if (mIncludes == null) {
406                maybeParseBackupSchemeLocked();
407            }
408            return mIncludes;
409        }
410
411        /**
412         * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the
413         * backup/restore set.
414         */
415        public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths()
416                throws IOException, XmlPullParserException {
417            if (mExcludes == null) {
418                maybeParseBackupSchemeLocked();
419            }
420            return mExcludes;
421        }
422
423        private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
424            // This not being null is how we know that we've tried to parse the xml already.
425            mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
426            mExcludes = new ArraySet<PathWithRequiredFlags>();
427
428            if (mFullBackupContent == 0) {
429                // android:fullBackupContent="true" which means that we'll do everything.
430                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
431                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
432                }
433            } else {
434                // android:fullBackupContent="@xml/some_resource".
435                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
436                    Log.v(FullBackup.TAG_XML_PARSER,
437                            "android:fullBackupContent - found xml resource");
438                }
439                XmlResourceParser parser = null;
440                try {
441                    parser = mPackageManager
442                            .getResourcesForApplication(mPackageName)
443                            .getXml(mFullBackupContent);
444                    parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
445                } catch (PackageManager.NameNotFoundException e) {
446                    // Throw it as an IOException
447                    throw new IOException(e);
448                } finally {
449                    if (parser != null) {
450                        parser.close();
451                    }
452                }
453            }
454        }
455
456        @VisibleForTesting
457        public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
458                                                   Set<PathWithRequiredFlags> excludes,
459                                                   Map<String, Set<PathWithRequiredFlags>> includes)
460                throws IOException, XmlPullParserException {
461            int event = parser.getEventType(); // START_DOCUMENT
462            while (event != XmlPullParser.START_TAG) {
463                event = parser.next();
464            }
465
466            if (!"full-backup-content".equals(parser.getName())) {
467                throw new XmlPullParserException("Xml file didn't start with correct tag" +
468                        " (<full-backup-content>). Found \"" + parser.getName() + "\"");
469            }
470
471            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
472                Log.v(TAG_XML_PARSER, "\n");
473                Log.v(TAG_XML_PARSER, "====================================================");
474                Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource.");
475                Log.v(TAG_XML_PARSER, "====================================================");
476                Log.v(TAG_XML_PARSER, "");
477            }
478
479            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
480                switch (event) {
481                    case XmlPullParser.START_TAG:
482                        validateInnerTagContents(parser);
483                        final String domainFromXml = parser.getAttributeValue(null, "domain");
484                        final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml);
485                        if (domainDirectory == null) {
486                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
487                                Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
488                                        + "domain=\"" + domainFromXml + "\" invalid; skipping");
489                            }
490                            break;
491                        }
492                        final File canonicalFile =
493                                extractCanonicalFile(domainDirectory,
494                                        parser.getAttributeValue(null, "path"));
495                        if (canonicalFile == null) {
496                            break;
497                        }
498
499                        int requiredFlags = 0; // no transport flags are required by default
500                        if (TAG_INCLUDE.equals(parser.getName())) {
501                            // requiredFlags are only supported for <include /> tag, for <exclude />
502                            // we should always leave them as the default = 0
503                            requiredFlags = getRequiredFlagsFromString(
504                                    parser.getAttributeValue(null, "requireFlags"));
505                        }
506
507                        // retrieve the include/exclude set we'll be adding this rule to
508                        Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain(
509                                parser, excludes, includes, domainFromXml);
510                        activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(),
511                                requiredFlags));
512                        if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
513                            Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
514                                    + " for domain \"" + domainFromXml + "\", requiredFlags + \""
515                                    + requiredFlags + "\"");
516                        }
517
518                        // Special case journal files (not dirs) for sqlite database. frowny-face.
519                        // Note that for a restore, the file is never a directory (b/c it doesn't
520                        // exist). We have no way of knowing a priori whether or not to expect a
521                        // dir, so we add the -journal anyway to be safe.
522                        if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
523                            final String canonicalJournalPath =
524                                    canonicalFile.getCanonicalPath() + "-journal";
525                            activeSet.add(new PathWithRequiredFlags(canonicalJournalPath,
526                                    requiredFlags));
527                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
528                                Log.v(TAG_XML_PARSER, "...automatically generated "
529                                        + canonicalJournalPath + ". Ignore if nonexistent.");
530                            }
531                            final String canonicalWalPath =
532                                    canonicalFile.getCanonicalPath() + "-wal";
533                            activeSet.add(new PathWithRequiredFlags(canonicalWalPath,
534                                    requiredFlags));
535                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
536                                Log.v(TAG_XML_PARSER, "...automatically generated "
537                                        + canonicalWalPath + ". Ignore if nonexistent.");
538                            }
539                        }
540
541                        // Special case for sharedpref files (not dirs) also add ".xml" suffix file.
542                        if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() &&
543                            !canonicalFile.getCanonicalPath().endsWith(".xml")) {
544                            final String canonicalXmlPath =
545                                    canonicalFile.getCanonicalPath() + ".xml";
546                            activeSet.add(new PathWithRequiredFlags(canonicalXmlPath,
547                                    requiredFlags));
548                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
549                                Log.v(TAG_XML_PARSER, "...automatically generated "
550                                        + canonicalXmlPath + ". Ignore if nonexistent.");
551                            }
552                        }
553                }
554            }
555            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
556                Log.v(TAG_XML_PARSER, "\n");
557                Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
558                Log.v(TAG_XML_PARSER, "Final tally.");
559                Log.v(TAG_XML_PARSER, "Includes:");
560                if (includes.isEmpty()) {
561                    Log.v(TAG_XML_PARSER, "  ...nothing specified (This means the entirety of app"
562                            + " data minus excludes)");
563                } else {
564                    for (Map.Entry<String, Set<PathWithRequiredFlags>> entry
565                            : includes.entrySet()) {
566                        Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
567                        for (PathWithRequiredFlags includeData : entry.getValue()) {
568                            Log.v(TAG_XML_PARSER, " path: " + includeData.getPath()
569                                    + " requiredFlags: " + includeData.getRequiredFlags());
570                        }
571                    }
572                }
573
574                Log.v(TAG_XML_PARSER, "Excludes:");
575                if (excludes.isEmpty()) {
576                    Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
577                } else {
578                    for (PathWithRequiredFlags excludeData : excludes) {
579                        Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath()
580                                + " requiredFlags: " + excludeData.getRequiredFlags());
581                    }
582                }
583
584                Log.v(TAG_XML_PARSER, "  ");
585                Log.v(TAG_XML_PARSER, "====================================================");
586                Log.v(TAG_XML_PARSER, "\n");
587            }
588        }
589
590        private int getRequiredFlagsFromString(String requiredFlags) {
591            int flags = 0;
592            if (requiredFlags == null || requiredFlags.length() == 0) {
593                // requiredFlags attribute was missing or empty in <include /> tag
594                return flags;
595            }
596            String[] flagsStr = requiredFlags.split("\\|");
597            for (String f : flagsStr) {
598                switch (f) {
599                    case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION:
600                        flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
601                        break;
602                    case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER:
603                        flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER;
604                        break;
605                    case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION:
606                        flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED;
607                    default:
608                        Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\"");
609                }
610            }
611            return flags;
612        }
613
614        private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser,
615                Set<PathWithRequiredFlags> excludes,
616                Map<String, Set<PathWithRequiredFlags>> includes, String domain)
617                throws XmlPullParserException {
618            if (TAG_INCLUDE.equals(parser.getName())) {
619                final String domainToken = getTokenForXmlDomain(domain);
620                Set<PathWithRequiredFlags> includeSet = includes.get(domainToken);
621                if (includeSet == null) {
622                    includeSet = new ArraySet<PathWithRequiredFlags>();
623                    includes.put(domainToken, includeSet);
624                }
625                return includeSet;
626            } else if (TAG_EXCLUDE.equals(parser.getName())) {
627                return excludes;
628            } else {
629                // Unrecognised tag => hard failure.
630                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
631                    Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
632                            + parser.getName() + "\"; aborting operation.");
633                }
634                throw new XmlPullParserException("Unrecognised tag in backup" +
635                        " criteria xml (" + parser.getName() + ")");
636            }
637        }
638
639        /**
640         * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
641         * BackupAgent internal data token.
642         * @return null if the xml domain was invalid.
643         */
644        private String getTokenForXmlDomain(String xmlDomain) {
645            if ("root".equals(xmlDomain)) {
646                return FullBackup.ROOT_TREE_TOKEN;
647            } else if ("file".equals(xmlDomain)) {
648                return FullBackup.FILES_TREE_TOKEN;
649            } else if ("database".equals(xmlDomain)) {
650                return FullBackup.DATABASE_TREE_TOKEN;
651            } else if ("sharedpref".equals(xmlDomain)) {
652                return FullBackup.SHAREDPREFS_TREE_TOKEN;
653            } else if ("device_root".equals(xmlDomain)) {
654                return FullBackup.DEVICE_ROOT_TREE_TOKEN;
655            } else if ("device_file".equals(xmlDomain)) {
656                return FullBackup.DEVICE_FILES_TREE_TOKEN;
657            } else if ("device_database".equals(xmlDomain)) {
658                return FullBackup.DEVICE_DATABASE_TREE_TOKEN;
659            } else if ("device_sharedpref".equals(xmlDomain)) {
660                return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
661            } else if ("external".equals(xmlDomain)) {
662                return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
663            } else {
664                return null;
665            }
666        }
667
668        /**
669         *
670         * @param domain Directory where the specified file should exist. Not null.
671         * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may
672         *                        be null.
673         * @return The canonical path of the file specified or null if no such file exists.
674         */
675        private File extractCanonicalFile(File domain, String filePathFromXml) {
676            if (filePathFromXml == null) {
677                // Allow things like <include domain="sharedpref"/>
678                filePathFromXml = "";
679            }
680            if (filePathFromXml.contains("..")) {
681                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
682                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
683                            + "\", but the \"..\" path is not permitted; skipping.");
684                }
685                return null;
686            }
687            if (filePathFromXml.contains("//")) {
688                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
689                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
690                            + "\", which contains the invalid \"//\" sequence; skipping.");
691                }
692                return null;
693            }
694            return new File(domain, filePathFromXml);
695        }
696
697        /**
698         * @param domain parsed from xml. Not sanitised before calling this function so may be null.
699         * @return The directory relevant to the domain specified.
700         */
701        private File getDirectoryForCriteriaDomain(String domain) {
702            if (TextUtils.isEmpty(domain)) {
703                return null;
704            }
705            if ("file".equals(domain)) {
706                return FILES_DIR;
707            } else if ("database".equals(domain)) {
708                return DATABASE_DIR;
709            } else if ("root".equals(domain)) {
710                return ROOT_DIR;
711            } else if ("sharedpref".equals(domain)) {
712                return SHAREDPREF_DIR;
713            } else if ("device_file".equals(domain)) {
714                return DEVICE_FILES_DIR;
715            } else if ("device_database".equals(domain)) {
716                return DEVICE_DATABASE_DIR;
717            } else if ("device_root".equals(domain)) {
718                return DEVICE_ROOT_DIR;
719            } else if ("device_sharedpref".equals(domain)) {
720                return DEVICE_SHAREDPREF_DIR;
721            } else if ("external".equals(domain)) {
722                return EXTERNAL_DIR;
723            } else {
724                return null;
725            }
726        }
727
728        /**
729         * Let's be strict about the type of xml the client can write. If we see anything untoward,
730         * throw an XmlPullParserException.
731         */
732        private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException {
733            if (parser == null) {
734                return;
735            }
736            switch (parser.getName()) {
737                case TAG_INCLUDE:
738                    if (parser.getAttributeCount() > 3) {
739                        throw new XmlPullParserException("At most 3 tag attributes allowed for "
740                                + "\"include\" tag (\"domain\" & \"path\""
741                                + " & optional \"requiredFlags\").");
742                    }
743                    break;
744                case TAG_EXCLUDE:
745                    if (parser.getAttributeCount() > 2) {
746                        throw new XmlPullParserException("At most 2 tag attributes allowed for "
747                                + "\"exclude\" tag (\"domain\" & \"path\".");
748                    }
749                    break;
750                default:
751                    throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
752                            " \"<exclude/>. You provided \"" + parser.getName() + "\"");
753            }
754        }
755    }
756}
757