blob: b7a8da59b1707ce68e03592c558d2620e272a6d5 [file] [log] [blame]
Christopher Tate4a627c72011-04-01 14:43:32 -07001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.app.backup;
18
Matthew Williams303650c2015-04-17 18:22:51 -070019import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.XmlResourceParser;
Jeff Sharkey8a372a02016-03-16 16:25:45 -060022import android.os.ParcelFileDescriptor;
Matthew Williams303650c2015-04-17 18:22:51 -070023import android.os.Process;
Christopher Tate60af5942016-08-04 17:13:25 -070024import android.os.storage.StorageManager;
25import android.os.storage.StorageVolume;
Elliott Hughes34385d32014-04-28 11:11:32 -070026import android.system.ErrnoException;
27import android.system.Os;
Matthew Williams303650c2015-04-17 18:22:51 -070028import android.text.TextUtils;
29import android.util.ArrayMap;
30import android.util.ArraySet;
Christopher Tate75a99702011-05-18 16:28:19 -070031import android.util.Log;
32
Matthew Williams303650c2015-04-17 18:22:51 -070033import com.android.internal.annotations.VisibleForTesting;
34
35import org.xmlpull.v1.XmlPullParser;
Jeff Sharkey8a372a02016-03-16 16:25:45 -060036import org.xmlpull.v1.XmlPullParserException;
Matthew Williams303650c2015-04-17 18:22:51 -070037
Christopher Tate75a99702011-05-18 16:28:19 -070038import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileOutputStream;
41import java.io.IOException;
Matthew Williams303650c2015-04-17 18:22:51 -070042import java.util.Map;
43import java.util.Set;
Christopher Tate75a99702011-05-18 16:28:19 -070044
Christopher Tate4a627c72011-04-01 14:43:32 -070045/**
46 * Global constant definitions et cetera related to the full-backup-to-fd
Christopher Tate79ec80d2011-06-24 14:58:49 -070047 * 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.
Christopher Tate4a627c72011-04-01 14:43:32 -070049 *
50 * @hide
51 */
52public class FullBackup {
Christopher Tate75a99702011-05-18 16:28:19 -070053 static final String TAG = "FullBackup";
Matthew Williams303650c2015-04-17 18:22:51 -070054 /** Enable this log tag to get verbose information while parsing the client xml. */
55 static final String TAG_XML_PARSER = "BackupXmlParserLogging";
Christopher Tate4a627c72011-04-01 14:43:32 -070056
Christopher Tate75a99702011-05-18 16:28:19 -070057 public static final String APK_TREE_TOKEN = "a";
58 public static final String OBB_TREE_TOKEN = "obb";
Johan Toras Halsethb59a4b82017-03-03 15:37:43 +000059 public static final String KEY_VALUE_DATA_TOKEN = "k";
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -070060
Christopher Tate75a99702011-05-18 16:28:19 -070061 public static final String ROOT_TREE_TOKEN = "r";
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -070062 public static final String FILES_TREE_TOKEN = "f";
Christopher Tatea7835b62014-07-11 17:25:57 -070063 public static final String NO_BACKUP_TREE_TOKEN = "nb";
Christopher Tate75a99702011-05-18 16:28:19 -070064 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";
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -070067
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";
Christopher Tate75a99702011-05-18 16:28:19 -070076 public static final String SHARED_STORAGE_TOKEN = "shared";
77
78 public static final String APPS_PREFIX = "apps/";
Christopher Tateb0628bf2011-06-02 15:08:13 -070079 public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
Christopher Tate75a99702011-05-18 16:28:19 -070080
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
Michal Karpinskib5e09312018-02-19 13:55:23 +000085 public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption";
86 public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer";
Robert Berry39cf42c2018-03-28 15:56:17 +010087 public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION =
88 "fakeClientSideEncryption";
Michal Karpinskib5e09312018-02-19 13:55:23 +000089
Christopher Tate79ec80d2011-06-24 14:58:49 -070090 /**
91 * @hide
92 */
Christopher Tate4a627c72011-04-01 14:43:32 -070093 static public native int backupToTar(String packageName, String domain,
Christopher Tate11ae7682015-03-24 18:48:10 -070094 String linkdomain, String rootpath, String path, FullBackupDataOutput output);
Christopher Tate75a99702011-05-18 16:28:19 -070095
Matthew Williams303650c2015-04-17 18:22:51 -070096 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
Christopher Tate79ec80d2011-06-24 14:58:49 -0700117 /**
118 * Copy data from a socket to the given File location on permanent storage. The
Christopher Tatef6d6fa82012-09-26 15:25:59 -0700119 * 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.
Christopher Tate79ec80d2011-06-24 14:58:49 -0700122 * 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
Christopher Tatef6d6fa82012-09-26 15:25:59 -0700136 * 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.
Christopher Tate79ec80d2011-06-24 14:58:49 -0700139 * @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
Christopher Tate46cc43c2013-02-19 14:08:59 -0800143 * to a location that is writeable by the caller, preferably using an absolute path.
Christopher Tate79ec80d2011-06-24 14:58:49 -0700144 * @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) {
Christopher Tate75a99702011-05-18 16:28:19 -0700149 // 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.
Matthew Williams303650c2015-04-17 18:22:51 -0700163 // This can also happen for the case where a parent directory has been
164 // excluded, but a file within that directory has been included.
Christopher Tate75a99702011-05-18 16:28:19 -0700165 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
Christopher Tate79ec80d2011-06-24 14:58:49 -0700202 if (mode >= 0 && outFile != null) {
Christopher Tate75a99702011-05-18 16:28:19 -0700203 try {
Christopher Tatef6d6fa82012-09-26 15:25:59 -0700204 // explicitly prevent emplacement of files accessible by outside apps
205 mode &= 0700;
Elliott Hughes34385d32014-04-28 11:11:32 -0700206 Os.chmod(outFile.getPath(), (int)mode);
Christopher Tate75a99702011-05-18 16:28:19 -0700207 } catch (ErrnoException e) {
208 e.rethrowAsIOException();
209 }
210 outFile.setLastModified(mtime);
211 }
212 }
Matthew Williams303650c2015-04-17 18:22:51 -0700213
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;
Matthew Williams303650c2015-04-17 18:22:51 -0700220 private final File CACHE_DIR;
221 private final File NOBACKUP_DIR;
222
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700223 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
Michal Karpinskib5e09312018-02-19 13:55:23 +0000232 private final static String TAG_INCLUDE = "include";
233 private final static String TAG_EXCLUDE = "exclude";
234
Matthew Williams303650c2015-04-17 18:22:51 -0700235 final int mFullBackupContent;
236 final PackageManager mPackageManager;
Christopher Tate60af5942016-08-04 17:13:25 -0700237 final StorageManager mStorageManager;
Matthew Williams303650c2015-04-17 18:22:51 -0700238 final String mPackageName;
239
Christopher Tate60af5942016-08-04 17:13:25 -0700240 // lazy initialized, only when needed
241 private StorageVolume[] mVolumes = null;
242
Matthew Williams303650c2015-04-17 18:22:51 -0700243 /**
244 * Parse out the semantic domains into the correct physical location.
245 */
246 String tokenToDirectoryPath(String domainToken) {
247 try {
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700248 if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
Matthew Williams303650c2015-04-17 18:22:51 -0700249 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();
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700258 } 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();
Matthew Williams303650c2015-04-17 18:22:51 -0700272 } 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 }
Christopher Tate60af5942016-08-04 17:13:25 -0700278 } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
279 return sharedDomainToPath(domainToken);
Matthew Williams303650c2015-04-17 18:22:51 -0700280 }
281 // Not a supported location
282 Log.i(TAG, "Unrecognized domain " + domainToken);
283 return null;
Christopher Tate60af5942016-08-04 17:13:25 -0700284 } catch (Exception e) {
Matthew Williams303650c2015-04-17 18:22:51 -0700285 Log.i(TAG, "Error reading directory for domain: " + domainToken);
286 return null;
287 }
288
289 }
Christopher Tate60af5942016-08-04 17:13:25 -0700290
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
Matthew Williams303650c2015-04-17 18:22:51 -0700313 /**
Michal Karpinskib5e09312018-02-19 13:55:23 +0000314 * 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.
Matthew Williams303650c2015-04-17 18:22:51 -0700321 */
Michal Karpinskib5e09312018-02-19 13:55:23 +0000322 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;
Matthew Williams303650c2015-04-17 18:22:51 -0700353
354 BackupScheme(Context context) {
355 mFullBackupContent = context.getApplicationInfo().fullBackupContent;
Christopher Tate60af5942016-08-04 17:13:25 -0700356 mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Matthew Williams303650c2015-04-17 18:22:51 -0700357 mPackageManager = context.getPackageManager();
358 mPackageName = context.getPackageName();
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700359
360 // System apps have control over where their default storage context
361 // is pointed, so we're always explicit when building paths.
Jeff Sharkey8a372a02016-03-16 16:25:45 -0600362 final Context ceContext = context.createCredentialProtectedStorageContext();
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700363 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
Jeff Sharkey8a372a02016-03-16 16:25:45 -0600370 final Context deContext = context.createDeviceProtectedStorageContext();
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700371 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
Matthew Williams303650c2015-04-17 18:22:51 -0700378 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 /**
Michal Karpinskib5e09312018-02-19 13:55:23 +0000397 * @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).
Matthew Williams303650c2015-04-17 18:22:51 -0700402 */
Michal Karpinskib5e09312018-02-19 13:55:23 +0000403 public synchronized Map<String, Set<PathWithRequiredFlags>>
404 maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException {
Matthew Williams303650c2015-04-17 18:22:51 -0700405 if (mIncludes == null) {
406 maybeParseBackupSchemeLocked();
407 }
408 return mIncludes;
409 }
410
411 /**
Michal Karpinskib5e09312018-02-19 13:55:23 +0000412 * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the
413 * backup/restore set.
Matthew Williams303650c2015-04-17 18:22:51 -0700414 */
Michal Karpinskib5e09312018-02-19 13:55:23 +0000415 public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths()
Matthew Williams303650c2015-04-17 18:22:51 -0700416 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.
Michal Karpinskib5e09312018-02-19 13:55:23 +0000425 mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
426 mExcludes = new ArraySet<PathWithRequiredFlags>();
Matthew Williams303650c2015-04-17 18:22:51 -0700427
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,
Michal Karpinskib5e09312018-02-19 13:55:23 +0000458 Set<PathWithRequiredFlags> excludes,
459 Map<String, Set<PathWithRequiredFlags>> includes)
Matthew Williams303650c2015-04-17 18:22:51 -0700460 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");
Michal Karpinskib5e09312018-02-19 13:55:23 +0000484 final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml);
Matthew Williams303650c2015-04-17 18:22:51 -0700485 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
Michal Karpinskib5e09312018-02-19 13:55:23 +0000499 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(
Matthew Williams303650c2015-04-17 18:22:51 -0700509 parser, excludes, includes, domainFromXml);
Michal Karpinskib5e09312018-02-19 13:55:23 +0000510 activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(),
511 requiredFlags));
Matthew Williams303650c2015-04-17 18:22:51 -0700512 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
513 Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
Michal Karpinskib5e09312018-02-19 13:55:23 +0000514 + " for domain \"" + domainFromXml + "\", requiredFlags + \""
515 + requiredFlags + "\"");
Matthew Williams303650c2015-04-17 18:22:51 -0700516 }
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";
Michal Karpinskib5e09312018-02-19 13:55:23 +0000525 activeSet.add(new PathWithRequiredFlags(canonicalJournalPath,
526 requiredFlags));
Matthew Williams303650c2015-04-17 18:22:51 -0700527 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
528 Log.v(TAG_XML_PARSER, "...automatically generated "
Dmitry Polukhin80e8db32015-08-05 14:44:18 +0200529 + canonicalJournalPath + ". Ignore if nonexistent.");
530 }
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700531 final String canonicalWalPath =
532 canonicalFile.getCanonicalPath() + "-wal";
Michal Karpinskib5e09312018-02-19 13:55:23 +0000533 activeSet.add(new PathWithRequiredFlags(canonicalWalPath,
534 requiredFlags));
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700535 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
536 Log.v(TAG_XML_PARSER, "...automatically generated "
537 + canonicalWalPath + ". Ignore if nonexistent.");
538 }
Dmitry Polukhin80e8db32015-08-05 14:44:18 +0200539 }
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";
Michal Karpinskib5e09312018-02-19 13:55:23 +0000546 activeSet.add(new PathWithRequiredFlags(canonicalXmlPath,
547 requiredFlags));
Dmitry Polukhin80e8db32015-08-05 14:44:18 +0200548 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
549 Log.v(TAG_XML_PARSER, "...automatically generated "
550 + canonicalXmlPath + ". Ignore if nonexistent.");
Matthew Williams303650c2015-04-17 18:22:51 -0700551 }
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 {
Michal Karpinskib5e09312018-02-19 13:55:23 +0000564 for (Map.Entry<String, Set<PathWithRequiredFlags>> entry
565 : includes.entrySet()) {
Matthew Williams303650c2015-04-17 18:22:51 -0700566 Log.v(TAG_XML_PARSER, " domain=" + entry.getKey());
Michal Karpinskib5e09312018-02-19 13:55:23 +0000567 for (PathWithRequiredFlags includeData : entry.getValue()) {
568 Log.v(TAG_XML_PARSER, " path: " + includeData.getPath()
569 + " requiredFlags: " + includeData.getRequiredFlags());
Matthew Williams303650c2015-04-17 18:22:51 -0700570 }
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 {
Michal Karpinskib5e09312018-02-19 13:55:23 +0000578 for (PathWithRequiredFlags excludeData : excludes) {
579 Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath()
580 + " requiredFlags: " + excludeData.getRequiredFlags());
Matthew Williams303650c2015-04-17 18:22:51 -0700581 }
582 }
583
584 Log.v(TAG_XML_PARSER, " ");
585 Log.v(TAG_XML_PARSER, "====================================================");
586 Log.v(TAG_XML_PARSER, "\n");
587 }
588 }
589
Michal Karpinskib5e09312018-02-19 13:55:23 +0000590 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;
Robert Berry39cf42c2018-03-28 15:56:17 +0100605 case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION:
606 flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED;
Michal Karpinskib5e09312018-02-19 13:55:23 +0000607 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)
Matthew Williams303650c2015-04-17 18:22:51 -0700617 throws XmlPullParserException {
Michal Karpinskib5e09312018-02-19 13:55:23 +0000618 if (TAG_INCLUDE.equals(parser.getName())) {
Matthew Williams303650c2015-04-17 18:22:51 -0700619 final String domainToken = getTokenForXmlDomain(domain);
Michal Karpinskib5e09312018-02-19 13:55:23 +0000620 Set<PathWithRequiredFlags> includeSet = includes.get(domainToken);
Matthew Williams303650c2015-04-17 18:22:51 -0700621 if (includeSet == null) {
Michal Karpinskib5e09312018-02-19 13:55:23 +0000622 includeSet = new ArraySet<PathWithRequiredFlags>();
Matthew Williams303650c2015-04-17 18:22:51 -0700623 includes.put(domainToken, includeSet);
624 }
625 return includeSet;
Michal Karpinskib5e09312018-02-19 13:55:23 +0000626 } else if (TAG_EXCLUDE.equals(parser.getName())) {
Matthew Williams303650c2015-04-17 18:22:51 -0700627 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)) {
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700648 return FullBackup.FILES_TREE_TOKEN;
Matthew Williams303650c2015-04-17 18:22:51 -0700649 } else if ("database".equals(xmlDomain)) {
650 return FullBackup.DATABASE_TREE_TOKEN;
651 } else if ("sharedpref".equals(xmlDomain)) {
652 return FullBackup.SHAREDPREFS_TREE_TOKEN;
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700653 } 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;
Matthew Williams303650c2015-04-17 18:22:51 -0700661 } 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.
Michal Karpinskib5e09312018-02-19 13:55:23 +0000671 * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may
672 * be null.
Matthew Williams303650c2015-04-17 18:22:51 -0700673 * @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;
Jeff Sharkey2c1ba9a2016-02-17 15:29:38 -0700713 } 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;
Matthew Williams303650c2015-04-17 18:22:51 -0700721 } 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 */
Michal Karpinskib5e09312018-02-19 13:55:23 +0000732 private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException {
733 if (parser == null) {
734 return;
Matthew Williams303650c2015-04-17 18:22:51 -0700735 }
Michal Karpinskib5e09312018-02-19 13:55:23 +0000736 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() + "\"");
Matthew Williams303650c2015-04-17 18:22:51 -0700753 }
754 }
755 }
Christopher Tate4a627c72011-04-01 14:43:32 -0700756}