1 /* 2 ** 3 ** Copyright 2007, The Android Open Source Project 4 ** 5 ** Licensed under the Apache License, Version 2.0 (the "License"); 6 ** you may not use this file except in compliance with the License. 7 ** You may obtain a copy of the License at 8 ** 9 ** http://www.apache.org/licenses/LICENSE-2.0 10 ** 11 ** Unless required by applicable law or agreed to in writing, software 12 ** distributed under the License is distributed on an "AS IS" BASIS, 13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 ** See the License for the specific language governing permissions and 15 ** limitations under the License. 16 */ 17 package com.android.packageinstaller; 18 19 import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY; 20 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 21 22 import android.Manifest; 23 import android.app.Activity; 24 import android.app.AlertDialog; 25 import android.app.AppOpsManager; 26 import android.app.Dialog; 27 import android.app.DialogFragment; 28 import android.content.ActivityNotFoundException; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.content.DialogInterface; 32 import android.content.Intent; 33 import android.content.pm.ApplicationInfo; 34 import android.content.pm.InstallSourceInfo; 35 import android.content.pm.PackageInfo; 36 import android.content.pm.PackageInstaller; 37 import android.content.pm.PackageInstaller.SessionInfo; 38 import android.content.pm.PackageManager; 39 import android.content.pm.PackageManager.ApplicationInfoFlags; 40 import android.content.pm.PackageManager.NameNotFoundException; 41 import android.graphics.drawable.BitmapDrawable; 42 import android.net.Uri; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.os.Looper; 46 import android.os.Process; 47 import android.os.UserHandle; 48 import android.os.UserManager; 49 import android.provider.Settings; 50 import android.text.Html; 51 import android.text.Spanned; 52 import android.text.TextUtils; 53 import android.text.method.ScrollingMovementMethod; 54 import android.util.Log; 55 import android.view.View; 56 import android.widget.Button; 57 import android.widget.TextView; 58 59 import androidx.annotation.NonNull; 60 61 import java.io.File; 62 import java.util.ArrayList; 63 import java.util.List; 64 65 /** 66 * This activity is launched when a new application is installed via side loading 67 * The package is first parsed and the user is notified of parse errors via a dialog. 68 * If the package is successfully parsed, the user is notified to turn on the install unknown 69 * applications setting. A memory check is made at this point and the user is notified of out 70 * of memory conditions if any. If the package is already existing on the device, 71 * a confirmation dialog (to replace the existing package) is presented to the user. 72 * Based on the user response the package is then installed by launching InstallAppConfirm 73 * sub activity. All state transitions are handled in this activity 74 */ 75 public class PackageInstallerActivity extends Activity { 76 private static final String TAG = "PackageInstaller"; 77 78 private static final int REQUEST_TRUST_EXTERNAL_SOURCE = 1; 79 80 static final String SCHEME_PACKAGE = "package"; 81 82 static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE"; 83 static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG"; 84 static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; 85 static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID"; 86 static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET"; 87 static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE"; 88 private static final String ALLOW_UNKNOWN_SOURCES_KEY = 89 PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; 90 91 private int mSessionId = -1; 92 private Uri mPackageURI; 93 private Uri mOriginatingURI; 94 private Uri mReferrerURI; 95 private int mOriginatingUid = Process.INVALID_UID; 96 /** 97 * The package name corresponding to #mOriginatingUid 98 */ 99 private String mOriginatingPackage; 100 private int mActivityResultCode = Activity.RESULT_CANCELED; 101 private int mPendingUserActionReason = -1; 102 103 private final boolean mLocalLOGV = false; 104 PackageManager mPm; 105 AppOpsManager mAppOpsManager; 106 UserManager mUserManager; 107 PackageInstaller mInstaller; 108 PackageInfo mPkgInfo; 109 String mCallingPackage; 110 private String mCallingAttributionTag; 111 ApplicationInfo mSourceInfo; 112 113 /** 114 * A collection of unknown sources listeners that are actively listening for app ops mode 115 * changes 116 */ 117 private List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1); 118 119 // ApplicationInfo object primarily used for already existing applications 120 private ApplicationInfo mAppInfo; 121 122 // Buttons to indicate user acceptance 123 private Button mOk; 124 125 private PackageUtil.AppSnippet mAppSnippet; 126 127 static final String PREFS_ALLOWED_SOURCES = "allowed_sources"; 128 129 // Dialog identifiers used in showDialog 130 private static final int DLG_BASE = 0; 131 private static final int DLG_PACKAGE_ERROR = DLG_BASE + 1; 132 private static final int DLG_OUT_OF_SPACE = DLG_BASE + 2; 133 private static final int DLG_INSTALL_ERROR = DLG_BASE + 3; 134 private static final int DLG_ANONYMOUS_SOURCE = DLG_BASE + 4; 135 private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 5; 136 137 // If unknown sources are temporary allowed 138 private boolean mAllowUnknownSources; 139 140 // Would the mOk button be enabled if this activity would be resumed 141 private boolean mEnableOk = false; 142 143 private AlertDialog mDialog; 144 startInstallConfirm()145 private void startInstallConfirm() { 146 TextView viewToEnable; 147 148 if (mAppInfo != null) { 149 viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update); 150 151 final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); 152 final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage); 153 if (!TextUtils.isEmpty(existingUpdateOwnerLabel) 154 && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) { 155 String updateOwnerString = 156 getString(R.string.install_confirm_question_update_owner_reminder, 157 requestedUpdateOwnerLabel, existingUpdateOwnerLabel); 158 Spanned styledUpdateOwnerString = 159 Html.fromHtml(updateOwnerString, Html.FROM_HTML_MODE_LEGACY); 160 viewToEnable.setText(styledUpdateOwnerString); 161 mOk.setText(R.string.update_anyway); 162 } else { 163 mOk.setText(R.string.update); 164 } 165 } else { 166 // This is a new application with no permissions. 167 viewToEnable = mDialog.requireViewById(R.id.install_confirm_question); 168 } 169 170 viewToEnable.setVisibility(View.VISIBLE); 171 viewToEnable.setMovementMethod(new ScrollingMovementMethod()); 172 173 mEnableOk = true; 174 mOk.setEnabled(true); 175 mOk.setFilterTouchesWhenObscured(true); 176 } 177 getExistingUpdateOwnerLabel()178 private CharSequence getExistingUpdateOwnerLabel() { 179 return getApplicationLabel(getExistingUpdateOwner()); 180 } 181 getExistingUpdateOwner()182 private String getExistingUpdateOwner() { 183 try { 184 final String packageName = mPkgInfo.packageName; 185 final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName); 186 return sourceInfo.getUpdateOwnerPackageName(); 187 } catch (NameNotFoundException e) { 188 return null; 189 } 190 } 191 getApplicationLabel(String packageName)192 private CharSequence getApplicationLabel(String packageName) { 193 try { 194 final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 195 ApplicationInfoFlags.of(0)); 196 return mPm.getApplicationLabel(appInfo); 197 } catch (NameNotFoundException e) { 198 return null; 199 } 200 } 201 202 /** 203 * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}. 204 * 205 * @param id The dialog type to add 206 */ showDialogInner(int id)207 private void showDialogInner(int id) { 208 if (mLocalLOGV) Log.i(TAG, "showDialogInner(" + id + ")"); 209 DialogFragment currentDialog = 210 (DialogFragment) getFragmentManager().findFragmentByTag("dialog"); 211 if (currentDialog != null) { 212 currentDialog.dismissAllowingStateLoss(); 213 } 214 215 DialogFragment newDialog = createDialog(id); 216 if (newDialog != null) { 217 getFragmentManager().beginTransaction() 218 .add(newDialog, "dialog").commitAllowingStateLoss(); 219 } 220 } 221 222 /** 223 * Create a new dialog. 224 * 225 * @param id The id of the dialog (determines dialog type) 226 * 227 * @return The dialog 228 */ createDialog(int id)229 private DialogFragment createDialog(int id) { 230 if (mLocalLOGV) Log.i(TAG, "createDialog(" + id + ")"); 231 switch (id) { 232 case DLG_PACKAGE_ERROR: 233 return PackageUtil.SimpleErrorDialog.newInstance(R.string.Parse_error_dlg_text); 234 case DLG_OUT_OF_SPACE: 235 return OutOfSpaceDialog.newInstance( 236 mPm.getApplicationLabel(mPkgInfo.applicationInfo)); 237 case DLG_INSTALL_ERROR: 238 return InstallErrorDialog.newInstance( 239 mPm.getApplicationLabel(mPkgInfo.applicationInfo)); 240 case DLG_EXTERNAL_SOURCE_BLOCKED: 241 return ExternalSourcesBlockedDialog.newInstance(mOriginatingPackage); 242 case DLG_ANONYMOUS_SOURCE: 243 return AnonymousSourceDialog.newInstance(); 244 } 245 return null; 246 } 247 248 @Override onActivityResult(int request, int result, Intent data)249 public void onActivityResult(int request, int result, Intent data) { 250 if (request == REQUEST_TRUST_EXTERNAL_SOURCE) { 251 // Log the fact that the app is requesting an install, and is now allowed to do it 252 // (before this point we could only log that it's requesting an install, but isn't 253 // allowed to do it yet). 254 String appOpStr = 255 AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); 256 int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, 257 mOriginatingPackage, mCallingAttributionTag, 258 "Successfully started package installation activity"); 259 if (appOpMode == AppOpsManager.MODE_ALLOWED) { 260 // The user has just allowed this package to install other packages 261 // (via Settings). 262 mAllowUnknownSources = true; 263 DialogFragment currentDialog = 264 (DialogFragment) getFragmentManager().findFragmentByTag("dialog"); 265 if (currentDialog != null) { 266 currentDialog.dismissAllowingStateLoss(); 267 } 268 initiateInstall(); 269 } else { 270 finish(); 271 } 272 } else { 273 finish(); 274 } 275 } 276 getPackageNameForUid(int sourceUid)277 private String getPackageNameForUid(int sourceUid) { 278 // If the sourceUid belongs to the system downloads provider, we explicitly return the 279 // name of the Download Manager package. This is because its UID is shared with multiple 280 // packages, resulting in uncertainty about which package will end up first in the list 281 // of packages associated with this UID 282 ApplicationInfo systemDownloadProviderInfo = PackageUtil.getSystemDownloadsProviderInfo( 283 mPm, sourceUid); 284 if (systemDownloadProviderInfo != null) { 285 return systemDownloadProviderInfo.packageName; 286 } 287 String[] packagesForUid = mPm.getPackagesForUid(sourceUid); 288 if (packagesForUid == null) { 289 return null; 290 } 291 if (packagesForUid.length > 1) { 292 if (mCallingPackage != null) { 293 for (String packageName : packagesForUid) { 294 if (packageName.equals(mCallingPackage)) { 295 return packageName; 296 } 297 } 298 } 299 Log.i(TAG, "Multiple packages found for source uid " + sourceUid); 300 } 301 return packagesForUid[0]; 302 } 303 initiateInstall()304 private void initiateInstall() { 305 final String existingUpdateOwner = getExistingUpdateOwner(); 306 if (mSessionId == SessionInfo.INVALID_ID && 307 !TextUtils.isEmpty(existingUpdateOwner) && 308 !TextUtils.equals(existingUpdateOwner, mOriginatingPackage)) { 309 // Since update ownership is being changed, the system will request another 310 // user confirmation shortly. Thus, we don't need to ask the user to confirm 311 // installation here. 312 startInstall(); 313 return; 314 } 315 316 // Proceed with user confirmation as we are not changing the update-owner in this install. 317 String pkgName = mPkgInfo.packageName; 318 // Check if there is already a package on the device with this name 319 // but it has been renamed to something else. 320 String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName }); 321 if (oldName != null && oldName.length > 0 && oldName[0] != null) { 322 pkgName = oldName[0]; 323 mPkgInfo.packageName = pkgName; 324 mPkgInfo.applicationInfo.packageName = pkgName; 325 } 326 // Check if package is already installed. display confirmation dialog if replacing pkg 327 try { 328 // This is a little convoluted because we want to get all uninstalled 329 // apps, but this may include apps with just data, and if it is just 330 // data we still want to count it as "installed". 331 mAppInfo = mPm.getApplicationInfo(pkgName, 332 PackageManager.MATCH_UNINSTALLED_PACKAGES); 333 // If the package is archived, treat it as update case. 334 if (!mAppInfo.isArchived && (mAppInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { 335 mAppInfo = null; 336 } 337 } catch (NameNotFoundException e) { 338 mAppInfo = null; 339 } 340 341 startInstallConfirm(); 342 } 343 setPmResult(int pmResult)344 void setPmResult(int pmResult) { 345 Intent result = new Intent(); 346 result.putExtra(Intent.EXTRA_INSTALL_RESULT, pmResult); 347 setResult(pmResult == PackageManager.INSTALL_SUCCEEDED 348 ? RESULT_OK : RESULT_FIRST_USER, result); 349 } 350 generateStubPackageInfo(String packageName)351 private static PackageInfo generateStubPackageInfo(String packageName) { 352 final PackageInfo info = new PackageInfo(); 353 final ApplicationInfo aInfo = new ApplicationInfo(); 354 info.applicationInfo = aInfo; 355 info.packageName = info.applicationInfo.packageName = packageName; 356 return info; 357 } 358 359 @Override onCreate(Bundle icicle)360 protected void onCreate(Bundle icicle) { 361 if (mLocalLOGV) Log.i(TAG, "creating for user " + UserHandle.myUserId()); 362 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 363 364 super.onCreate(null); 365 366 if (icicle != null) { 367 mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY); 368 } 369 setFinishOnTouchOutside(true); 370 371 mPm = getPackageManager(); 372 mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); 373 mInstaller = mPm.getPackageInstaller(); 374 mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); 375 376 final Intent intent = getIntent(); 377 final String action = intent.getAction(); 378 379 mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE); 380 mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG); 381 mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO); 382 mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID); 383 mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID) 384 ? getPackageNameForUid(mOriginatingUid) : null; 385 386 final Object packageSource; 387 if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) { 388 final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 389 -1 /* defaultValue */); 390 final SessionInfo info = mInstaller.getSessionInfo(sessionId); 391 String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null; 392 if (info == null || !info.isSealed() || resolvedPath == null) { 393 Log.w(TAG, "Session " + sessionId + " in funky state; ignoring"); 394 finish(); 395 return; 396 } 397 398 mSessionId = sessionId; 399 packageSource = Uri.fromFile(new File(resolvedPath)); 400 mOriginatingURI = null; 401 mReferrerURI = null; 402 mPendingUserActionReason = info.getPendingUserActionReason(); 403 } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) { 404 final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 405 -1 /* defaultValue */); 406 final SessionInfo info = mInstaller.getSessionInfo(sessionId); 407 if (info == null || !info.isPreApprovalRequested()) { 408 Log.w(TAG, "Session " + sessionId + " in funky state; ignoring"); 409 finish(); 410 return; 411 } 412 413 mSessionId = sessionId; 414 packageSource = info; 415 mOriginatingURI = null; 416 mReferrerURI = null; 417 mPendingUserActionReason = info.getPendingUserActionReason(); 418 } else { 419 // Two possible callers: 420 // 1. InstallStart with "SCHEME_PACKAGE". 421 // 2. InstallStaging with "SCHEME_FILE" and EXTRA_STAGED_SESSION_ID with staged 422 // session id. 423 mSessionId = -1; 424 packageSource = intent.getData(); 425 mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); 426 mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER); 427 mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; 428 } 429 430 // if there's nothing to do, quietly slip into the ether 431 if (packageSource == null) { 432 Log.w(TAG, "Unspecified source"); 433 setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); 434 finish(); 435 return; 436 } 437 438 final boolean wasSetUp = processAppSnippet(packageSource); 439 440 if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp); 441 442 if (!wasSetUp) { 443 return; 444 } 445 } 446 447 @Override onResume()448 protected void onResume() { 449 super.onResume(); 450 451 if (mLocalLOGV) Log.i(TAG, "onResume(): mAppSnippet=" + mAppSnippet); 452 453 if (mAppSnippet != null) { 454 // load placeholder layout with OK button disabled until we override this layout in 455 // startInstallConfirm 456 bindUi(); 457 checkIfAllowedAndInitiateInstall(); 458 } 459 460 if (mOk != null) { 461 mOk.setEnabled(mEnableOk); 462 } 463 } 464 465 @Override onPause()466 protected void onPause() { 467 super.onPause(); 468 469 if (mOk != null) { 470 // Don't allow the install button to be clicked as there might be overlays 471 mOk.setEnabled(false); 472 } 473 } 474 475 @Override onSaveInstanceState(Bundle outState)476 protected void onSaveInstanceState(Bundle outState) { 477 super.onSaveInstanceState(outState); 478 479 outState.putBoolean(ALLOW_UNKNOWN_SOURCES_KEY, mAllowUnknownSources); 480 } 481 482 @Override onDestroy()483 protected void onDestroy() { 484 while (!mActiveUnknownSourcesListeners.isEmpty()) { 485 unregister(mActiveUnknownSourcesListeners.get(0)); 486 } 487 if (mDialog != null) { 488 mDialog.dismiss(); 489 } 490 super.onDestroy(); 491 } 492 bindUi()493 private void bindUi() { 494 AlertDialog.Builder builder = new AlertDialog.Builder(this); 495 builder.setIcon(mAppSnippet.icon); 496 builder.setTitle(mAppSnippet.label); 497 builder.setView(R.layout.install_content_view); 498 builder.setPositiveButton(getString(R.string.install), 499 (ignored, ignored2) -> { 500 if (mOk.isEnabled()) { 501 if (mSessionId != -1) { 502 setActivityResult(RESULT_OK); 503 finish(); 504 } else { 505 startInstall(); 506 } 507 } 508 }); 509 builder.setNegativeButton(getString(R.string.cancel), 510 (ignored, ignored2) -> { 511 // Cancel and finish 512 setActivityResult(RESULT_CANCELED); 513 finish(); 514 }); 515 builder.setOnCancelListener(dialog -> { 516 // Cancel and finish 517 setActivityResult(RESULT_CANCELED); 518 finish(); 519 }); 520 mDialog = builder.create(); 521 mDialog.show(); 522 523 mOk = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 524 mOk.setEnabled(false); 525 526 if (!mOk.isInTouchMode()) { 527 mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus(); 528 } 529 } 530 setActivityResult(int resultCode)531 private void setActivityResult(int resultCode) { 532 mActivityResultCode = resultCode; 533 super.setResult(resultCode); 534 } 535 536 @Override finish()537 public void finish() { 538 if (mSessionId != -1) { 539 if (mActivityResultCode == Activity.RESULT_OK) { 540 mInstaller.setPermissionsResult(mSessionId, true); 541 } else { 542 mInstaller.setPermissionsResult(mSessionId, false); 543 } 544 } 545 super.finish(); 546 } 547 548 /** 549 * Check if it is allowed to install the package and initiate install if allowed. 550 */ checkIfAllowedAndInitiateInstall()551 private void checkIfAllowedAndInitiateInstall() { 552 if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) { 553 if (mLocalLOGV) Log.i(TAG, "install allowed"); 554 initiateInstall(); 555 } else { 556 handleUnknownSources(); 557 } 558 } 559 handleUnknownSources()560 private void handleUnknownSources() { 561 if (mOriginatingPackage == null) { 562 Log.i(TAG, "No source found for package " + mPkgInfo.packageName); 563 showDialogInner(DLG_ANONYMOUS_SOURCE); 564 return; 565 } 566 // Shouldn't use static constant directly, see b/65534401. 567 final String appOpStr = 568 AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); 569 final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, 570 mOriginatingPackage, mCallingAttributionTag, 571 "Started package installation activity"); 572 if (mLocalLOGV) Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode); 573 switch (appOpMode) { 574 case AppOpsManager.MODE_DEFAULT: 575 mAppOpsManager.setMode(appOpStr, mOriginatingUid, 576 mOriginatingPackage, AppOpsManager.MODE_ERRORED); 577 // fall through 578 case AppOpsManager.MODE_ERRORED: 579 showDialogInner(DLG_EXTERNAL_SOURCE_BLOCKED); 580 break; 581 case AppOpsManager.MODE_ALLOWED: 582 initiateInstall(); 583 break; 584 default: 585 Log.e(TAG, "Invalid app op mode " + appOpMode 586 + " for OP_REQUEST_INSTALL_PACKAGES found for uid " + mOriginatingUid); 587 finish(); 588 break; 589 } 590 } 591 592 /** 593 * Parse the Uri and set up the installer for this package. 594 * 595 * @param packageUri The URI to parse 596 * 597 * @return {@code true} iff the installer could be set up 598 */ processPackageUri(final Uri packageUri)599 private boolean processPackageUri(final Uri packageUri) { 600 mPackageURI = packageUri; 601 final String scheme = packageUri.getScheme(); 602 final String packageName = packageUri.getSchemeSpecificPart(); 603 604 if (mLocalLOGV) Log.i(TAG, "processPackageUri(): uri=" + packageUri + ", scheme=" + scheme); 605 606 switch (scheme) { 607 case SCHEME_PACKAGE: { 608 for (UserHandle handle : mUserManager.getUserHandles(true)) { 609 PackageManager pmForUser = createContextAsUser(handle, 0) 610 .getPackageManager(); 611 try { 612 if (pmForUser.canPackageQuery(mCallingPackage, packageName)) { 613 mPkgInfo = pmForUser.getPackageInfo(packageName, 614 PackageManager.GET_PERMISSIONS 615 | PackageManager.MATCH_UNINSTALLED_PACKAGES); 616 } 617 } catch (NameNotFoundException e) { 618 } 619 } 620 if (mPkgInfo == null) { 621 Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart() 622 + " not available. Discontinuing installation"); 623 showDialogInner(DLG_PACKAGE_ERROR); 624 setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); 625 return false; 626 } 627 CharSequence label = mPm.getApplicationLabel(mPkgInfo.applicationInfo); 628 if (mLocalLOGV) Log.i(TAG, "creating snippet for " + label); 629 mAppSnippet = new PackageUtil.AppSnippet(label, 630 mPm.getApplicationIcon(mPkgInfo.applicationInfo), getBaseContext()); 631 } break; 632 633 case ContentResolver.SCHEME_FILE: { 634 File sourceFile = new File(packageUri.getPath()); 635 mPkgInfo = PackageUtil.getPackageInfo(this, sourceFile, 636 PackageManager.GET_PERMISSIONS); 637 638 // Check for parse errors 639 if (mPkgInfo == null) { 640 Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); 641 showDialogInner(DLG_PACKAGE_ERROR); 642 setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); 643 return false; 644 } 645 if (mLocalLOGV) Log.i(TAG, "creating snippet for local file " + sourceFile); 646 mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile); 647 } break; 648 649 default: { 650 throw new IllegalArgumentException("Unexpected URI scheme " + packageUri); 651 } 652 } 653 654 return true; 655 } 656 657 /** 658 * Use the SessionInfo and set up the installer for pre-commit install session. 659 * 660 * @param info The SessionInfo to compose 661 * 662 * @return {@code true} iff the installer could be set up 663 */ processSessionInfo(@onNull SessionInfo info)664 private boolean processSessionInfo(@NonNull SessionInfo info) { 665 mPkgInfo = generateStubPackageInfo(info.getAppPackageName()); 666 mAppSnippet = new PackageUtil.AppSnippet(info.getAppLabel(), 667 info.getAppIcon() != null ? new BitmapDrawable(getResources(), info.getAppIcon()) 668 : getPackageManager().getDefaultActivityIcon(), getBaseContext()); 669 return true; 670 } 671 672 /** 673 * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install 674 * session) to set up the installer for this install. 675 * 676 * @param source The source of package URI or SessionInfo 677 * 678 * @return {@code true} iff the installer could be set up 679 */ processAppSnippet(@onNull Object source)680 private boolean processAppSnippet(@NonNull Object source) { 681 if (source instanceof Uri) { 682 return processPackageUri((Uri) source); 683 } else if (source instanceof SessionInfo) { 684 return processSessionInfo((SessionInfo) source); 685 } 686 687 return false; 688 } 689 690 @Override onBackPressed()691 public void onBackPressed() { 692 if (mSessionId != -1) { 693 setActivityResult(RESULT_CANCELED); 694 } 695 super.onBackPressed(); 696 } 697 startInstall()698 private void startInstall() { 699 String installerPackageName = getIntent().getStringExtra( 700 Intent.EXTRA_INSTALLER_PACKAGE_NAME); 701 int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0); 702 703 // Start subactivity to actually install the application 704 Intent newIntent = new Intent(); 705 newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, 706 mPkgInfo.applicationInfo); 707 newIntent.setData(mPackageURI); 708 newIntent.setClass(this, InstallInstalling.class); 709 if (mOriginatingURI != null) { 710 newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI); 711 } 712 if (mReferrerURI != null) { 713 newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI); 714 } 715 if (mOriginatingUid != Process.INVALID_UID) { 716 newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid); 717 } 718 if (installerPackageName != null) { 719 newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, 720 installerPackageName); 721 } 722 if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { 723 newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); 724 } 725 if (stagedSessionId > 0) { 726 newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId); 727 } 728 if (mAppSnippet != null) { 729 newIntent.putExtra(EXTRA_APP_SNIPPET, mAppSnippet); 730 } 731 newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 732 if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI); 733 startActivity(newIntent); 734 finish(); 735 } 736 737 /** 738 * Dialog to show when the source of apk can not be identified 739 */ 740 public static class AnonymousSourceDialog extends DialogFragment { newInstance()741 static AnonymousSourceDialog newInstance() { 742 return new AnonymousSourceDialog(); 743 } 744 745 @Override onCreateDialog(Bundle savedInstanceState)746 public Dialog onCreateDialog(Bundle savedInstanceState) { 747 return new AlertDialog.Builder(getActivity()) 748 .setMessage(R.string.anonymous_source_warning) 749 .setPositiveButton(R.string.anonymous_source_continue, 750 ((dialog, which) -> { 751 PackageInstallerActivity activity = ((PackageInstallerActivity) 752 getActivity()); 753 754 activity.mAllowUnknownSources = true; 755 activity.initiateInstall(); 756 })) 757 .setNegativeButton(R.string.cancel, ((dialog, which) -> getActivity().finish())) 758 .create(); 759 } 760 761 @Override onCancel(DialogInterface dialog)762 public void onCancel(DialogInterface dialog) { 763 getActivity().finish(); 764 } 765 } 766 767 /** 768 * An error dialog shown when the device is out of space 769 */ 770 public static class OutOfSpaceDialog extends AppErrorDialog { newInstance(@onNull CharSequence applicationLabel)771 static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) { 772 OutOfSpaceDialog dialog = new OutOfSpaceDialog(); 773 dialog.setArgument(applicationLabel); 774 return dialog; 775 } 776 777 @Override createDialog(@onNull CharSequence argument)778 protected Dialog createDialog(@NonNull CharSequence argument) { 779 String dlgText = getString(R.string.out_of_space_dlg_text, argument); 780 return new AlertDialog.Builder(getActivity()) 781 .setMessage(dlgText) 782 .setPositiveButton(R.string.manage_applications, (dialog, which) -> { 783 // launch manage applications 784 Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE"); 785 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 786 startActivity(intent); 787 getActivity().finish(); 788 }) 789 .setNegativeButton(R.string.cancel, (dialog, which) -> getActivity().finish()) 790 .create(); 791 } 792 } 793 794 /** 795 * A generic install-error dialog 796 */ 797 public static class InstallErrorDialog extends AppErrorDialog { 798 static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) { 799 InstallErrorDialog dialog = new InstallErrorDialog(); 800 dialog.setArgument(applicationLabel); 801 return dialog; 802 } 803 804 @Override 805 protected Dialog createDialog(@NonNull CharSequence argument) { 806 return new AlertDialog.Builder(getActivity()) 807 .setNeutralButton(R.string.ok, (dialog, which) -> getActivity().finish()) 808 .setMessage(getString(R.string.install_failed_msg, argument)) 809 .create(); 810 } 811 } 812 813 private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener { 814 815 @Override 816 public void onOpChanged(String op, String packageName) { 817 if (!mOriginatingPackage.equals(packageName)) { 818 return; 819 } 820 unregister(this); 821 mActiveUnknownSourcesListeners.remove(this); 822 if (isDestroyed()) { 823 return; 824 } 825 new Handler(Looper.getMainLooper()).postDelayed(() -> { 826 if (!isDestroyed()) { 827 startActivity(getIntent()); 828 // The start flag (FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP) doesn't 829 // work for the multiple user case, i.e. the caller task user and started 830 // Activity user are not the same. To avoid having multiple PIAs in the task, 831 // finish the current PackageInstallerActivity 832 // Because finish() is overridden to set the installation result, we must use 833 // the original finish() method, or the confirmation dialog fails to appear. 834 PackageInstallerActivity.super.finish(); 835 } 836 }, 500); 837 838 } 839 840 } 841 842 private void register(UnknownSourcesListener listener) { 843 mAppOpsManager.startWatchingMode( 844 AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, mOriginatingPackage, 845 listener); 846 mActiveUnknownSourcesListeners.add(listener); 847 } 848 849 private void unregister(UnknownSourcesListener listener) { 850 mAppOpsManager.stopWatchingMode(listener); 851 mActiveUnknownSourcesListeners.remove(listener); 852 } 853 854 /** 855 * An error dialog shown when external sources are not allowed 856 */ 857 public static class ExternalSourcesBlockedDialog extends AppErrorDialog { 858 static AppErrorDialog newInstance(@NonNull String originationPkg) { 859 ExternalSourcesBlockedDialog dialog = 860 new ExternalSourcesBlockedDialog(); 861 dialog.setArgument(originationPkg); 862 return dialog; 863 } 864 865 @Override 866 protected Dialog createDialog(@NonNull CharSequence argument) { 867 868 final PackageInstallerActivity activity = (PackageInstallerActivity)getActivity(); 869 try { 870 PackageManager pm = activity.getPackageManager(); 871 872 ApplicationInfo sourceInfo = pm.getApplicationInfo(argument.toString(), 0); 873 874 return new AlertDialog.Builder(activity) 875 .setTitle(pm.getApplicationLabel(sourceInfo)) 876 .setIcon(pm.getApplicationIcon(sourceInfo)) 877 .setMessage(R.string.untrusted_external_source_warning) 878 .setPositiveButton(R.string.external_sources_settings, 879 (dialog, which) -> { 880 Intent settingsIntent = new Intent(); 881 settingsIntent.setAction( 882 Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); 883 final Uri packageUri = Uri.parse("package:" + argument); 884 settingsIntent.setData(packageUri); 885 settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY); 886 try { 887 activity.register(activity.new UnknownSourcesListener()); 888 activity.startActivityForResult(settingsIntent, 889 REQUEST_TRUST_EXTERNAL_SOURCE); 890 } catch (ActivityNotFoundException exc) { 891 Log.e(TAG, "Settings activity not found for action: " 892 + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); 893 } 894 }) 895 .setNegativeButton(R.string.cancel, 896 (dialog, which) -> activity.finish()) 897 .create(); 898 } catch (NameNotFoundException e) { 899 Log.e(TAG, "Did not find app info for " + argument); 900 activity.finish(); 901 return null; 902 } 903 } 904 } 905 906 /** 907 * Superclass for all error dialogs. Stores a single CharSequence argument 908 */ 909 public abstract static class AppErrorDialog extends DialogFragment { 910 private static final String ARGUMENT_KEY = AppErrorDialog.class.getName() + "ARGUMENT_KEY"; 911 912 protected void setArgument(@NonNull CharSequence argument) { 913 Bundle args = new Bundle(); 914 args.putCharSequence(ARGUMENT_KEY, argument); 915 setArguments(args); 916 } 917 918 protected abstract Dialog createDialog(@NonNull CharSequence argument); 919 920 @Override 921 public Dialog onCreateDialog(Bundle savedInstanceState) { 922 return createDialog(getArguments().getString(ARGUMENT_KEY)); 923 } 924 925 @Override 926 public void onCancel(DialogInterface dialog) { 927 getActivity().finish(); 928 } 929 } 930 } 931