BackupAgent.java revision b848b7a99c81081723077d7fb2006b2a35760259
1/* 2 * Copyright (C) 2009 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 19import android.app.IBackupAgent; 20import android.app.backup.IBackupManager; 21import android.content.Context; 22import android.content.ContextWrapper; 23import android.content.pm.ApplicationInfo; 24import android.os.Binder; 25import android.os.IBinder; 26import android.os.ParcelFileDescriptor; 27import android.os.RemoteException; 28import android.util.Log; 29 30import java.io.File; 31import java.io.IOException; 32import java.util.HashSet; 33import java.util.LinkedList; 34 35import libcore.io.ErrnoException; 36import libcore.io.Libcore; 37import libcore.io.OsConstants; 38import libcore.io.StructStat; 39 40/** 41 * Provides the central interface between an 42 * application and Android's data backup infrastructure. An application that wishes 43 * to participate in the backup and restore mechanism will declare a subclass of 44 * {@link android.app.backup.BackupAgent}, implement the 45 * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} 46 * and {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} methods, 47 * and provide the name of its backup agent class in its {@code AndroidManifest.xml} file via 48 * the <code><a 49 * href="{@docRoot}guide/topics/manifest/application-element.html"><application></a></code> 50 * tag's {@code android:backupAgent} attribute. 51 * <h3>Basic Operation</h3> 52 * <p> 53 * When the application makes changes to data that it wishes to keep backed up, 54 * it should call the 55 * {@link android.app.backup.BackupManager#dataChanged() BackupManager.dataChanged()} method. 56 * This notifies the Android Backup Manager that the application needs an opportunity 57 * to update its backup image. The Backup Manager, in turn, schedules a 58 * backup pass to be performed at an opportune time. 59 * <p> 60 * Restore operations are typically performed only when applications are first 61 * installed on a device. At that time, the operating system checks to see whether 62 * there is a previously-saved data set available for the application being installed, and if so, 63 * begins an immediate restore pass to deliver the backup data as part of the installation 64 * process. 65 * <p> 66 * When a backup or restore pass is run, the application's process is launched 67 * (if not already running), the manifest-declared backup agent class (in the {@code 68 * android:backupAgent} attribute) is instantiated within 69 * that process, and the agent's {@link #onCreate()} method is invoked. This prepares the 70 * agent instance to run the actual backup or restore logic. At this point the 71 * agent's 72 * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor) onBackup()} or 73 * {@link #onRestore(BackupDataInput, int, ParcelFileDescriptor) onRestore()} method will be 74 * invoked as appropriate for the operation being performed. 75 * <p> 76 * A backup data set consists of one or more "entities," flattened binary data 77 * records that are each identified with a key string unique within the data set. Adding a 78 * record to the active data set or updating an existing record is done by simply 79 * writing new entity data under the desired key. Deleting an entity from the data set 80 * is done by writing an entity under that key with header specifying a negative data 81 * size, and no actual entity data. 82 * <p> 83 * <b>Helper Classes</b> 84 * <p> 85 * An extensible agent based on convenient helper classes is available in 86 * {@link android.app.backup.BackupAgentHelper}. That class is particularly 87 * suited to handling of simple file or {@link android.content.SharedPreferences} 88 * backup and restore. 89 * 90 * @see android.app.backup.BackupManager 91 * @see android.app.backup.BackupAgentHelper 92 * @see android.app.backup.BackupDataInput 93 * @see android.app.backup.BackupDataOutput 94 */ 95public abstract class BackupAgent extends ContextWrapper { 96 private static final String TAG = "BackupAgent"; 97 private static final boolean DEBUG = true; 98 99 /** @hide */ 100 public static final int TYPE_EOF = 0; 101 102 /** 103 * During a full restore, indicates that the file system object being restored 104 * is an ordinary file. 105 */ 106 public static final int TYPE_FILE = 1; 107 108 /** 109 * During a full restore, indicates that the file system object being restored 110 * is a directory. 111 */ 112 public static final int TYPE_DIRECTORY = 2; 113 114 /** @hide */ 115 public static final int TYPE_SYMLINK = 3; 116 117 public BackupAgent() { 118 super(null); 119 } 120 121 /** 122 * Provided as a convenience for agent implementations that need an opportunity 123 * to do one-time initialization before the actual backup or restore operation 124 * is begun. 125 * <p> 126 * Agents do not need to override this method. 127 */ 128 public void onCreate() { 129 } 130 131 /** 132 * Provided as a convenience for agent implementations that need to do some 133 * sort of shutdown process after backup or restore is completed. 134 * <p> 135 * Agents do not need to override this method. 136 */ 137 public void onDestroy() { 138 } 139 140 /** 141 * The application is being asked to write any data changed since the last 142 * time it performed a backup operation. The state data recorded during the 143 * last backup pass is provided in the <code>oldState</code> file 144 * descriptor. If <code>oldState</code> is <code>null</code>, no old state 145 * is available and the application should perform a full backup. In both 146 * cases, a representation of the final backup state after this pass should 147 * be written to the file pointed to by the file descriptor wrapped in 148 * <code>newState</code>. 149 * <p> 150 * Each entity written to the {@link android.app.backup.BackupDataOutput} 151 * <code>data</code> stream will be transmitted 152 * over the current backup transport and stored in the remote data set under 153 * the key supplied as part of the entity. Writing an entity with a negative 154 * data size instructs the transport to delete whatever entity currently exists 155 * under that key from the remote data set. 156 * 157 * @param oldState An open, read-only ParcelFileDescriptor pointing to the 158 * last backup state provided by the application. May be 159 * <code>null</code>, in which case no prior state is being 160 * provided and the application should perform a full backup. 161 * @param data A structured wrapper around an open, read/write 162 * file descriptor pointing to the backup data destination. 163 * Typically the application will use backup helper classes to 164 * write to this file. 165 * @param newState An open, read/write ParcelFileDescriptor pointing to an 166 * empty file. The application should record the final backup 167 * state here after writing the requested data to the <code>data</code> 168 * output stream. 169 */ 170 public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 171 ParcelFileDescriptor newState) throws IOException; 172 173 /** 174 * The application is being restored from backup and should replace any 175 * existing data with the contents of the backup. The backup data is 176 * provided through the <code>data</code> parameter. Once 177 * the restore is finished, the application should write a representation of 178 * the final state to the <code>newState</code> file descriptor. 179 * <p> 180 * The application is responsible for properly erasing its old data and 181 * replacing it with the data supplied to this method. No "clear user data" 182 * operation will be performed automatically by the operating system. The 183 * exception to this is in the case of a failed restore attempt: if 184 * onRestore() throws an exception, the OS will assume that the 185 * application's data may now be in an incoherent state, and will clear it 186 * before proceeding. 187 * 188 * @param data A structured wrapper around an open, read-only 189 * file descriptor pointing to a full snapshot of the 190 * application's data. The application should consume every 191 * entity represented in this data stream. 192 * @param appVersionCode The value of the <a 193 * href="{@docRoot}guide/topics/manifest/manifest-element.html#vcode">{@code 194 * android:versionCode}</a> manifest attribute, 195 * from the application that backed up this particular data set. This 196 * makes it possible for an application's agent to distinguish among any 197 * possible older data versions when asked to perform the restore 198 * operation. 199 * @param newState An open, read/write ParcelFileDescriptor pointing to an 200 * empty file. The application should record the final backup 201 * state here after restoring its data from the <code>data</code> stream. 202 * When a full-backup dataset is being restored, this will be <code>null</code>. 203 */ 204 public abstract void onRestore(BackupDataInput data, int appVersionCode, 205 ParcelFileDescriptor newState) 206 throws IOException; 207 208 /** 209 * The default implementation backs up the entirety of the application's "owned" 210 * file system trees to the output. 211 */ 212 public void onFullBackup(FullBackupDataOutput data) throws IOException { 213 ApplicationInfo appInfo = getApplicationInfo(); 214 215 String rootDir = new File(appInfo.dataDir).getAbsolutePath(); 216 String filesDir = getFilesDir().getAbsolutePath(); 217 String databaseDir = getDatabasePath("foo").getParentFile().getAbsolutePath(); 218 String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath(); 219 String cacheDir = getCacheDir().getAbsolutePath(); 220 String libDir = (appInfo.nativeLibraryDir != null) 221 ? new File(appInfo.nativeLibraryDir).getAbsolutePath() 222 : null; 223 224 // Filters, the scan queue, and the set of resulting entities 225 HashSet<String> filterSet = new HashSet<String>(); 226 String packageName = getPackageName(); 227 228 // Okay, start with the app's root tree, but exclude all of the canonical subdirs 229 if (libDir != null) { 230 filterSet.add(libDir); 231 } 232 filterSet.add(cacheDir); 233 filterSet.add(databaseDir); 234 filterSet.add(sharedPrefsDir); 235 filterSet.add(filesDir); 236 fullBackupFileTree(packageName, FullBackup.ROOT_TREE_TOKEN, rootDir, filterSet, data); 237 238 // Now do the same for the files dir, db dir, and shared prefs dir 239 filterSet.add(rootDir); 240 filterSet.remove(filesDir); 241 fullBackupFileTree(packageName, FullBackup.DATA_TREE_TOKEN, filesDir, filterSet, data); 242 243 filterSet.add(filesDir); 244 filterSet.remove(databaseDir); 245 fullBackupFileTree(packageName, FullBackup.DATABASE_TREE_TOKEN, databaseDir, filterSet, data); 246 247 filterSet.add(databaseDir); 248 filterSet.remove(sharedPrefsDir); 249 fullBackupFileTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, sharedPrefsDir, filterSet, data); 250 } 251 252 /** 253 * Write an entire file as part of a full-backup operation. The file's contents 254 * will be delivered to the backup destination along with the metadata necessary 255 * to place it with the proper location and permissions on the device where the 256 * data is restored. 257 * @hide 258 * 259 * @param context The BackupAgent that is calling this method. It is an error to 260 * call it from something other than a running BackupAgent instance. 261 * @param file The file to be backed up. The file must exist and be readable by 262 * the caller. 263 * @param output The destination to which the backed-up file data will be sent. 264 */ 265 public final void fullBackupFile(File file, FullBackupDataOutput output) { 266 // Look up where all of our various well-defined dir trees live on this device 267 String mainDir; 268 String filesDir; 269 String dbDir; 270 String spDir; 271 String cacheDir; 272 String libDir; 273 274 ApplicationInfo appInfo = getApplicationInfo(); 275 276 mainDir = new File(appInfo.dataDir).getAbsolutePath(); 277 filesDir = getFilesDir().getAbsolutePath(); 278 dbDir = getDatabasePath("foo").getParentFile().getAbsolutePath(); 279 spDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath(); 280 cacheDir = getCacheDir().getAbsolutePath(); 281 libDir = (appInfo.nativeLibraryDir == null) ? null 282 : new File(appInfo.nativeLibraryDir).getAbsolutePath(); 283 284 // Now figure out which well-defined tree the file is placed in, working from 285 // most to least specific. We also specifically exclude the lib and cache dirs. 286 String filePath = file.getAbsolutePath(); 287 288 if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) { 289 Log.w(TAG, "lib and cache files are not backed up"); 290 return; 291 } 292 293 final String domain; 294 String rootpath = null; 295 if (filePath.startsWith(dbDir)) { 296 domain = FullBackup.DATABASE_TREE_TOKEN; 297 rootpath = dbDir; 298 } else if (filePath.startsWith(spDir)) { 299 domain = FullBackup.SHAREDPREFS_TREE_TOKEN; 300 rootpath = spDir; 301 } else if (filePath.startsWith(filesDir)) { 302 domain = FullBackup.DATA_TREE_TOKEN; 303 rootpath = filesDir; 304 } else if (filePath.startsWith(mainDir)) { 305 domain = FullBackup.ROOT_TREE_TOKEN; 306 rootpath = mainDir; 307 } else { 308 Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping"); 309 return; 310 } 311 312 // And now that we know where it lives, semantically, back it up appropriately 313 Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain 314 + " rootpath=" + rootpath); 315 FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, 316 output.getData()); 317 } 318 319 /** 320 * Scan the dir tree (if it actually exists) and process each entry we find. If the 321 * 'excludes' parameter is non-null, it is consulted each time a new file system entity 322 * is visited to see whether that entity (and its subtree, if appropriate) should be 323 * omitted from the backup process. 324 * 325 * @hide 326 */ 327 protected final void fullBackupFileTree(String packageName, String domain, String rootPath, 328 HashSet<String> excludes, FullBackupDataOutput output) { 329 File rootFile = new File(rootPath); 330 if (rootFile.exists()) { 331 LinkedList<File> scanQueue = new LinkedList<File>(); 332 scanQueue.add(rootFile); 333 334 while (scanQueue.size() > 0) { 335 File file = scanQueue.remove(0); 336 String filePath = file.getAbsolutePath(); 337 338 // prune this subtree? 339 if (excludes != null && excludes.contains(filePath)) { 340 continue; 341 } 342 343 // If it's a directory, enqueue its contents for scanning. 344 try { 345 StructStat stat = Libcore.os.lstat(filePath); 346 if (OsConstants.S_ISLNK(stat.st_mode)) { 347 if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file); 348 continue; 349 } else if (OsConstants.S_ISDIR(stat.st_mode)) { 350 File[] contents = file.listFiles(); 351 if (contents != null) { 352 for (File entry : contents) { 353 scanQueue.add(0, entry); 354 } 355 } 356 } 357 } catch (ErrnoException e) { 358 if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); 359 continue; 360 } 361 362 // Finally, back this file up before proceeding 363 FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, 364 output.getData()); 365 } 366 } 367 } 368 369 /** 370 * Handle the data delivered via the given file descriptor during a full restore 371 * operation. The agent is given the path to the file's original location as well 372 * as its size and metadata. 373 * <p> 374 * The file descriptor can only be read for {@code size} bytes; attempting to read 375 * more data has undefined behavior. 376 * <p> 377 * The default implementation creates the destination file/directory and populates it 378 * with the data from the file descriptor, then sets the file's access mode and 379 * modification time to match the restore arguments. 380 * 381 * @param data A read-only file descriptor from which the agent can read {@code size} 382 * bytes of file data. 383 * @param size The number of bytes of file content to be restored to the given 384 * destination. If the file system object being restored is a directory, {@code size} 385 * will be zero. 386 * @param destination The File on disk to be restored with the given data. 387 * @param type The kind of file system object being restored. This will be either 388 * {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}. 389 * @param mode The access mode to be assigned to the destination after its data is 390 * written. This is in the standard format used by {@code chmod()}. 391 * @param mtime The modification time of the file when it was backed up, suitable to 392 * be assigned to the file after its data is written. 393 * @throws IOException 394 */ 395 public void onRestoreFile(ParcelFileDescriptor data, long size, 396 File destination, int type, long mode, long mtime) 397 throws IOException { 398 FullBackup.restoreFile(data, size, type, mode, mtime, destination); 399 } 400 401 /** 402 * Only specialized platform agents should overload this entry point to support 403 * restores to crazy non-app locations. 404 * @hide 405 */ 406 protected void onRestoreFile(ParcelFileDescriptor data, long size, 407 int type, String domain, String path, long mode, long mtime) 408 throws IOException { 409 String basePath = null; 410 411 if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type 412 + " domain=" + domain + " relpath=" + path + " mode=" + mode 413 + " mtime=" + mtime); 414 415 // Parse out the semantic domains into the correct physical location 416 if (domain.equals(FullBackup.DATA_TREE_TOKEN)) { 417 basePath = getFilesDir().getAbsolutePath(); 418 } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) { 419 basePath = getDatabasePath("foo").getParentFile().getAbsolutePath(); 420 } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { 421 basePath = new File(getApplicationInfo().dataDir).getAbsolutePath(); 422 } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { 423 basePath = getSharedPrefsFile("foo").getParentFile().getAbsolutePath(); 424 } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) { 425 basePath = getCacheDir().getAbsolutePath(); 426 } else { 427 // Not a supported location 428 Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring"); 429 } 430 431 // Now that we've figured out where the data goes, send it on its way 432 if (basePath != null) { 433 File outFile = new File(basePath, path); 434 if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outFile.getPath()); 435 onRestoreFile(data, size, outFile, type, mode, mtime); 436 } else { 437 // Not a supported output location? We need to consume the data 438 // anyway, so just use the default "copy the data out" implementation 439 // with a null destination. 440 if (DEBUG) Log.i(TAG, "[ skipping data from unsupported domain " + domain + "]"); 441 FullBackup.restoreFile(data, size, type, mode, mtime, null); 442 } 443 } 444 445 // ----- Core implementation ----- 446 447 /** @hide */ 448 public final IBinder onBind() { 449 return mBinder; 450 } 451 452 private final IBinder mBinder = new BackupServiceBinder().asBinder(); 453 454 /** @hide */ 455 public void attach(Context context) { 456 attachBaseContext(context); 457 } 458 459 // ----- IBackupService binder interface ----- 460 private class BackupServiceBinder extends IBackupAgent.Stub { 461 private static final String TAG = "BackupServiceBinder"; 462 463 @Override 464 public void doBackup(ParcelFileDescriptor oldState, 465 ParcelFileDescriptor data, 466 ParcelFileDescriptor newState, 467 int token, IBackupManager callbackBinder) throws RemoteException { 468 // Ensure that we're running with the app's normal permission level 469 long ident = Binder.clearCallingIdentity(); 470 471 if (DEBUG) Log.v(TAG, "doBackup() invoked"); 472 BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); 473 474 try { 475 BackupAgent.this.onBackup(oldState, output, newState); 476 } catch (IOException ex) { 477 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 478 throw new RuntimeException(ex); 479 } catch (RuntimeException ex) { 480 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 481 throw ex; 482 } finally { 483 Binder.restoreCallingIdentity(ident); 484 try { 485 callbackBinder.opComplete(token); 486 } catch (RemoteException e) { 487 // we'll time out anyway, so we're safe 488 } 489 } 490 } 491 492 @Override 493 public void doRestore(ParcelFileDescriptor data, int appVersionCode, 494 ParcelFileDescriptor newState, 495 int token, IBackupManager callbackBinder) throws RemoteException { 496 // Ensure that we're running with the app's normal permission level 497 long ident = Binder.clearCallingIdentity(); 498 499 if (DEBUG) Log.v(TAG, "doRestore() invoked"); 500 BackupDataInput input = new BackupDataInput(data.getFileDescriptor()); 501 try { 502 BackupAgent.this.onRestore(input, appVersionCode, newState); 503 } catch (IOException ex) { 504 Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); 505 throw new RuntimeException(ex); 506 } catch (RuntimeException ex) { 507 Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); 508 throw ex; 509 } finally { 510 Binder.restoreCallingIdentity(ident); 511 try { 512 callbackBinder.opComplete(token); 513 } catch (RemoteException e) { 514 // we'll time out anyway, so we're safe 515 } 516 } 517 } 518 519 @Override 520 public void doFullBackup(ParcelFileDescriptor data, 521 int token, IBackupManager callbackBinder) { 522 // Ensure that we're running with the app's normal permission level 523 long ident = Binder.clearCallingIdentity(); 524 525 if (DEBUG) Log.v(TAG, "doFullBackup() invoked"); 526 527 try { 528 BackupAgent.this.onFullBackup(new FullBackupDataOutput(data)); 529 } catch (IOException ex) { 530 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 531 throw new RuntimeException(ex); 532 } catch (RuntimeException ex) { 533 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 534 throw ex; 535 } finally { 536 Binder.restoreCallingIdentity(ident); 537 try { 538 callbackBinder.opComplete(token); 539 } catch (RemoteException e) { 540 // we'll time out anyway, so we're safe 541 } 542 } 543 } 544 545 @Override 546 public void doRestoreFile(ParcelFileDescriptor data, long size, 547 int type, String domain, String path, long mode, long mtime, 548 int token, IBackupManager callbackBinder) throws RemoteException { 549 long ident = Binder.clearCallingIdentity(); 550 try { 551 BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime); 552 } catch (IOException e) { 553 throw new RuntimeException(e); 554 } finally { 555 Binder.restoreCallingIdentity(ident); 556 try { 557 callbackBinder.opComplete(token); 558 } catch (RemoteException e) { 559 // we'll time out anyway, so we're safe 560 } 561 } 562 } 563 } 564} 565