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 */ backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output)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 getBackupScheme(Context context)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 getBackupSchemeForTest(Context context)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 */ restoreFile(ParcelFileDescriptor data, long size, int type, long mode, long mtime, File outFile)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 */ tokenToDirectoryPath(String domainToken)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 sharedDomainToPath(String domain)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 getVolumeList()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 PathWithRequiredFlags(String path, int requiredFlags)326 public PathWithRequiredFlags(String path, int requiredFlags) { 327 mPath = path; 328 mRequiredFlags = requiredFlags; 329 } 330 getPath()331 public String getPath() { 332 return mPath; 333 } 334 getRequiredFlags()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 BackupScheme(Context context)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 isFullBackupContentEnabled()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>> maybeParseAndGetCanonicalIncludePaths()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 */ maybeParseAndGetCanonicalExcludePaths()415 public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths() 416 throws IOException, XmlPullParserException { 417 if (mExcludes == null) { 418 maybeParseBackupSchemeLocked(); 419 } 420 return mExcludes; 421 } 422 maybeParseBackupSchemeLocked()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 parseBackupSchemeFromXmlLocked(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)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 getRequiredFlagsFromString(String requiredFlags)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 parseCurrentTagForDomain(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, String domain)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 */ getTokenForXmlDomain(String xmlDomain)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 */ extractCanonicalFile(File domain, String filePathFromXml)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 */ getDirectoryForCriteriaDomain(String domain)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 */ validateInnerTagContents(XmlPullParser parser)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