BackupAgent.java revision 78be158ce4b95fa537c6cb60a55dbc9161e53ef1
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).getCanonicalPath(); 217 String filesDir = getFilesDir().getCanonicalPath(); 218 String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); 219 String sharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); 220 String cacheDir = getCacheDir().getCanonicalPath(); 221 String libDir = (appInfo.nativeLibraryDir != null) 222 ? new File(appInfo.nativeLibraryDir).getCanonicalPath() 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 * 259 * @param file The file to be backed up. The file must exist and be readable by 260 * the caller. 261 * @param output The destination to which the backed-up file data will be sent. 262 */ 263 public final void fullBackupFile(File file, FullBackupDataOutput output) { 264 // Look up where all of our various well-defined dir trees live on this device 265 String mainDir; 266 String filesDir; 267 String dbDir; 268 String spDir; 269 String cacheDir; 270 String libDir; 271 String filePath; 272 273 ApplicationInfo appInfo = getApplicationInfo(); 274 275 try { 276 mainDir = new File(appInfo.dataDir).getCanonicalPath(); 277 filesDir = getFilesDir().getCanonicalPath(); 278 dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath(); 279 spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); 280 cacheDir = getCacheDir().getCanonicalPath(); 281 libDir = (appInfo.nativeLibraryDir == null) 282 ? null 283 : new File(appInfo.nativeLibraryDir).getCanonicalPath(); 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 filePath = file.getCanonicalPath(); 288 } catch (IOException e) { 289 Log.w(TAG, "Unable to obtain canonical paths"); 290 return; 291 } 292 293 if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) { 294 Log.w(TAG, "lib and cache files are not backed up"); 295 return; 296 } 297 298 final String domain; 299 String rootpath = null; 300 if (filePath.startsWith(dbDir)) { 301 domain = FullBackup.DATABASE_TREE_TOKEN; 302 rootpath = dbDir; 303 } else if (filePath.startsWith(spDir)) { 304 domain = FullBackup.SHAREDPREFS_TREE_TOKEN; 305 rootpath = spDir; 306 } else if (filePath.startsWith(filesDir)) { 307 domain = FullBackup.DATA_TREE_TOKEN; 308 rootpath = filesDir; 309 } else if (filePath.startsWith(mainDir)) { 310 domain = FullBackup.ROOT_TREE_TOKEN; 311 rootpath = mainDir; 312 } else { 313 Log.w(TAG, "File " + filePath + " is in an unsupported location; skipping"); 314 return; 315 } 316 317 // And now that we know where it lives, semantically, back it up appropriately 318 Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain 319 + " rootpath=" + rootpath); 320 FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, 321 output.getData()); 322 } 323 324 /** 325 * Scan the dir tree (if it actually exists) and process each entry we find. If the 326 * 'excludes' parameter is non-null, it is consulted each time a new file system entity 327 * is visited to see whether that entity (and its subtree, if appropriate) should be 328 * omitted from the backup process. 329 * 330 * @hide 331 */ 332 protected final void fullBackupFileTree(String packageName, String domain, String rootPath, 333 HashSet<String> excludes, FullBackupDataOutput output) { 334 File rootFile = new File(rootPath); 335 if (rootFile.exists()) { 336 LinkedList<File> scanQueue = new LinkedList<File>(); 337 scanQueue.add(rootFile); 338 339 while (scanQueue.size() > 0) { 340 File file = scanQueue.remove(0); 341 String filePath; 342 try { 343 filePath = file.getCanonicalPath(); 344 345 // prune this subtree? 346 if (excludes != null && excludes.contains(filePath)) { 347 continue; 348 } 349 350 // If it's a directory, enqueue its contents for scanning. 351 StructStat stat = Libcore.os.lstat(filePath); 352 if (OsConstants.S_ISLNK(stat.st_mode)) { 353 if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file); 354 continue; 355 } else if (OsConstants.S_ISDIR(stat.st_mode)) { 356 File[] contents = file.listFiles(); 357 if (contents != null) { 358 for (File entry : contents) { 359 scanQueue.add(0, entry); 360 } 361 } 362 } 363 } catch (IOException e) { 364 if (DEBUG) Log.w(TAG, "Error canonicalizing path of " + file); 365 continue; 366 } catch (ErrnoException e) { 367 if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); 368 continue; 369 } 370 371 // Finally, back this file up before proceeding 372 FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, 373 output.getData()); 374 } 375 } 376 } 377 378 /** 379 * Handle the data delivered via the given file descriptor during a full restore 380 * operation. The agent is given the path to the file's original location as well 381 * as its size and metadata. 382 * <p> 383 * The file descriptor can only be read for {@code size} bytes; attempting to read 384 * more data has undefined behavior. 385 * <p> 386 * The default implementation creates the destination file/directory and populates it 387 * with the data from the file descriptor, then sets the file's access mode and 388 * modification time to match the restore arguments. 389 * 390 * @param data A read-only file descriptor from which the agent can read {@code size} 391 * bytes of file data. 392 * @param size The number of bytes of file content to be restored to the given 393 * destination. If the file system object being restored is a directory, {@code size} 394 * will be zero. 395 * @param destination The File on disk to be restored with the given data. 396 * @param type The kind of file system object being restored. This will be either 397 * {@link BackupAgent#TYPE_FILE} or {@link BackupAgent#TYPE_DIRECTORY}. 398 * @param mode The access mode to be assigned to the destination after its data is 399 * written. This is in the standard format used by {@code chmod()}. 400 * @param mtime The modification time of the file when it was backed up, suitable to 401 * be assigned to the file after its data is written. 402 * @throws IOException 403 */ 404 public void onRestoreFile(ParcelFileDescriptor data, long size, 405 File destination, int type, long mode, long mtime) 406 throws IOException { 407 FullBackup.restoreFile(data, size, type, mode, mtime, destination); 408 } 409 410 /** 411 * Only specialized platform agents should overload this entry point to support 412 * restores to crazy non-app locations. 413 * @hide 414 */ 415 protected void onRestoreFile(ParcelFileDescriptor data, long size, 416 int type, String domain, String path, long mode, long mtime) 417 throws IOException { 418 String basePath = null; 419 420 if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type 421 + " domain=" + domain + " relpath=" + path + " mode=" + mode 422 + " mtime=" + mtime); 423 424 // Parse out the semantic domains into the correct physical location 425 if (domain.equals(FullBackup.DATA_TREE_TOKEN)) { 426 basePath = getFilesDir().getCanonicalPath(); 427 } else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) { 428 basePath = getDatabasePath("foo").getParentFile().getCanonicalPath(); 429 } else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { 430 basePath = new File(getApplicationInfo().dataDir).getCanonicalPath(); 431 } else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) { 432 basePath = getSharedPrefsFile("foo").getParentFile().getCanonicalPath(); 433 } else if (domain.equals(FullBackup.CACHE_TREE_TOKEN)) { 434 basePath = getCacheDir().getCanonicalPath(); 435 } else { 436 // Not a supported location 437 Log.i(TAG, "Data restored from non-app domain " + domain + ", ignoring"); 438 } 439 440 // Now that we've figured out where the data goes, send it on its way 441 if (basePath != null) { 442 File outFile = new File(basePath, path); 443 if (DEBUG) Log.i(TAG, "[" + domain + " : " + path + "] mapped to " + outFile.getPath()); 444 onRestoreFile(data, size, outFile, type, mode, mtime); 445 } else { 446 // Not a supported output location? We need to consume the data 447 // anyway, so just use the default "copy the data out" implementation 448 // with a null destination. 449 if (DEBUG) Log.i(TAG, "[ skipping data from unsupported domain " + domain + "]"); 450 FullBackup.restoreFile(data, size, type, mode, mtime, null); 451 } 452 } 453 454 // ----- Core implementation ----- 455 456 /** @hide */ 457 public final IBinder onBind() { 458 return mBinder; 459 } 460 461 private final IBinder mBinder = new BackupServiceBinder().asBinder(); 462 463 /** @hide */ 464 public void attach(Context context) { 465 attachBaseContext(context); 466 } 467 468 // ----- IBackupService binder interface ----- 469 private class BackupServiceBinder extends IBackupAgent.Stub { 470 private static final String TAG = "BackupServiceBinder"; 471 472 @Override 473 public void doBackup(ParcelFileDescriptor oldState, 474 ParcelFileDescriptor data, 475 ParcelFileDescriptor newState, 476 int token, IBackupManager callbackBinder) throws RemoteException { 477 // Ensure that we're running with the app's normal permission level 478 long ident = Binder.clearCallingIdentity(); 479 480 if (DEBUG) Log.v(TAG, "doBackup() invoked"); 481 BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); 482 483 try { 484 BackupAgent.this.onBackup(oldState, output, newState); 485 } catch (IOException ex) { 486 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 487 throw new RuntimeException(ex); 488 } catch (RuntimeException ex) { 489 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 490 throw ex; 491 } finally { 492 Binder.restoreCallingIdentity(ident); 493 try { 494 callbackBinder.opComplete(token); 495 } catch (RemoteException e) { 496 // we'll time out anyway, so we're safe 497 } 498 } 499 } 500 501 @Override 502 public void doRestore(ParcelFileDescriptor data, int appVersionCode, 503 ParcelFileDescriptor newState, 504 int token, IBackupManager callbackBinder) throws RemoteException { 505 // Ensure that we're running with the app's normal permission level 506 long ident = Binder.clearCallingIdentity(); 507 508 if (DEBUG) Log.v(TAG, "doRestore() invoked"); 509 BackupDataInput input = new BackupDataInput(data.getFileDescriptor()); 510 try { 511 BackupAgent.this.onRestore(input, appVersionCode, newState); 512 } catch (IOException ex) { 513 Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); 514 throw new RuntimeException(ex); 515 } catch (RuntimeException ex) { 516 Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); 517 throw ex; 518 } finally { 519 Binder.restoreCallingIdentity(ident); 520 try { 521 callbackBinder.opComplete(token); 522 } catch (RemoteException e) { 523 // we'll time out anyway, so we're safe 524 } 525 } 526 } 527 528 @Override 529 public void doFullBackup(ParcelFileDescriptor data, 530 int token, IBackupManager callbackBinder) { 531 // Ensure that we're running with the app's normal permission level 532 long ident = Binder.clearCallingIdentity(); 533 534 if (DEBUG) Log.v(TAG, "doFullBackup() invoked"); 535 536 try { 537 BackupAgent.this.onFullBackup(new FullBackupDataOutput(data)); 538 } catch (IOException ex) { 539 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 540 throw new RuntimeException(ex); 541 } catch (RuntimeException ex) { 542 Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); 543 throw ex; 544 } finally { 545 // Send the EOD marker indicating that there is no more data 546 // forthcoming from this agent. 547 try { 548 FileOutputStream out = new FileOutputStream(data.getFileDescriptor()); 549 byte[] buf = new byte[4]; 550 out.write(buf); 551 } catch (IOException e) { 552 Log.e(TAG, "Unable to finalize backup stream!"); 553 } 554 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 @Override 565 public void doRestoreFile(ParcelFileDescriptor data, long size, 566 int type, String domain, String path, long mode, long mtime, 567 int token, IBackupManager callbackBinder) throws RemoteException { 568 long ident = Binder.clearCallingIdentity(); 569 try { 570 BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime); 571 } catch (IOException e) { 572 throw new RuntimeException(e); 573 } finally { 574 Binder.restoreCallingIdentity(ident); 575 try { 576 callbackBinder.opComplete(token); 577 } catch (RemoteException e) { 578 // we'll time out anyway, so we're safe 579 } 580 } 581 } 582 } 583} 584