PackageInstallerSession.java revision bb7b7bea19223c1eba74f525c7fe87ca3911813b
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 if ((internalStageDir == null) == (externalStageCid == null)) { 199 throw new IllegalArgumentException( 200 "Exactly one of internal or external stage must be set"); 201 } 202 203 mSealed = sealed; 204 205 // Always derived at runtime 206 installerUid = mPm.getPackageUid(installerPackageName, userId); 207 208 if (mPm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES, 209 installerPackageName) == PackageManager.PERMISSION_GRANTED) { 210 mPermissionsAccepted = true; 211 } else { 212 mPermissionsAccepted = false; 213 } 214 215 computeProgressLocked(); 216 } 217 218 public SessionInfo generateInfo() { 219 final SessionInfo info = new SessionInfo(); 220 synchronized (mLock) { 221 info.sessionId = sessionId; 222 info.installerPackageName = installerPackageName; 223 info.resolvedBaseCodePath = mResolvedBaseCodePath; 224 info.progress = mProgress; 225 info.sealed = mSealed; 226 info.open = mOpenCount.get() > 0; 227 228 info.mode = params.mode; 229 info.sizeBytes = params.sizeBytes; 230 info.appPackageName = params.appPackageName; 231 info.appIcon = params.appIcon; 232 info.appLabel = params.appLabel; 233 } 234 return info; 235 } 236 237 public boolean isSealed() { 238 synchronized (mLock) { 239 return mSealed; 240 } 241 } 242 243 private void assertNotSealed(String cookie) { 244 synchronized (mLock) { 245 if (mSealed) { 246 throw new SecurityException(cookie + " not allowed after commit"); 247 } 248 } 249 } 250 251 /** 252 * Resolve the actual location where staged data should be written. This 253 * might point at an ASEC mount point, which is why we delay path resolution 254 * until someone actively works with the session. 255 */ 256 private File getStageDir() throws IOException { 257 synchronized (mLock) { 258 if (mResolvedStageDir == null) { 259 if (internalStageDir != null) { 260 mResolvedStageDir = internalStageDir; 261 } else { 262 final String path = PackageHelper.getSdDir(externalStageCid); 263 if (path != null) { 264 mResolvedStageDir = new File(path); 265 } else { 266 throw new IOException( 267 "Failed to resolve container path for " + externalStageCid); 268 } 269 } 270 } 271 return mResolvedStageDir; 272 } 273 } 274 275 @Override 276 public void setClientProgress(float progress) { 277 synchronized (mLock) { 278 mClientProgress = progress; 279 computeProgressLocked(); 280 } 281 maybePublishProgress(); 282 } 283 284 @Override 285 public void addClientProgress(float progress) { 286 synchronized (mLock) { 287 mClientProgress += progress; 288 computeProgressLocked(); 289 } 290 maybePublishProgress(); 291 } 292 293 private void computeProgressLocked() { 294 mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f); 295 } 296 297 private void maybePublishProgress() { 298 // Only publish when meaningful change 299 if (Math.abs(mProgress - mReportedProgress) > 0.01) { 300 mReportedProgress = mProgress; 301 mCallback.onSessionProgressChanged(this, mProgress); 302 } 303 } 304 305 @Override 306 public String[] getNames() { 307 assertNotSealed("getNames"); 308 try { 309 return getStageDir().list(); 310 } catch (IOException e) { 311 throw ExceptionUtils.wrap(e); 312 } 313 } 314 315 @Override 316 public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { 317 try { 318 return openWriteInternal(name, offsetBytes, lengthBytes); 319 } catch (IOException e) { 320 throw ExceptionUtils.wrap(e); 321 } 322 } 323 324 private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes) 325 throws IOException { 326 // Quick sanity check of state, and allocate a pipe for ourselves. We 327 // then do heavy disk allocation outside the lock, but this open pipe 328 // will block any attempted install transitions. 329 final FileBridge bridge; 330 synchronized (mLock) { 331 assertNotSealed("openWrite"); 332 333 bridge = new FileBridge(); 334 mBridges.add(bridge); 335 } 336 337 try { 338 // Use installer provided name for now; we always rename later 339 if (!FileUtils.isValidExtFilename(name)) { 340 throw new IllegalArgumentException("Invalid name: " + name); 341 } 342 final File target = new File(getStageDir(), name); 343 344 final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), 345 O_CREAT | O_WRONLY, 0644); 346 Os.chmod(target.getAbsolutePath(), 0644); 347 348 // If caller specified a total length, allocate it for them. Free up 349 // cache space to grow, if needed. 350 if (lengthBytes > 0) { 351 final StructStat stat = Libcore.os.fstat(targetFd); 352 final long deltaBytes = lengthBytes - stat.st_size; 353 if (deltaBytes > 0) { 354 mPm.freeStorage(deltaBytes); 355 } 356 Libcore.os.posix_fallocate(targetFd, 0, lengthBytes); 357 } 358 359 if (offsetBytes > 0) { 360 Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); 361 } 362 363 bridge.setTargetFile(targetFd); 364 bridge.start(); 365 return new ParcelFileDescriptor(bridge.getClientSocket()); 366 367 } catch (ErrnoException e) { 368 throw e.rethrowAsIOException(); 369 } 370 } 371 372 @Override 373 public ParcelFileDescriptor openRead(String name) { 374 try { 375 return openReadInternal(name); 376 } catch (IOException e) { 377 throw ExceptionUtils.wrap(e); 378 } 379 } 380 381 private ParcelFileDescriptor openReadInternal(String name) throws IOException { 382 assertNotSealed("openRead"); 383 384 try { 385 if (!FileUtils.isValidExtFilename(name)) { 386 throw new IllegalArgumentException("Invalid name: " + name); 387 } 388 final File target = new File(getStageDir(), name); 389 390 final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), O_RDONLY, 0); 391 return new ParcelFileDescriptor(targetFd); 392 393 } catch (ErrnoException e) { 394 throw e.rethrowAsIOException(); 395 } 396 } 397 398 @Override 399 public void commit(IntentSender statusReceiver) { 400 Preconditions.checkNotNull(statusReceiver); 401 402 final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(mContext, 403 statusReceiver, sessionId); 404 mHandler.obtainMessage(MSG_COMMIT, adapter.getBinder()).sendToTarget(); 405 } 406 407 private void commitLocked() throws PackageManagerException { 408 if (mDestroyed) { 409 throw new PackageManagerException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session"); 410 } 411 412 // Verify that all writers are hands-off 413 if (!mSealed) { 414 for (FileBridge bridge : mBridges) { 415 if (!bridge.isClosed()) { 416 throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED, 417 "Files still open"); 418 } 419 } 420 mSealed = true; 421 422 // Persist the fact that we've sealed ourselves to prevent mutations 423 // of any hard links we create below. 424 mCallback.onSessionSealed(this); 425 } 426 427 final File stageDir; 428 try { 429 stageDir = getStageDir(); 430 } catch (IOException e) { 431 throw new PackageManagerException(INSTALL_FAILED_CONTAINER_ERROR, 432 "Failed to resolve stage dir", e); 433 } 434 435 // Verify that stage looks sane with respect to existing application. 436 // This currently only ensures packageName, versionCode, and certificate 437 // consistency. 438 validateInstallLocked(stageDir); 439 440 Preconditions.checkNotNull(mPackageName); 441 Preconditions.checkNotNull(mSignatures); 442 Preconditions.checkNotNull(mResolvedBaseCodePath); 443 444 if (!mPermissionsAccepted) { 445 // User needs to accept permissions; give installer an intent they 446 // can use to involve user. 447 final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS); 448 intent.setPackage("com.android.packageinstaller"); 449 intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); 450 try { 451 mRemoteObserver.onUserActionRequired(intent); 452 } catch (RemoteException ignored) { 453 } 454 return; 455 } 456 457 // Inherit any packages and native libraries from existing install that 458 // haven't been overridden. 459 if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { 460 // TODO: implement splicing into existing ASEC 461 spliceExistingFilesIntoStage(stageDir); 462 } 463 464 // TODO: surface more granular state from dexopt 465 mCallback.onSessionProgressChanged(this, 0.9f); 466 467 // TODO: for ASEC based applications, grow and stream in packages 468 469 // We've reached point of no return; call into PMS to install the stage. 470 // Regardless of success or failure we always destroy session. 471 final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { 472 @Override 473 public void onUserActionRequired(Intent intent) { 474 throw new IllegalStateException(); 475 } 476 477 @Override 478 public void onPackageInstalled(String basePackageName, int returnCode, String msg, 479 Bundle extras) { 480 destroyInternal(); 481 dispatchSessionFinished(returnCode, msg, extras); 482 } 483 }; 484 485 mPm.installStage(mPackageName, this.internalStageDir, this.externalStageCid, localObserver, 486 params, installerPackageName, installerUid, new UserHandle(userId)); 487 } 488 489 /** 490 * Validate install by confirming that all application packages are have 491 * consistent package name, version code, and signing certificates. 492 * <p> 493 * Renames package files in stage to match split names defined inside. 494 * <p> 495 * Note that upgrade compatibility is still performed by 496 * {@link PackageManagerService}. 497 */ 498 private void validateInstallLocked(File stageDir) throws PackageManagerException { 499 mPackageName = null; 500 mVersionCode = -1; 501 mSignatures = null; 502 mResolvedBaseCodePath = null; 503 504 final File[] files = stageDir.listFiles(); 505 if (ArrayUtils.isEmpty(files)) { 506 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); 507 } 508 509 // Verify that all staged packages are internally consistent 510 final ArraySet<String> seenSplits = new ArraySet<>(); 511 for (File file : files) { 512 513 // Installers can't stage directories, so it's fine to ignore 514 // entries like "lost+found". 515 if (file.isDirectory()) continue; 516 517 final ApkLite info; 518 try { 519 info = PackageParser.parseApkLite(file, PackageParser.PARSE_COLLECT_CERTIFICATES); 520 } catch (PackageParserException e) { 521 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 522 "Failed to parse " + file + ": " + e); 523 } 524 525 if (!seenSplits.add(info.splitName)) { 526 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 527 "Split " + info.splitName + " was defined multiple times"); 528 } 529 530 // Use first package to define unknown values 531 if (mPackageName == null) { 532 mPackageName = info.packageName; 533 mVersionCode = info.versionCode; 534 } 535 if (mSignatures == null) { 536 mSignatures = info.signatures; 537 } 538 539 assertPackageConsistent(String.valueOf(file), info.packageName, info.versionCode, 540 info.signatures); 541 542 // Take this opportunity to enforce uniform naming 543 final String targetName; 544 if (info.splitName == null) { 545 targetName = "base.apk"; 546 } else { 547 targetName = "split_" + info.splitName + ".apk"; 548 } 549 if (!FileUtils.isValidExtFilename(targetName)) { 550 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 551 "Invalid filename: " + targetName); 552 } 553 554 final File targetFile = new File(stageDir, targetName); 555 if (!file.equals(targetFile)) { 556 file.renameTo(targetFile); 557 } 558 559 // Base is coming from session 560 if (info.splitName == null) { 561 mResolvedBaseCodePath = targetFile.getAbsolutePath(); 562 } 563 } 564 565 if (params.mode == SessionParams.MODE_FULL_INSTALL) { 566 // Full installs must include a base package 567 if (!seenSplits.contains(null)) { 568 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 569 "Full install must include a base package"); 570 } 571 572 } else { 573 // Partial installs must be consistent with existing install 574 final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); 575 if (app == null) { 576 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 577 "Missing existing base package for " + mPackageName); 578 } 579 580 // Base might be inherited from existing install 581 if (mResolvedBaseCodePath == null) { 582 mResolvedBaseCodePath = app.getBaseCodePath(); 583 } 584 585 final ApkLite info; 586 try { 587 info = PackageParser.parseApkLite(new File(app.getBaseCodePath()), 588 PackageParser.PARSE_COLLECT_CERTIFICATES); 589 } catch (PackageParserException e) { 590 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 591 "Failed to parse existing base " + app.getBaseCodePath() + ": " + e); 592 } 593 594 assertPackageConsistent("Existing base", info.packageName, info.versionCode, 595 info.signatures); 596 } 597 } 598 599 private void assertPackageConsistent(String tag, String packageName, int versionCode, 600 Signature[] signatures) throws PackageManagerException { 601 if (!mPackageName.equals(packageName)) { 602 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package " 603 + packageName + " inconsistent with " + mPackageName); 604 } 605 if (mVersionCode != versionCode) { 606 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag 607 + " version code " + versionCode + " inconsistent with " 608 + mVersionCode); 609 } 610 if (!Signature.areExactMatch(mSignatures, signatures)) { 611 throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, 612 tag + " signatures are inconsistent"); 613 } 614 } 615 616 /** 617 * Application is already installed; splice existing files that haven't been 618 * overridden into our stage. 619 */ 620 private void spliceExistingFilesIntoStage(File stageDir) throws PackageManagerException { 621 final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); 622 623 int n = 0; 624 final File[] oldFiles = new File(app.getCodePath()).listFiles(); 625 if (!ArrayUtils.isEmpty(oldFiles)) { 626 for (File oldFile : oldFiles) { 627 if (!PackageParser.isApkFile(oldFile)) continue; 628 629 final File newFile = new File(stageDir, oldFile.getName()); 630 try { 631 Os.link(oldFile.getAbsolutePath(), newFile.getAbsolutePath()); 632 n++; 633 } catch (ErrnoException e) { 634 throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, 635 "Failed to splice into stage", e); 636 } 637 } 638 } 639 640 if (LOGD) Slog.d(TAG, "Spliced " + n + " existing APKs into stage"); 641 } 642 643 void setPermissionsResult(boolean accepted) { 644 if (!mSealed) { 645 throw new SecurityException("Must be sealed to accept permissions"); 646 } 647 648 if (accepted) { 649 // Mark and kick off another install pass 650 mPermissionsAccepted = true; 651 mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); 652 } else { 653 destroyInternal(); 654 dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); 655 } 656 } 657 658 public void open() { 659 if (mOpenCount.getAndIncrement() == 0) { 660 mCallback.onSessionOpened(this); 661 } 662 } 663 664 @Override 665 public void close() { 666 if (mOpenCount.decrementAndGet() == 0) { 667 mCallback.onSessionClosed(this); 668 } 669 } 670 671 @Override 672 public void abandon() { 673 destroyInternal(); 674 dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); 675 } 676 677 private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { 678 mFinalStatus = returnCode; 679 mFinalMessage = msg; 680 681 if (mRemoteObserver != null) { 682 try { 683 mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras); 684 } catch (RemoteException ignored) { 685 } 686 } 687 688 final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED); 689 mCallback.onSessionFinished(this, success); 690 } 691 692 private void destroyInternal() { 693 synchronized (mLock) { 694 mSealed = true; 695 mDestroyed = true; 696 } 697 if (internalStageDir != null) { 698 FileUtils.deleteContents(internalStageDir); 699 internalStageDir.delete(); 700 } 701 if (externalStageCid != null) { 702 PackageHelper.destroySdDir(externalStageCid); 703 } 704 } 705 706 void dump(IndentingPrintWriter pw) { 707 synchronized (mLock) { 708 dumpLocked(pw); 709 } 710 } 711 712 private void dumpLocked(IndentingPrintWriter pw) { 713 pw.println("Session " + sessionId + ":"); 714 pw.increaseIndent(); 715 716 pw.printPair("userId", userId); 717 pw.printPair("installerPackageName", installerPackageName); 718 pw.printPair("installerUid", installerUid); 719 pw.printPair("createdMillis", createdMillis); 720 pw.printPair("internalStageDir", internalStageDir); 721 pw.printPair("externalStageCid", externalStageCid); 722 pw.println(); 723 724 params.dump(pw); 725 726 pw.printPair("mClientProgress", mClientProgress); 727 pw.printPair("mProgress", mProgress); 728 pw.printPair("mSealed", mSealed); 729 pw.printPair("mPermissionsAccepted", mPermissionsAccepted); 730 pw.printPair("mDestroyed", mDestroyed); 731 pw.printPair("mBridges", mBridges.size()); 732 pw.printPair("mFinalStatus", mFinalStatus); 733 pw.printPair("mFinalMessage", mFinalMessage); 734 pw.println(); 735 736 pw.decreaseIndent(); 737 } 738} 739