PackageInstaller.java revision 7328a1b39b3dae1c0cd390c0a3695c6a46b8e9d8
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 android.content.pm;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.SdkConstant;
22import android.annotation.SdkConstant.SdkConstantType;
23import android.app.PackageDeleteObserver;
24import android.app.PackageInstallObserver;
25import android.content.Intent;
26import android.os.Bundle;
27import android.os.FileBridge;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.Message;
31import android.os.ParcelFileDescriptor;
32import android.os.RemoteException;
33import android.util.ExceptionUtils;
34
35import java.io.Closeable;
36import java.io.IOException;
37import java.io.InputStream;
38import java.io.OutputStream;
39import java.security.MessageDigest;
40import java.util.ArrayList;
41import java.util.Iterator;
42import java.util.List;
43
44/**
45 * Offers the ability to install, upgrade, and remove applications on the
46 * device. This includes support for apps packaged either as a single
47 * "monolithic" APK, or apps packaged as multiple "split" APKs.
48 * <p>
49 * An app is delivered for installation through a
50 * {@link PackageInstaller.Session}, which any app can create. Once the session
51 * is created, the installer can stream one or more APKs into place until it
52 * decides to either commit or destroy the session. Committing may require user
53 * intervention to complete the installation.
54 * <p>
55 * Sessions can install brand new apps, upgrade existing apps, or add new splits
56 * into an existing app.
57 * <p>
58 * Apps packaged as multiple split APKs always consist of a single "base" APK
59 * (with a {@code null} split name) and zero or more "split" APKs (with unique
60 * split names). Any subset of these APKs can be installed together, as long as
61 * the following constraints are met:
62 * <ul>
63 * <li>All APKs must have the exact same package name, version code, and signing
64 * certificates.
65 * <li>All APKs must have unique split names.
66 * <li>All installations must contain a single base APK.
67 * </ul>
68 */
69public class PackageInstaller {
70    /**
71     * Activity Action: Show details about a particular install session. This
72     * may surface actions such as pause, resume, or cancel.
73     * <p>
74     * This should always be scoped to the installer package that owns the
75     * session. Clients should use {@link InstallSessionInfo#getDetailsIntent()}
76     * to build this intent correctly.
77     * <p>
78     * In some cases, a matching Activity may not exist, so ensure you safeguard
79     * against this.
80     */
81    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
82    public static final String ACTION_SESSION_DETAILS = "android.content.pm.action.SESSION_DETAILS";
83
84    /** {@hide} */
85    public static final String
86            ACTION_CONFIRM_PERMISSIONS = "android.content.pm.action.CONFIRM_PERMISSIONS";
87
88    /**
89     * An integer session ID.
90     *
91     * @see #ACTION_SESSION_DETAILS
92     */
93    public static final String EXTRA_SESSION_ID = "android.content.pm.extra.SESSION_ID";
94
95    private final PackageManager mPm;
96    private final IPackageInstaller mInstaller;
97    private final int mUserId;
98    private final String mInstallerPackageName;
99
100    private final ArrayList<SessionCallbackDelegate> mDelegates = new ArrayList<>();
101
102    /** {@hide} */
103    public PackageInstaller(PackageManager pm, IPackageInstaller installer,
104            String installerPackageName, int userId) {
105        mPm = pm;
106        mInstaller = installer;
107        mInstallerPackageName = installerPackageName;
108        mUserId = userId;
109    }
110
111    /**
112     * Create a new session using the given parameters, returning a unique ID
113     * that represents the session. Once created, the session can be opened
114     * multiple times across multiple device boots.
115     * <p>
116     * The system may automatically destroy sessions that have not been
117     * finalized (either committed or abandoned) within a reasonable period of
118     * time, typically on the order of a day.
119     *
120     * @throws IOException if parameters were unsatisfiable, such as lack of
121     *             disk space or unavailable media.
122     * @return positive, non-zero unique ID that represents the created session.
123     *         This ID remains consistent across device reboots until the
124     *         session is finalized. IDs are not reused during a given boot.
125     */
126    public int createSession(@NonNull InstallSessionParams params) throws IOException {
127        try {
128            return mInstaller.createSession(params, mInstallerPackageName, mUserId);
129        } catch (RuntimeException e) {
130            ExceptionUtils.maybeUnwrapIOException(e);
131            throw e;
132        } catch (RemoteException e) {
133            throw e.rethrowAsRuntimeException();
134        }
135    }
136
137    /**
138     * Open an existing session to actively perform work. To succeed, the caller
139     * must be the owner of the install session.
140     */
141    public @NonNull Session openSession(int sessionId) {
142        try {
143            return new Session(mInstaller.openSession(sessionId));
144        } catch (RemoteException e) {
145            throw e.rethrowAsRuntimeException();
146        }
147    }
148
149    /**
150     * Return details for a specific session. To succeed, the caller must either
151     * own this session, or be the current home app.
152     */
153    public @Nullable InstallSessionInfo getSessionInfo(int sessionId) {
154        try {
155            return mInstaller.getSessionInfo(sessionId);
156        } catch (RemoteException e) {
157            throw e.rethrowAsRuntimeException();
158        }
159    }
160
161    /**
162     * Return list of all active install sessions, regardless of the installer.
163     * To succeed, the caller must be the current home app.
164     */
165    public @NonNull List<InstallSessionInfo> getAllSessions() {
166        try {
167            return mInstaller.getAllSessions(mUserId);
168        } catch (RemoteException e) {
169            throw e.rethrowAsRuntimeException();
170        }
171    }
172
173    /**
174     * Return list of all install sessions owned by the calling app.
175     */
176    public @NonNull List<InstallSessionInfo> getMySessions() {
177        try {
178            return mInstaller.getMySessions(mInstallerPackageName, mUserId);
179        } catch (RemoteException e) {
180            throw e.rethrowAsRuntimeException();
181        }
182    }
183
184    /**
185     * Uninstall the given package, removing it completely from the device. This
186     * method is only available to the current "installer of record" for the
187     * package.
188     */
189    public void uninstall(@NonNull String packageName, @NonNull UninstallCallback callback) {
190        try {
191            mInstaller.uninstall(packageName, 0,
192                    new UninstallCallbackDelegate(callback).getBinder(), mUserId);
193        } catch (RemoteException e) {
194            throw e.rethrowAsRuntimeException();
195        }
196    }
197
198    /**
199     * Uninstall only a specific split from the given package.
200     *
201     * @hide
202     */
203    public void uninstall(@NonNull String packageName, @NonNull String splitName,
204            @NonNull UninstallCallback callback) {
205        try {
206            mInstaller.uninstallSplit(packageName, splitName, 0,
207                    new UninstallCallbackDelegate(callback).getBinder(), mUserId);
208        } catch (RemoteException e) {
209            throw e.rethrowAsRuntimeException();
210        }
211    }
212
213    /** {@hide} */
214    public void setPermissionsResult(int sessionId, boolean accepted) {
215        try {
216            mInstaller.setPermissionsResult(sessionId, accepted);
217        } catch (RemoteException e) {
218            throw e.rethrowAsRuntimeException();
219        }
220    }
221
222    /**
223     * Events for observing session lifecycle.
224     * <p>
225     * A typical session lifecycle looks like this:
226     * <ul>
227     * <li>An installer creates a session to indicate pending app delivery. All
228     * install details are available at this point.
229     * <li>The installer opens the session to deliver APK data. Note that a
230     * session may be opened and closed multiple times as network connectivity
231     * changes. The installer may deliver periodic progress updates.
232     * <li>The installer commits or abandons the session, resulting in the
233     * session being finished.
234     * </ul>
235     */
236    public static abstract class SessionCallback {
237        /**
238         * New session has been created. Details about the session can be
239         * obtained from {@link PackageInstaller#getSessionInfo(int)}.
240         */
241        public abstract void onCreated(int sessionId);
242
243        /**
244         * Session has been opened. A session is usually opened when the
245         * installer is actively writing data.
246         */
247        public abstract void onOpened(int sessionId);
248
249        /**
250         * Progress for given session has been updated.
251         * <p>
252         * Note that this progress may not directly correspond to the value
253         * reported by {@link PackageInstaller.Session#setProgress(float)}, as
254         * the system may carve out a portion of the overall progress to
255         * represent its own internal installation work.
256         */
257        public abstract void onProgressChanged(int sessionId, float progress);
258
259        /**
260         * Session has been closed.
261         */
262        public abstract void onClosed(int sessionId);
263
264        /**
265         * Session has completely finished, either with success or failure.
266         */
267        public abstract void onFinished(int sessionId, boolean success);
268    }
269
270    /** {@hide} */
271    private static class SessionCallbackDelegate extends IPackageInstallerCallback.Stub implements
272            Handler.Callback {
273        private static final int MSG_SESSION_CREATED = 1;
274        private static final int MSG_SESSION_OPENED = 2;
275        private static final int MSG_SESSION_PROGRESS_CHANGED = 3;
276        private static final int MSG_SESSION_CLOSED = 4;
277        private static final int MSG_SESSION_FINISHED = 5;
278
279        final SessionCallback mCallback;
280        final Handler mHandler;
281
282        public SessionCallbackDelegate(SessionCallback callback, Looper looper) {
283            mCallback = callback;
284            mHandler = new Handler(looper, this);
285        }
286
287        @Override
288        public boolean handleMessage(Message msg) {
289            switch (msg.what) {
290                case MSG_SESSION_CREATED:
291                    mCallback.onCreated(msg.arg1);
292                    return true;
293                case MSG_SESSION_OPENED:
294                    mCallback.onOpened(msg.arg1);
295                    return true;
296                case MSG_SESSION_PROGRESS_CHANGED:
297                    mCallback.onProgressChanged(msg.arg1, (float) msg.obj);
298                    return true;
299                case MSG_SESSION_CLOSED:
300                    mCallback.onClosed(msg.arg1);
301                    return true;
302                case MSG_SESSION_FINISHED:
303                    mCallback.onFinished(msg.arg1, msg.arg2 != 0);
304                    return true;
305            }
306            return false;
307        }
308
309        @Override
310        public void onSessionCreated(int sessionId) {
311            mHandler.obtainMessage(MSG_SESSION_CREATED, sessionId, 0).sendToTarget();
312        }
313
314        @Override
315        public void onSessionOpened(int sessionId) {
316            mHandler.obtainMessage(MSG_SESSION_OPENED, sessionId, 0).sendToTarget();
317        }
318
319        @Override
320        public void onSessionProgressChanged(int sessionId, float progress) {
321            mHandler.obtainMessage(MSG_SESSION_PROGRESS_CHANGED, sessionId, 0, progress)
322                    .sendToTarget();
323        }
324
325        @Override
326        public void onSessionClosed(int sessionId) {
327            mHandler.obtainMessage(MSG_SESSION_CLOSED, sessionId, 0).sendToTarget();
328        }
329
330        @Override
331        public void onSessionFinished(int sessionId, boolean success) {
332            mHandler.obtainMessage(MSG_SESSION_FINISHED, sessionId, success ? 1 : 0)
333                    .sendToTarget();
334        }
335    }
336
337    /**
338     * Register to watch for session lifecycle events. To succeed, the caller
339     * must be the current home app.
340     */
341    public void addSessionCallback(@NonNull SessionCallback callback) {
342        addSessionCallback(callback, new Handler());
343    }
344
345    /**
346     * Register to watch for session lifecycle events. To succeed, the caller
347     * must be the current home app.
348     *
349     * @param handler to dispatch callback events through, otherwise uses
350     *            calling thread.
351     */
352    public void addSessionCallback(@NonNull SessionCallback callback, @NonNull Handler handler) {
353        synchronized (mDelegates) {
354            final SessionCallbackDelegate delegate = new SessionCallbackDelegate(callback,
355                    handler.getLooper());
356            try {
357                mInstaller.registerCallback(delegate, mUserId);
358            } catch (RemoteException e) {
359                throw e.rethrowAsRuntimeException();
360            }
361            mDelegates.add(delegate);
362        }
363    }
364
365    /**
366     * Unregister an existing callback.
367     */
368    public void removeSessionCallback(@NonNull SessionCallback callback) {
369        synchronized (mDelegates) {
370            for (Iterator<SessionCallbackDelegate> i = mDelegates.iterator(); i.hasNext();) {
371                final SessionCallbackDelegate delegate = i.next();
372                if (delegate.mCallback == callback) {
373                    try {
374                        mInstaller.unregisterCallback(delegate);
375                    } catch (RemoteException e) {
376                        throw e.rethrowAsRuntimeException();
377                    }
378                    i.remove();
379                }
380            }
381        }
382    }
383
384    /**
385     * An installation that is being actively staged. For an install to succeed,
386     * all existing and new packages must have identical package names, version
387     * codes, and signing certificates.
388     * <p>
389     * A session may contain any number of split packages. If the application
390     * does not yet exist, this session must include a base package.
391     * <p>
392     * If an APK included in this session is already defined by the existing
393     * installation (for example, the same split name), the APK in this session
394     * will replace the existing APK.
395     */
396    public static class Session implements Closeable {
397        private IPackageInstallerSession mSession;
398
399        /** {@hide} */
400        public Session(IPackageInstallerSession session) {
401            mSession = session;
402        }
403
404        /**
405         * Set current progress. Valid values are anywhere between 0 and 1.
406         */
407        public void setProgress(float progress) {
408            try {
409                mSession.setClientProgress(progress);
410            } catch (RemoteException e) {
411                throw e.rethrowAsRuntimeException();
412            }
413        }
414
415        /** {@hide} */
416        public void addProgress(float progress) {
417            try {
418                mSession.addClientProgress(progress);
419            } catch (RemoteException e) {
420                throw e.rethrowAsRuntimeException();
421            }
422        }
423
424        /**
425         * Open a stream to write an APK file into the session.
426         * <p>
427         * The returned stream will start writing data at the requested offset
428         * in the underlying file, which can be used to resume a partially
429         * written file. If a valid file length is specified, the system will
430         * preallocate the underlying disk space to optimize placement on disk.
431         * It's strongly recommended to provide a valid file length when known.
432         * <p>
433         * You can write data into the returned stream, optionally call
434         * {@link #fsync(OutputStream)} as needed to ensure bytes have been
435         * persisted to disk, and then close when finished. All streams must be
436         * closed before calling {@link #commit(CommitCallback)}.
437         *
438         * @param name arbitrary, unique name of your choosing to identify the
439         *            APK being written. You can open a file again for
440         *            additional writes (such as after a reboot) by using the
441         *            same name. This name is only meaningful within the context
442         *            of a single install session.
443         * @param offsetBytes offset into the file to begin writing at, or 0 to
444         *            start at the beginning of the file.
445         * @param lengthBytes total size of the file being written, used to
446         *            preallocate the underlying disk space, or -1 if unknown.
447         */
448        public @NonNull OutputStream openWrite(@NonNull String name, long offsetBytes,
449                long lengthBytes) throws IOException {
450            try {
451                final ParcelFileDescriptor clientSocket = mSession.openWrite(name,
452                        offsetBytes, lengthBytes);
453                return new FileBridge.FileBridgeOutputStream(clientSocket.getFileDescriptor());
454            } catch (RuntimeException e) {
455                ExceptionUtils.maybeUnwrapIOException(e);
456                throw e;
457            } catch (RemoteException e) {
458                throw e.rethrowAsRuntimeException();
459            }
460        }
461
462        /**
463         * Ensure that any outstanding data for given stream has been committed
464         * to disk. This is only valid for streams returned from
465         * {@link #openWrite(String, long, long)}.
466         */
467        public void fsync(@NonNull OutputStream out) throws IOException {
468            if (out instanceof FileBridge.FileBridgeOutputStream) {
469                ((FileBridge.FileBridgeOutputStream) out).fsync();
470            } else {
471                throw new IllegalArgumentException("Unrecognized stream");
472            }
473        }
474
475        /**
476         * List all APK names contained in this session.
477         * <p>
478         * This returns all names which have been previously written through
479         * {@link #openWrite(String, long, long)} as part of this session.
480         */
481        public @NonNull String[] list() {
482            try {
483                return mSession.list();
484            } catch (RemoteException e) {
485                throw e.rethrowAsRuntimeException();
486            }
487        }
488
489        /**
490         * Open a stream to read an APK file from the session.
491         * <p>
492         * This is only valid for names which have been previously written
493         * through {@link #openWrite(String, long, long)} as part of this
494         * session. For example, this stream may be used to calculate a
495         * {@link MessageDigest} of a written APK before committing.
496         */
497        public @NonNull InputStream openRead(@NonNull String name) throws IOException {
498            try {
499                final ParcelFileDescriptor pfd = mSession.openRead(name);
500                return new ParcelFileDescriptor.AutoCloseInputStream(pfd);
501            } catch (RuntimeException e) {
502                ExceptionUtils.maybeUnwrapIOException(e);
503                throw e;
504            } catch (RemoteException e) {
505                throw e.rethrowAsRuntimeException();
506            }
507        }
508
509        /**
510         * Attempt to commit everything staged in this session. This may require
511         * user intervention, and so it may not happen immediately. The final
512         * result of the commit will be reported through the given callback.
513         * <p>
514         * Once this method is called, no additional mutations may be performed
515         * on the session. If the device reboots before the session has been
516         * finalized, you may commit the session again.
517         */
518        public void commit(@NonNull CommitCallback callback) {
519            try {
520                mSession.commit(new CommitCallbackDelegate(callback).getBinder());
521            } catch (RemoteException e) {
522                throw e.rethrowAsRuntimeException();
523            }
524        }
525
526        /**
527         * Release this session object. You can open the session again if it
528         * hasn't been finalized.
529         */
530        @Override
531        public void close() {
532            try {
533                mSession.close();
534            } catch (RemoteException e) {
535                throw e.rethrowAsRuntimeException();
536            }
537        }
538
539        /**
540         * Completely abandon this session, destroying all staged data and
541         * rendering it invalid.
542         */
543        public void abandon() {
544            try {
545                mSession.abandon();
546            } catch (RemoteException e) {
547                throw e.rethrowAsRuntimeException();
548            }
549        }
550    }
551
552    /**
553     * Events for a specific uninstall request.
554     */
555    public static abstract class UninstallCallback {
556        /**
557         * User action is required to proceed. You can start the given intent
558         * activity to involve the user and continue.
559         * <p>
560         * You may choose to immediately launch the intent if the user is
561         * actively using your app. However, you should use a notification to
562         * guide the user back into your app if not currently active.
563         */
564        public abstract void onUserActionRequired(Intent intent);
565
566        public abstract void onSuccess();
567        public abstract void onFailure(String msg);
568    }
569
570    /** {@hide} */
571    private static class UninstallCallbackDelegate extends PackageDeleteObserver {
572        private final UninstallCallback target;
573
574        public UninstallCallbackDelegate(UninstallCallback target) {
575            this.target = target;
576        }
577
578        @Override
579        public void onUserActionRequired(Intent intent) {
580            target.onUserActionRequired(intent);
581        }
582
583        @Override
584        public void onPackageDeleted(String basePackageName, int returnCode, String msg) {
585            if (returnCode == PackageManager.DELETE_SUCCEEDED) {
586                target.onSuccess();
587            } else {
588                msg = PackageManager.deleteStatusToString(returnCode) + ": " + msg;
589                target.onFailure(msg);
590            }
591        }
592    }
593
594    /**
595     * Final result of a session commit request.
596     */
597    public static abstract class CommitCallback {
598        /**
599         * Generic unknown failure. The system will always try to provide a more
600         * specific failure reason, but in some rare cases this may be
601         * delivered.
602         */
603        public static final int FAILURE_UNKNOWN = 0;
604
605        /**
606         * One or more of the APKs included in the session was invalid. For
607         * example, they might be malformed, corrupt, incorrectly signed,
608         * mismatched, etc. The installer may want to try downloading and
609         * installing again.
610         */
611        public static final int FAILURE_INVALID = 1;
612
613        /**
614         * This install session conflicts (or is inconsistent with) with another
615         * package already installed on the device. For example, an existing
616         * permission, incompatible certificates, etc. The user may be able to
617         * uninstall another app to fix the issue.
618         * <p>
619         * The extras bundle may contain {@link #EXTRA_PACKAGE_NAME} with the
620         * specific packages identified as the cause of the conflict.
621         */
622        public static final int FAILURE_CONFLICT = 2;
623
624        /**
625         * This install session failed due to storage issues. For example,
626         * the device may be running low on space, or the required external
627         * media may be unavailable. The user may be able to help free space
628         * or insert the correct media.
629         */
630        public static final int FAILURE_STORAGE = 3;
631
632        /**
633         * This install session is fundamentally incompatible with this
634         * device. For example, the package may require a hardware feature
635         * that doesn't exist, it may be missing native code for the device
636         * ABI, or it requires a newer SDK version, etc. This install would
637         * never succeed.
638         */
639        public static final int FAILURE_INCOMPATIBLE = 4;
640
641        /**
642         * This install session failed because it was rejected. For example, the
643         * user declined requested permissions, or a package verifier rejected
644         * the session.
645         *
646         * @see PackageManager#VERIFICATION_REJECT
647         */
648        public static final int FAILURE_REJECTED = 5;
649
650        public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
651
652        /**
653         * User action is required to proceed. You can start the given intent
654         * activity to involve the user and continue.
655         * <p>
656         * You may choose to immediately launch the intent if the user is
657         * actively using your app. However, you should use a notification to
658         * guide the user back into your app if not currently active.
659         */
660        public abstract void onUserActionRequired(Intent intent);
661
662        public abstract void onSuccess();
663        public abstract void onFailure(int failureReason, String msg, Bundle extras);
664    }
665
666    /** {@hide} */
667    private static class CommitCallbackDelegate extends PackageInstallObserver {
668        private final CommitCallback target;
669
670        public CommitCallbackDelegate(CommitCallback target) {
671            this.target = target;
672        }
673
674        @Override
675        public void onUserActionRequired(Intent intent) {
676            target.onUserActionRequired(intent);
677        }
678
679        @Override
680        public void onPackageInstalled(String basePackageName, int returnCode, String msg,
681                Bundle extras) {
682            if (returnCode == PackageManager.INSTALL_SUCCEEDED) {
683                target.onSuccess();
684            } else {
685                final int failureReason = PackageManager.installStatusToFailureReason(returnCode);
686                msg = PackageManager.installStatusToString(returnCode) + ": " + msg;
687
688                if (extras != null) {
689                    extras.putString(CommitCallback.EXTRA_PACKAGE_NAME,
690                            extras.getString(PackageManager.EXTRA_FAILURE_EXISTING_PACKAGE));
691                }
692
693                target.onFailure(failureReason, msg, extras);
694            }
695        }
696    }
697}
698