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