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