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