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    /**
86     * @hide
87     */
88    static public native int backupToTar(String packageName, String domain,
89            String linkdomain, String rootpath, String path, FullBackupDataOutput output);
90
91    private static final Map<String, BackupScheme> kPackageBackupSchemeMap =
92            new ArrayMap<String, BackupScheme>();
93
94    static synchronized BackupScheme getBackupScheme(Context context) {
95        BackupScheme backupSchemeForPackage =
96                kPackageBackupSchemeMap.get(context.getPackageName());
97        if (backupSchemeForPackage == null) {
98            backupSchemeForPackage = new BackupScheme(context);
99            kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage);
100        }
101        return backupSchemeForPackage;
102    }
103
104    public static BackupScheme getBackupSchemeForTest(Context context) {
105        BackupScheme testing = new BackupScheme(context);
106        testing.mExcludes = new ArraySet();
107        testing.mIncludes = new ArrayMap();
108        return testing;
109    }
110
111
112    /**
113     * Copy data from a socket to the given File location on permanent storage.  The
114     * modification time and access mode of the resulting file will be set if desired,
115     * although group/all rwx modes will be stripped: the restored file will not be
116     * accessible from outside the target application even if the original file was.
117     * If the {@code type} parameter indicates that the result should be a directory,
118     * the socket parameter may be {@code null}; even if it is valid, no data will be
119     * read from it in this case.
120     * <p>
121     * If the {@code mode} argument is negative, then the resulting output file will not
122     * have its access mode or last modification time reset as part of this operation.
123     *
124     * @param data Socket supplying the data to be copied to the output file.  If the
125     *    output is a directory, this may be {@code null}.
126     * @param size Number of bytes of data to copy from the socket to the file.  At least
127     *    this much data must be available through the {@code data} parameter.
128     * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
129     *    or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
130     * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
131     *    the output file or directory.  group/all rwx modes are stripped even if set
132     *    in this parameter.  If this parameter is negative then neither
133     *    the mode nor the mtime values will be applied to the restored file.
134     * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
135     *    last modification time of the output file.  if the {@code mode} parameter is
136     *    negative then this parameter will be ignored.
137     * @param outFile Location within the filesystem to place the data.  This must point
138     *    to a location that is writeable by the caller, preferably using an absolute path.
139     * @throws IOException
140     */
141    static public void restoreFile(ParcelFileDescriptor data,
142            long size, int type, long mode, long mtime, File outFile) throws IOException {
143        if (type == BackupAgent.TYPE_DIRECTORY) {
144            // Canonically a directory has no associated content, so we don't need to read
145            // anything from the pipe in this case.  Just create the directory here and
146            // drop down to the final metadata adjustment.
147            if (outFile != null) outFile.mkdirs();
148        } else {
149            FileOutputStream out = null;
150
151            // Pull the data from the pipe, copying it to the output file, until we're done
152            try {
153                if (outFile != null) {
154                    File parent = outFile.getParentFile();
155                    if (!parent.exists()) {
156                        // in practice this will only be for the default semantic directories,
157                        // and using the default mode for those is appropriate.
158                        // This can also happen for the case where a parent directory has been
159                        // excluded, but a file within that directory has been included.
160                        parent.mkdirs();
161                    }
162                    out = new FileOutputStream(outFile);
163                }
164            } catch (IOException e) {
165                Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
166            }
167
168            byte[] buffer = new byte[32 * 1024];
169            final long origSize = size;
170            FileInputStream in = new FileInputStream(data.getFileDescriptor());
171            while (size > 0) {
172                int toRead = (size > buffer.length) ? buffer.length : (int)size;
173                int got = in.read(buffer, 0, toRead);
174                if (got <= 0) {
175                    Log.w(TAG, "Incomplete read: expected " + size + " but got "
176                            + (origSize - size));
177                    break;
178                }
179                if (out != null) {
180                    try {
181                        out.write(buffer, 0, got);
182                    } catch (IOException e) {
183                        // Problem writing to the file.  Quit copying data and delete
184                        // the file, but of course keep consuming the input stream.
185                        Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
186                        out.close();
187                        out = null;
188                        outFile.delete();
189                    }
190                }
191                size -= got;
192            }
193            if (out != null) out.close();
194        }
195
196        // Now twiddle the state to match the backup, assuming all went well
197        if (mode >= 0 && outFile != null) {
198            try {
199                // explicitly prevent emplacement of files accessible by outside apps
200                mode &= 0700;
201                Os.chmod(outFile.getPath(), (int)mode);
202            } catch (ErrnoException e) {
203                e.rethrowAsIOException();
204            }
205            outFile.setLastModified(mtime);
206        }
207    }
208
209    @VisibleForTesting
210    public static class BackupScheme {
211        private final File FILES_DIR;
212        private final File DATABASE_DIR;
213        private final File ROOT_DIR;
214        private final File SHAREDPREF_DIR;
215        private final File CACHE_DIR;
216        private final File NOBACKUP_DIR;
217
218        private final File DEVICE_FILES_DIR;
219        private final File DEVICE_DATABASE_DIR;
220        private final File DEVICE_ROOT_DIR;
221        private final File DEVICE_SHAREDPREF_DIR;
222        private final File DEVICE_CACHE_DIR;
223        private final File DEVICE_NOBACKUP_DIR;
224
225        private final File EXTERNAL_DIR;
226
227        final int mFullBackupContent;
228        final PackageManager mPackageManager;
229        final StorageManager mStorageManager;
230        final String mPackageName;
231
232        // lazy initialized, only when needed
233        private StorageVolume[] mVolumes = null;
234
235        /**
236         * Parse out the semantic domains into the correct physical location.
237         */
238        String tokenToDirectoryPath(String domainToken) {
239            try {
240                if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
241                    return FILES_DIR.getCanonicalPath();
242                } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
243                    return DATABASE_DIR.getCanonicalPath();
244                } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
245                    return ROOT_DIR.getCanonicalPath();
246                } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
247                    return SHAREDPREF_DIR.getCanonicalPath();
248                } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
249                    return CACHE_DIR.getCanonicalPath();
250                } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
251                    return NOBACKUP_DIR.getCanonicalPath();
252                } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) {
253                    return DEVICE_FILES_DIR.getCanonicalPath();
254                } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) {
255                    return DEVICE_DATABASE_DIR.getCanonicalPath();
256                } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) {
257                    return DEVICE_ROOT_DIR.getCanonicalPath();
258                } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) {
259                    return DEVICE_SHAREDPREF_DIR.getCanonicalPath();
260                } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) {
261                    return DEVICE_CACHE_DIR.getCanonicalPath();
262                } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) {
263                    return DEVICE_NOBACKUP_DIR.getCanonicalPath();
264                } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
265                    if (EXTERNAL_DIR != null) {
266                        return EXTERNAL_DIR.getCanonicalPath();
267                    } else {
268                        return null;
269                    }
270                } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
271                    return sharedDomainToPath(domainToken);
272                }
273                // Not a supported location
274                Log.i(TAG, "Unrecognized domain " + domainToken);
275                return null;
276            } catch (Exception e) {
277                Log.i(TAG, "Error reading directory for domain: " + domainToken);
278                return null;
279            }
280
281        }
282
283        private String sharedDomainToPath(String domain) throws IOException {
284            // already known to start with SHARED_PREFIX, so we just look after that
285            final String volume = domain.substring(FullBackup.SHARED_PREFIX.length());
286            final StorageVolume[] volumes = getVolumeList();
287            final int volNum = Integer.parseInt(volume);
288            if (volNum < mVolumes.length) {
289                return volumes[volNum].getPathFile().getCanonicalPath();
290            }
291            return null;
292        }
293
294        private StorageVolume[] getVolumeList() {
295            if (mStorageManager != null) {
296                if (mVolumes == null) {
297                    mVolumes = mStorageManager.getVolumeList();
298                }
299            } else {
300                Log.e(TAG, "Unable to access Storage Manager");
301            }
302            return mVolumes;
303        }
304
305        /**
306        * A map of domain -> list of canonical file names in that domain that are to be included.
307        * We keep track of the domain so that we can go through the file system in order later on.
308        */
309        Map<String, Set<String>> mIncludes;
310        /**e
311         * List that will be populated with the canonical names of each file or directory that is
312         * to be excluded.
313         */
314        ArraySet<String> mExcludes;
315
316        BackupScheme(Context context) {
317            mFullBackupContent = context.getApplicationInfo().fullBackupContent;
318            mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
319            mPackageManager = context.getPackageManager();
320            mPackageName = context.getPackageName();
321
322            // System apps have control over where their default storage context
323            // is pointed, so we're always explicit when building paths.
324            final Context ceContext = context.createCredentialProtectedStorageContext();
325            FILES_DIR = ceContext.getFilesDir();
326            DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile();
327            ROOT_DIR = ceContext.getDataDir();
328            SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile();
329            CACHE_DIR = ceContext.getCacheDir();
330            NOBACKUP_DIR = ceContext.getNoBackupFilesDir();
331
332            final Context deContext = context.createDeviceProtectedStorageContext();
333            DEVICE_FILES_DIR = deContext.getFilesDir();
334            DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile();
335            DEVICE_ROOT_DIR = deContext.getDataDir();
336            DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile();
337            DEVICE_CACHE_DIR = deContext.getCacheDir();
338            DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir();
339
340            if (android.os.Process.myUid() != Process.SYSTEM_UID) {
341                EXTERNAL_DIR = context.getExternalFilesDir(null);
342            } else {
343                EXTERNAL_DIR = null;
344            }
345        }
346
347        boolean isFullBackupContentEnabled() {
348            if (mFullBackupContent < 0) {
349                // android:fullBackupContent="false", bail.
350                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
351                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
352                }
353                return false;
354            }
355            return true;
356        }
357
358        /**
359         * @return A mapping of domain -> canonical paths within that domain. Each of these paths
360         * specifies a file that the client has explicitly included in their backup set. If this
361         * map is empty we will back up the entire data directory (including managed external
362         * storage).
363         */
364        public synchronized Map<String, Set<String>> maybeParseAndGetCanonicalIncludePaths()
365                throws IOException, XmlPullParserException {
366            if (mIncludes == null) {
367                maybeParseBackupSchemeLocked();
368            }
369            return mIncludes;
370        }
371
372        /**
373         * @return A set of canonical paths that are to be excluded from the backup/restore set.
374         */
375        public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths()
376                throws IOException, XmlPullParserException {
377            if (mExcludes == null) {
378                maybeParseBackupSchemeLocked();
379            }
380            return mExcludes;
381        }
382
383        private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
384            // This not being null is how we know that we've tried to parse the xml already.
385            mIncludes = new ArrayMap<String, Set<String>>();
386            mExcludes = new ArraySet<String>();
387
388            if (mFullBackupContent == 0) {
389                // android:fullBackupContent="true" which means that we'll do everything.
390                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
391                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
392                }
393            } else {
394                // android:fullBackupContent="@xml/some_resource".
395                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
396                    Log.v(FullBackup.TAG_XML_PARSER,
397                            "android:fullBackupContent - found xml resource");
398                }
399                XmlResourceParser parser = null;
400                try {
401                    parser = mPackageManager
402                            .getResourcesForApplication(mPackageName)
403                            .getXml(mFullBackupContent);
404                    parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
405                } catch (PackageManager.NameNotFoundException e) {
406                    // Throw it as an IOException
407                    throw new IOException(e);
408                } finally {
409                    if (parser != null) {
410                        parser.close();
411                    }
412                }
413            }
414        }
415
416        @VisibleForTesting
417        public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
418                                                   Set<String> excludes,
419                                                   Map<String, Set<String>> includes)
420                throws IOException, XmlPullParserException {
421            int event = parser.getEventType(); // START_DOCUMENT
422            while (event != XmlPullParser.START_TAG) {
423                event = parser.next();
424            }
425
426            if (!"full-backup-content".equals(parser.getName())) {
427                throw new XmlPullParserException("Xml file didn't start with correct tag" +
428                        " (<full-backup-content>). Found \"" + parser.getName() + "\"");
429            }
430
431            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
432                Log.v(TAG_XML_PARSER, "\n");
433                Log.v(TAG_XML_PARSER, "====================================================");
434                Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource.");
435                Log.v(TAG_XML_PARSER, "====================================================");
436                Log.v(TAG_XML_PARSER, "");
437            }
438
439            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
440                switch (event) {
441                    case XmlPullParser.START_TAG:
442                        validateInnerTagContents(parser);
443                        final String domainFromXml = parser.getAttributeValue(null, "domain");
444                        final File domainDirectory =
445                                getDirectoryForCriteriaDomain(domainFromXml);
446                        if (domainDirectory == null) {
447                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
448                                Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
449                                        + "domain=\"" + domainFromXml + "\" invalid; skipping");
450                            }
451                            break;
452                        }
453                        final File canonicalFile =
454                                extractCanonicalFile(domainDirectory,
455                                        parser.getAttributeValue(null, "path"));
456                        if (canonicalFile == null) {
457                            break;
458                        }
459
460                        Set<String> activeSet = parseCurrentTagForDomain(
461                                parser, excludes, includes, domainFromXml);
462                        activeSet.add(canonicalFile.getCanonicalPath());
463                        if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
464                            Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
465                                    + " for domain \"" + domainFromXml + "\"");
466                        }
467
468                        // Special case journal files (not dirs) for sqlite database. frowny-face.
469                        // Note that for a restore, the file is never a directory (b/c it doesn't
470                        // exist). We have no way of knowing a priori whether or not to expect a
471                        // dir, so we add the -journal anyway to be safe.
472                        if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
473                            final String canonicalJournalPath =
474                                    canonicalFile.getCanonicalPath() + "-journal";
475                            activeSet.add(canonicalJournalPath);
476                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
477                                Log.v(TAG_XML_PARSER, "...automatically generated "
478                                        + canonicalJournalPath + ". Ignore if nonexistent.");
479                            }
480                            final String canonicalWalPath =
481                                    canonicalFile.getCanonicalPath() + "-wal";
482                            activeSet.add(canonicalWalPath);
483                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
484                                Log.v(TAG_XML_PARSER, "...automatically generated "
485                                        + canonicalWalPath + ". Ignore if nonexistent.");
486                            }
487                        }
488
489                        // Special case for sharedpref files (not dirs) also add ".xml" suffix file.
490                        if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() &&
491                            !canonicalFile.getCanonicalPath().endsWith(".xml")) {
492                            final String canonicalXmlPath =
493                                    canonicalFile.getCanonicalPath() + ".xml";
494                            activeSet.add(canonicalXmlPath);
495                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
496                                Log.v(TAG_XML_PARSER, "...automatically generated "
497                                        + canonicalXmlPath + ". Ignore if nonexistent.");
498                            }
499                        }
500                }
501            }
502            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
503                Log.v(TAG_XML_PARSER, "\n");
504                Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
505                Log.v(TAG_XML_PARSER, "Final tally.");
506                Log.v(TAG_XML_PARSER, "Includes:");
507                if (includes.isEmpty()) {
508                    Log.v(TAG_XML_PARSER, "  ...nothing specified (This means the entirety of app"
509                            + " data minus excludes)");
510                } else {
511                    for (Map.Entry<String, Set<String>> entry : includes.entrySet()) {
512                        Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
513                        for (String includeData : entry.getValue()) {
514                            Log.v(TAG_XML_PARSER, "  " + includeData);
515                        }
516                    }
517                }
518
519                Log.v(TAG_XML_PARSER, "Excludes:");
520                if (excludes.isEmpty()) {
521                    Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
522                } else {
523                    for (String excludeData : excludes) {
524                        Log.v(TAG_XML_PARSER, "  " + excludeData);
525                    }
526                }
527
528                Log.v(TAG_XML_PARSER, "  ");
529                Log.v(TAG_XML_PARSER, "====================================================");
530                Log.v(TAG_XML_PARSER, "\n");
531            }
532        }
533
534        private Set<String> parseCurrentTagForDomain(XmlPullParser parser,
535                                                     Set<String> excludes,
536                                                     Map<String, Set<String>> includes,
537                                                     String domain)
538                throws XmlPullParserException {
539            if ("include".equals(parser.getName())) {
540                final String domainToken = getTokenForXmlDomain(domain);
541                Set<String> includeSet = includes.get(domainToken);
542                if (includeSet == null) {
543                    includeSet = new ArraySet<String>();
544                    includes.put(domainToken, includeSet);
545                }
546                return includeSet;
547            } else if ("exclude".equals(parser.getName())) {
548                return excludes;
549            } else {
550                // Unrecognised tag => hard failure.
551                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
552                    Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
553                            + parser.getName() + "\"; aborting operation.");
554                }
555                throw new XmlPullParserException("Unrecognised tag in backup" +
556                        " criteria xml (" + parser.getName() + ")");
557            }
558        }
559
560        /**
561         * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
562         * BackupAgent internal data token.
563         * @return null if the xml domain was invalid.
564         */
565        private String getTokenForXmlDomain(String xmlDomain) {
566            if ("root".equals(xmlDomain)) {
567                return FullBackup.ROOT_TREE_TOKEN;
568            } else if ("file".equals(xmlDomain)) {
569                return FullBackup.FILES_TREE_TOKEN;
570            } else if ("database".equals(xmlDomain)) {
571                return FullBackup.DATABASE_TREE_TOKEN;
572            } else if ("sharedpref".equals(xmlDomain)) {
573                return FullBackup.SHAREDPREFS_TREE_TOKEN;
574            } else if ("device_root".equals(xmlDomain)) {
575                return FullBackup.DEVICE_ROOT_TREE_TOKEN;
576            } else if ("device_file".equals(xmlDomain)) {
577                return FullBackup.DEVICE_FILES_TREE_TOKEN;
578            } else if ("device_database".equals(xmlDomain)) {
579                return FullBackup.DEVICE_DATABASE_TREE_TOKEN;
580            } else if ("device_sharedpref".equals(xmlDomain)) {
581                return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
582            } else if ("external".equals(xmlDomain)) {
583                return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
584            } else {
585                return null;
586            }
587        }
588
589        /**
590         *
591         * @param domain Directory where the specified file should exist. Not null.
592         * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may be
593         *                        null.
594         * @return The canonical path of the file specified or null if no such file exists.
595         */
596        private File extractCanonicalFile(File domain, String filePathFromXml) {
597            if (filePathFromXml == null) {
598                // Allow things like <include domain="sharedpref"/>
599                filePathFromXml = "";
600            }
601            if (filePathFromXml.contains("..")) {
602                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
603                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
604                            + "\", but the \"..\" path is not permitted; skipping.");
605                }
606                return null;
607            }
608            if (filePathFromXml.contains("//")) {
609                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
610                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
611                            + "\", which contains the invalid \"//\" sequence; skipping.");
612                }
613                return null;
614            }
615            return new File(domain, filePathFromXml);
616        }
617
618        /**
619         * @param domain parsed from xml. Not sanitised before calling this function so may be null.
620         * @return The directory relevant to the domain specified.
621         */
622        private File getDirectoryForCriteriaDomain(String domain) {
623            if (TextUtils.isEmpty(domain)) {
624                return null;
625            }
626            if ("file".equals(domain)) {
627                return FILES_DIR;
628            } else if ("database".equals(domain)) {
629                return DATABASE_DIR;
630            } else if ("root".equals(domain)) {
631                return ROOT_DIR;
632            } else if ("sharedpref".equals(domain)) {
633                return SHAREDPREF_DIR;
634            } else if ("device_file".equals(domain)) {
635                return DEVICE_FILES_DIR;
636            } else if ("device_database".equals(domain)) {
637                return DEVICE_DATABASE_DIR;
638            } else if ("device_root".equals(domain)) {
639                return DEVICE_ROOT_DIR;
640            } else if ("device_sharedpref".equals(domain)) {
641                return DEVICE_SHAREDPREF_DIR;
642            } else if ("external".equals(domain)) {
643                return EXTERNAL_DIR;
644            } else {
645                return null;
646            }
647        }
648
649        /**
650         * Let's be strict about the type of xml the client can write. If we see anything untoward,
651         * throw an XmlPullParserException.
652         */
653        private void validateInnerTagContents(XmlPullParser parser)
654                throws XmlPullParserException {
655            if (parser.getAttributeCount() > 2) {
656                throw new XmlPullParserException("At most 2 tag attributes allowed for \""
657                        + parser.getName() + "\" tag (\"domain\" & \"path\".");
658            }
659            if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) {
660                throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
661                        " \"<exclude/>. You provided \"" + parser.getName() + "\"");
662            }
663        }
664    }
665}
666