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