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     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