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 17 package android.app.backup; 18 19 import android.app.IBackupAgent; 20 import android.app.backup.IBackupManager; 21 import android.content.Context; 22 import android.content.ContextWrapper; 23 import android.content.pm.ApplicationInfo; 24 import android.os.Binder; 25 import android.os.IBinder; 26 import android.os.ParcelFileDescriptor; 27 import android.os.RemoteException; 28 import android.util.Log; 29 30 import java.io.File; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.util.HashSet; 34 import java.util.LinkedList; 35 36 import libcore.io.ErrnoException; 37 import libcore.io.Libcore; 38 import libcore.io.OsConstants; 39 import 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 */ 102 public 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 BackupAgent()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 */ onCreate()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 */ onDestroy()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 */ onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)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 */ onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)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 */ onFullBackup(FullBackupDataOutput data)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 */ fullBackupFile(File file, FullBackupDataOutput output)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 */ fullBackupFileTree(String packageName, String domain, String rootPath, HashSet<String> excludes, FullBackupDataOutput output)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 */ onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime)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 */ onRestoreFile(ParcelFileDescriptor data, long size, int type, String domain, String path, long mode, long mtime)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 */ onBind()463 public final IBinder onBind() { 464 return mBinder; 465 } 466 467 private final IBinder mBinder = new BackupServiceBinder().asBinder(); 468 469 /** @hide */ attach(Context context)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 doBackup(ParcelFileDescriptor oldState, ParcelFileDescriptor data, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder)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 doRestore(ParcelFileDescriptor data, int appVersionCode, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder)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 doFullBackup(ParcelFileDescriptor data, int token, IBackupManager callbackBinder)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 doRestoreFile(ParcelFileDescriptor data, long size, int type, String domain, String path, long mode, long mtime, int token, IBackupManager callbackBinder)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