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