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