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