PackageInstallerSession.java revision 742e790294b3441b79f715fe447069b63c6065db
1/* 2 * Copyright (C) 2014 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 com.android.server.pm; 18 19import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; 20import static android.content.pm.PackageManager.INSTALL_FAILED_ALREADY_EXISTS; 21import static android.content.pm.PackageManager.INSTALL_FAILED_CONTAINER_ERROR; 22import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; 23import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; 24import static android.content.pm.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED; 25import static android.system.OsConstants.O_CREAT; 26import static android.system.OsConstants.O_RDONLY; 27import static android.system.OsConstants.O_WRONLY; 28 29import android.content.Context; 30import android.content.Intent; 31import android.content.IntentSender; 32import android.content.pm.ApplicationInfo; 33import android.content.pm.IPackageInstallObserver2; 34import android.content.pm.IPackageInstallerSession; 35import android.content.pm.PackageInstaller; 36import android.content.pm.PackageInstaller.SessionInfo; 37import android.content.pm.PackageInstaller.SessionParams; 38import android.content.pm.PackageManager; 39import android.content.pm.PackageParser; 40import android.content.pm.PackageParser.ApkLite; 41import android.content.pm.PackageParser.PackageParserException; 42import android.content.pm.Signature; 43import android.os.Bundle; 44import android.os.FileBridge; 45import android.os.FileUtils; 46import android.os.Handler; 47import android.os.Looper; 48import android.os.Message; 49import android.os.ParcelFileDescriptor; 50import android.os.RemoteException; 51import android.os.UserHandle; 52import android.system.ErrnoException; 53import android.system.Os; 54import android.system.OsConstants; 55import android.system.StructStat; 56import android.util.ArraySet; 57import android.util.ExceptionUtils; 58import android.util.MathUtils; 59import android.util.Slog; 60 61import com.android.internal.annotations.GuardedBy; 62import com.android.internal.content.PackageHelper; 63import com.android.internal.util.ArrayUtils; 64import com.android.internal.util.IndentingPrintWriter; 65import com.android.internal.util.Preconditions; 66import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapter; 67 68import libcore.io.Libcore; 69 70import java.io.File; 71import java.io.FileDescriptor; 72import java.io.IOException; 73import java.util.ArrayList; 74import java.util.concurrent.atomic.AtomicInteger; 75 76public class PackageInstallerSession extends IPackageInstallerSession.Stub { 77 private static final String TAG = "PackageInstaller"; 78 private static final boolean LOGD = true; 79 80 private static final int MSG_COMMIT = 0; 81 82 // TODO: enforce INSTALL_ALLOW_TEST 83 // TODO: enforce INSTALL_ALLOW_DOWNGRADE 84 85 // TODO: treat INHERIT_EXISTING as installExistingPackage() 86 87 private final PackageInstallerService.InternalCallback mCallback; 88 private final Context mContext; 89 private final PackageManagerService mPm; 90 private final Handler mHandler; 91 92 final int sessionId; 93 final int userId; 94 final String installerPackageName; 95 final SessionParams params; 96 final long createdMillis; 97 98 /** Internal location where staged data is written. */ 99 final File internalStageDir; 100 /** External container where staged data is written. */ 101 final String externalStageCid; 102 103 /** 104 * When a {@link SessionParams#MODE_INHERIT_EXISTING} session is installed 105 * into an ASEC, this is the container where the stage is combined with the 106 * existing install. 107 */ 108 // TODO: persist this cid once we start splicing 109 String combinedCid; 110 111 /** Note that UID is not persisted; it's always derived at runtime. */ 112 final int installerUid; 113 114 private final AtomicInteger mOpenCount = new AtomicInteger(); 115 116 private final Object mLock = new Object(); 117 118 @GuardedBy("mLock") 119 private float mClientProgress = 0; 120 @GuardedBy("mLock") 121 private float mProgress = 0; 122 @GuardedBy("mLock") 123 private float mReportedProgress = -1; 124 125 @GuardedBy("mLock") 126 private boolean mSealed = false; 127 @GuardedBy("mLock") 128 private boolean mPermissionsAccepted = false; 129 @GuardedBy("mLock") 130 private boolean mDestroyed = false; 131 132 private int mFinalStatus; 133 private String mFinalMessage; 134 135 @GuardedBy("mLock") 136 private File mResolvedStageDir; 137 138 /** 139 * Path to the resolved base APK for this session, which may point at an APK 140 * inside the session (when the session defines the base), or it may point 141 * at the existing base APK (when adding splits to an existing app). 142 * <p> 143 * This is used when confirming permissions, since we can't fully stage the 144 * session inside an ASEC before confirming with user. 145 */ 146 @GuardedBy("mLock") 147 private String mResolvedBaseCodePath; 148 149 @GuardedBy("mLock") 150 private ArrayList<FileBridge> mBridges = new ArrayList<>(); 151 152 @GuardedBy("mLock") 153 private IPackageInstallObserver2 mRemoteObserver; 154 155 /** Fields derived from commit parsing */ 156 private String mPackageName; 157 private int mVersionCode; 158 private Signature[] mSignatures; 159 160 private final Handler.Callback mHandlerCallback = new Handler.Callback() { 161 @Override 162 public boolean handleMessage(Message msg) { 163 synchronized (mLock) { 164 if (msg.obj != null) { 165 mRemoteObserver = (IPackageInstallObserver2) msg.obj; 166 } 167 168 try { 169 commitLocked(); 170 } catch (PackageManagerException e) { 171 Slog.e(TAG, "Install failed: " + e); 172 destroyInternal(); 173 dispatchSessionFinished(e.error, e.getMessage(), null); 174 } 175 176 return true; 177 } 178 } 179 }; 180 181 public PackageInstallerSession(PackageInstallerService.InternalCallback callback, 182 Context context, PackageManagerService pm, Looper looper, int sessionId, int userId, 183 String installerPackageName, SessionParams params, long createdMillis, 184 File internalStageDir, String externalStageCid, boolean sealed) { 185 mCallback = callback; 186 mContext = context; 187 mPm = pm; 188 mHandler = new Handler(looper, mHandlerCallback); 189 190 this.sessionId = sessionId; 191 this.userId = userId; 192 this.installerPackageName = installerPackageName; 193 this.params = params; 194 this.createdMillis = createdMillis; 195 this.internalStageDir = internalStageDir; 196 this.externalStageCid = externalStageCid; 197 198 mSealed = sealed; 199 200 // Always derived at runtime 201 installerUid = mPm.getPackageUid(installerPackageName, userId); 202 203 if (mPm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES, 204 installerPackageName) == PackageManager.PERMISSION_GRANTED) { 205 mPermissionsAccepted = true; 206 } else { 207 mPermissionsAccepted = false; 208 } 209 210 computeProgressLocked(); 211 } 212 213 public SessionInfo generateInfo() { 214 final SessionInfo info = new SessionInfo(); 215 synchronized (mLock) { 216 info.sessionId = sessionId; 217 info.installerPackageName = installerPackageName; 218 info.resolvedBaseCodePath = mResolvedBaseCodePath; 219 info.progress = mProgress; 220 info.sealed = mSealed; 221 info.open = mOpenCount.get() > 0; 222 223 info.mode = params.mode; 224 info.sizeBytes = params.sizeBytes; 225 info.appPackageName = params.appPackageName; 226 info.appIcon = params.appIcon; 227 info.appLabel = params.appLabel; 228 } 229 return info; 230 } 231 232 public boolean isSealed() { 233 synchronized (mLock) { 234 return mSealed; 235 } 236 } 237 238 private void assertNotSealed(String cookie) { 239 synchronized (mLock) { 240 if (mSealed) { 241 throw new SecurityException(cookie + " not allowed after commit"); 242 } 243 } 244 } 245 246 /** 247 * Resolve the actual location where staged data should be written. This 248 * might point at an ASEC mount point, which is why we delay path resolution 249 * until someone actively works with the session. 250 */ 251 private File getStageDir() throws IOException { 252 synchronized (mLock) { 253 if (mResolvedStageDir == null) { 254 if (internalStageDir != null) { 255 mResolvedStageDir = internalStageDir; 256 } else { 257 final String path = PackageHelper.getSdDir(externalStageCid); 258 if (path != null) { 259 mResolvedStageDir = new File(path); 260 } else { 261 throw new IOException( 262 "Failed to resolve container path for " + externalStageCid); 263 } 264 } 265 } 266 return mResolvedStageDir; 267 } 268 } 269 270 @Override 271 public void setClientProgress(float progress) { 272 synchronized (mLock) { 273 mClientProgress = progress; 274 computeProgressLocked(); 275 } 276 maybePublishProgress(); 277 } 278 279 @Override 280 public void addClientProgress(float progress) { 281 synchronized (mLock) { 282 mClientProgress += progress; 283 computeProgressLocked(); 284 } 285 maybePublishProgress(); 286 } 287 288 private void computeProgressLocked() { 289 mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f); 290 } 291 292 private void maybePublishProgress() { 293 // Only publish when meaningful change 294 if (Math.abs(mProgress - mReportedProgress) > 0.01) { 295 mReportedProgress = mProgress; 296 mCallback.onSessionProgressChanged(this, mProgress); 297 } 298 } 299 300 @Override 301 public String[] getNames() { 302 assertNotSealed("getNames"); 303 try { 304 return getStageDir().list(); 305 } catch (IOException e) { 306 throw ExceptionUtils.wrap(e); 307 } 308 } 309 310 @Override 311 public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { 312 try { 313 return openWriteInternal(name, offsetBytes, lengthBytes); 314 } catch (IOException e) { 315 throw ExceptionUtils.wrap(e); 316 } 317 } 318 319 private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes) 320 throws IOException { 321 // Quick sanity check of state, and allocate a pipe for ourselves. We 322 // then do heavy disk allocation outside the lock, but this open pipe 323 // will block any attempted install transitions. 324 final FileBridge bridge; 325 synchronized (mLock) { 326 assertNotSealed("openWrite"); 327 328 bridge = new FileBridge(); 329 mBridges.add(bridge); 330 } 331 332 try { 333 // Use installer provided name for now; we always rename later 334 if (!FileUtils.isValidExtFilename(name)) { 335 throw new IllegalArgumentException("Invalid name: " + name); 336 } 337 final File target = new File(getStageDir(), name); 338 339 final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), 340 O_CREAT | O_WRONLY, 0644); 341 Os.chmod(target.getAbsolutePath(), 0644); 342 343 // If caller specified a total length, allocate it for them. Free up 344 // cache space to grow, if needed. 345 if (lengthBytes > 0) { 346 final StructStat stat = Libcore.os.fstat(targetFd); 347 final long deltaBytes = lengthBytes - stat.st_size; 348 if (deltaBytes > 0) { 349 mPm.freeStorage(deltaBytes); 350 } 351 Libcore.os.posix_fallocate(targetFd, 0, lengthBytes); 352 } 353 354 if (offsetBytes > 0) { 355 Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); 356 } 357 358 bridge.setTargetFile(targetFd); 359 bridge.start(); 360 return new ParcelFileDescriptor(bridge.getClientSocket()); 361 362 } catch (ErrnoException e) { 363 throw e.rethrowAsIOException(); 364 } 365 } 366 367 @Override 368 public ParcelFileDescriptor openRead(String name) { 369 try { 370 return openReadInternal(name); 371 } catch (IOException e) { 372 throw ExceptionUtils.wrap(e); 373 } 374 } 375 376 private ParcelFileDescriptor openReadInternal(String name) throws IOException { 377 assertNotSealed("openRead"); 378 379 try { 380 if (!FileUtils.isValidExtFilename(name)) { 381 throw new IllegalArgumentException("Invalid name: " + name); 382 } 383 final File target = new File(getStageDir(), name); 384 385 final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), O_RDONLY, 0); 386 return new ParcelFileDescriptor(targetFd); 387 388 } catch (ErrnoException e) { 389 throw e.rethrowAsIOException(); 390 } 391 } 392 393 @Override 394 public void commit(IntentSender statusReceiver) { 395 Preconditions.checkNotNull(statusReceiver); 396 397 final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(mContext, 398 statusReceiver); 399 mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget(); 400 } 401 402 private void commitLocked() throws PackageManagerException { 403 if (mDestroyed) { 404 throw new PackageManagerException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session"); 405 } 406 407 // Verify that all writers are hands-off 408 if (!mSealed) { 409 for (FileBridge bridge : mBridges) { 410 if (!bridge.isClosed()) { 411 throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED, 412 "Files still open"); 413 } 414 } 415 mSealed = true; 416 417 // TODO: persist disabled mutations before going forward, since 418 // beyond this point we may have hardlinks to the valid install 419 } 420 421 final File stageDir; 422 try { 423 stageDir = getStageDir(); 424 } catch (IOException e) { 425 throw new PackageManagerException(INSTALL_FAILED_CONTAINER_ERROR, 426 "Failed to resolve stage dir", e); 427 } 428 429 // Verify that stage looks sane with respect to existing application. 430 // This currently only ensures packageName, versionCode, and certificate 431 // consistency. 432 validateInstallLocked(stageDir); 433 434 Preconditions.checkNotNull(mPackageName); 435 Preconditions.checkNotNull(mSignatures); 436 Preconditions.checkNotNull(mResolvedBaseCodePath); 437 438 if (!mPermissionsAccepted) { 439 // User needs to accept permissions; give installer an intent they 440 // can use to involve user. 441 final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS); 442 intent.setPackage("com.android.packageinstaller"); 443 intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); 444 try { 445 mRemoteObserver.onUserActionRequired(intent); 446 } catch (RemoteException ignored) { 447 } 448 return; 449 } 450 451 // Inherit any packages and native libraries from existing install that 452 // haven't been overridden. 453 if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { 454 // TODO: implement splicing into existing ASEC 455 spliceExistingFilesIntoStage(stageDir); 456 } 457 458 // TODO: surface more granular state from dexopt 459 mCallback.onSessionProgressChanged(this, 0.9f); 460 461 // TODO: for ASEC based applications, grow and stream in packages 462 463 // We've reached point of no return; call into PMS to install the stage. 464 // Regardless of success or failure we always destroy session. 465 final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { 466 @Override 467 public void onUserActionRequired(Intent intent) { 468 throw new IllegalStateException(); 469 } 470 471 @Override 472 public void onPackageInstalled(String basePackageName, int returnCode, String msg, 473 Bundle extras) { 474 destroyInternal(); 475 dispatchSessionFinished(returnCode, msg, extras); 476 } 477 }; 478 479 // TODO: send ASEC cid if that's where we staged things 480 mPm.installStage(mPackageName, this.internalStageDir, null, localObserver, params, 481 installerPackageName, installerUid, new UserHandle(userId)); 482 } 483 484 /** 485 * Validate install by confirming that all application packages are have 486 * consistent package name, version code, and signing certificates. 487 * <p> 488 * Renames package files in stage to match split names defined inside. 489 */ 490 private void validateInstallLocked(File stageDir) throws PackageManagerException { 491 mPackageName = null; 492 mVersionCode = -1; 493 mSignatures = null; 494 mResolvedBaseCodePath = null; 495 496 final File[] files = stageDir.listFiles(); 497 if (ArrayUtils.isEmpty(files)) { 498 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); 499 } 500 501 final ArraySet<String> seenSplits = new ArraySet<>(); 502 503 // Verify that all staged packages are internally consistent 504 for (File file : files) { 505 final ApkLite info; 506 try { 507 info = PackageParser.parseApkLite(file, PackageParser.PARSE_GET_SIGNATURES); 508 } catch (PackageParserException e) { 509 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 510 "Failed to parse " + file + ": " + e); 511 } 512 513 if (!seenSplits.add(info.splitName)) { 514 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 515 "Split " + info.splitName + " was defined multiple times"); 516 } 517 518 // Use first package to define unknown values 519 if (mPackageName == null) { 520 mPackageName = info.packageName; 521 mVersionCode = info.versionCode; 522 } 523 if (mSignatures == null) { 524 mSignatures = info.signatures; 525 } 526 527 assertPackageConsistent(String.valueOf(file), info.packageName, info.versionCode, 528 info.signatures); 529 530 // Take this opportunity to enforce uniform naming 531 final String targetName; 532 if (info.splitName == null) { 533 targetName = "base.apk"; 534 } else { 535 targetName = "split_" + info.splitName + ".apk"; 536 } 537 if (!FileUtils.isValidExtFilename(targetName)) { 538 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 539 "Invalid filename: " + targetName); 540 } 541 542 final File targetFile = new File(stageDir, targetName); 543 if (!file.equals(targetFile)) { 544 file.renameTo(targetFile); 545 } 546 547 // Base is coming from session 548 if (info.splitName == null) { 549 mResolvedBaseCodePath = targetFile.getAbsolutePath(); 550 } 551 } 552 553 // TODO: shift package signature verification to installer; we're 554 // currently relying on PMS to do this. 555 // TODO: teach about compatible upgrade keysets. 556 557 if (params.mode == SessionParams.MODE_FULL_INSTALL) { 558 // Full installs must include a base package 559 if (!seenSplits.contains(null)) { 560 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 561 "Full install must include a base package"); 562 } 563 564 } else { 565 // Partial installs must be consistent with existing install 566 final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); 567 if (app == null) { 568 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 569 "Missing existing base package for " + mPackageName); 570 } 571 572 // Base might be inherited from existing install 573 if (mResolvedBaseCodePath == null) { 574 mResolvedBaseCodePath = app.getBaseCodePath(); 575 } 576 577 final ApkLite info; 578 try { 579 info = PackageParser.parseApkLite(new File(app.getBaseCodePath()), 580 PackageParser.PARSE_GET_SIGNATURES); 581 } catch (PackageParserException e) { 582 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 583 "Failed to parse existing base " + app.getBaseCodePath() + ": " + e); 584 } 585 586 assertPackageConsistent("Existing base", info.packageName, info.versionCode, 587 info.signatures); 588 } 589 } 590 591 private void assertPackageConsistent(String tag, String packageName, int versionCode, 592 Signature[] signatures) throws PackageManagerException { 593 if (!mPackageName.equals(packageName)) { 594 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package " 595 + packageName + " inconsistent with " + mPackageName); 596 } 597 if (mVersionCode != versionCode) { 598 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag 599 + " version code " + versionCode + " inconsistent with " 600 + mVersionCode); 601 } 602 if (!Signature.areExactMatch(mSignatures, signatures)) { 603 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 604 tag + " signatures are inconsistent"); 605 } 606 } 607 608 /** 609 * Application is already installed; splice existing files that haven't been 610 * overridden into our stage. 611 */ 612 private void spliceExistingFilesIntoStage(File stageDir) throws PackageManagerException { 613 final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); 614 615 int n = 0; 616 final File[] oldFiles = new File(app.getCodePath()).listFiles(); 617 if (!ArrayUtils.isEmpty(oldFiles)) { 618 for (File oldFile : oldFiles) { 619 if (!PackageParser.isApkFile(oldFile)) continue; 620 621 final File newFile = new File(stageDir, oldFile.getName()); 622 try { 623 Os.link(oldFile.getAbsolutePath(), newFile.getAbsolutePath()); 624 n++; 625 } catch (ErrnoException e) { 626 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, 627 "Failed to splice into stage", e); 628 } 629 } 630 } 631 632 if (LOGD) Slog.d(TAG, "Spliced " + n + " existing APKs into stage"); 633 } 634 635 void setPermissionsResult(boolean accepted) { 636 if (!mSealed) { 637 throw new SecurityException("Must be sealed to accept permissions"); 638 } 639 640 if (accepted) { 641 // Mark and kick off another install pass 642 mPermissionsAccepted = true; 643 mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); 644 } else { 645 destroyInternal(); 646 dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); 647 } 648 } 649 650 public void open() { 651 if (mOpenCount.getAndIncrement() == 0) { 652 mCallback.onSessionOpened(this); 653 } 654 } 655 656 @Override 657 public void close() { 658 if (mOpenCount.decrementAndGet() == 0) { 659 mCallback.onSessionClosed(this); 660 } 661 } 662 663 @Override 664 public void abandon() { 665 destroyInternal(); 666 dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); 667 } 668 669 private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { 670 mFinalStatus = returnCode; 671 mFinalMessage = msg; 672 673 if (mRemoteObserver != null) { 674 try { 675 mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras); 676 } catch (RemoteException ignored) { 677 } 678 } 679 680 final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED); 681 mCallback.onSessionFinished(this, success); 682 } 683 684 private void destroyInternal() { 685 synchronized (mLock) { 686 mSealed = true; 687 mDestroyed = true; 688 } 689 if (internalStageDir != null) { 690 FileUtils.deleteContents(internalStageDir); 691 internalStageDir.delete(); 692 } 693 if (externalStageCid != null) { 694 PackageHelper.destroySdDir(externalStageCid); 695 } 696 } 697 698 void dump(IndentingPrintWriter pw) { 699 synchronized (mLock) { 700 dumpLocked(pw); 701 } 702 } 703 704 private void dumpLocked(IndentingPrintWriter pw) { 705 pw.println("Session " + sessionId + ":"); 706 pw.increaseIndent(); 707 708 pw.printPair("userId", userId); 709 pw.printPair("installerPackageName", installerPackageName); 710 pw.printPair("installerUid", installerUid); 711 pw.printPair("createdMillis", createdMillis); 712 pw.printPair("internalStageDir", internalStageDir); 713 pw.printPair("externalStageCid", externalStageCid); 714 pw.println(); 715 716 params.dump(pw); 717 718 pw.printPair("mClientProgress", mClientProgress); 719 pw.printPair("mProgress", mProgress); 720 pw.printPair("mSealed", mSealed); 721 pw.printPair("mPermissionsAccepted", mPermissionsAccepted); 722 pw.printPair("mDestroyed", mDestroyed); 723 pw.printPair("mBridges", mBridges.size()); 724 pw.printPair("mFinalStatus", mFinalStatus); 725 pw.printPair("mFinalMessage", mFinalMessage); 726 pw.println(); 727 728 pw.decreaseIndent(); 729 } 730} 731