/* ** ** Copyright 2007, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** ** http://www.apache.org/licenses/LICENSE-2.0 ** ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. */ package com.android.packageinstaller; import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.Dialog; import android.app.DialogFragment; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import java.io.File; import java.util.ArrayList; import java.util.List; /** * This activity is launched when a new application is installed via side loading * The package is first parsed and the user is notified of parse errors via a dialog. * If the package is successfully parsed, the user is notified to turn on the install unknown * applications setting. A memory check is made at this point and the user is notified of out * of memory conditions if any. If the package is already existing on the device, * a confirmation dialog (to replace the existing package) is presented to the user. * Based on the user response the package is then installed by launching InstallAppConfirm * sub activity. All state transitions are handled in this activity */ public class PackageInstallerActivity extends Activity { private static final String TAG = "PackageInstaller"; private static final int REQUEST_TRUST_EXTERNAL_SOURCE = 1; static final String SCHEME_PACKAGE = "package"; static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE"; static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG"; static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID"; static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET"; static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE"; private static final String ALLOW_UNKNOWN_SOURCES_KEY = PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; private int mSessionId = -1; private Uri mPackageURI; private Uri mOriginatingURI; private Uri mReferrerURI; private int mOriginatingUid = Process.INVALID_UID; /** * The package name corresponding to #mOriginatingUid */ private String mOriginatingPackage; private int mActivityResultCode = Activity.RESULT_CANCELED; private int mPendingUserActionReason = -1; private final boolean mLocalLOGV = false; PackageManager mPm; AppOpsManager mAppOpsManager; UserManager mUserManager; PackageInstaller mInstaller; PackageInfo mPkgInfo; String mCallingPackage; private String mCallingAttributionTag; ApplicationInfo mSourceInfo; /** * A collection of unknown sources listeners that are actively listening for app ops mode * changes */ private List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1); // ApplicationInfo object primarily used for already existing applications private ApplicationInfo mAppInfo; // Buttons to indicate user acceptance private Button mOk; private PackageUtil.AppSnippet mAppSnippet; static final String PREFS_ALLOWED_SOURCES = "allowed_sources"; // Dialog identifiers used in showDialog private static final int DLG_BASE = 0; private static final int DLG_PACKAGE_ERROR = DLG_BASE + 1; private static final int DLG_OUT_OF_SPACE = DLG_BASE + 2; private static final int DLG_INSTALL_ERROR = DLG_BASE + 3; private static final int DLG_ANONYMOUS_SOURCE = DLG_BASE + 4; private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 5; // If unknown sources are temporary allowed private boolean mAllowUnknownSources; // Would the mOk button be enabled if this activity would be resumed private boolean mEnableOk = false; private AlertDialog mDialog; private void startInstallConfirm() { TextView viewToEnable; if (mAppInfo != null) { viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update); final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage); if (!TextUtils.isEmpty(existingUpdateOwnerLabel) && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) { String updateOwnerString = getString(R.string.install_confirm_question_update_owner_reminder, requestedUpdateOwnerLabel, existingUpdateOwnerLabel); Spanned styledUpdateOwnerString = Html.fromHtml(updateOwnerString, Html.FROM_HTML_MODE_LEGACY); viewToEnable.setText(styledUpdateOwnerString); mOk.setText(R.string.update_anyway); } else { mOk.setText(R.string.update); } } else { // This is a new application with no permissions. viewToEnable = mDialog.requireViewById(R.id.install_confirm_question); } viewToEnable.setVisibility(View.VISIBLE); viewToEnable.setMovementMethod(new ScrollingMovementMethod()); mEnableOk = true; mOk.setEnabled(true); mOk.setFilterTouchesWhenObscured(true); } private CharSequence getExistingUpdateOwnerLabel() { return getApplicationLabel(getExistingUpdateOwner()); } private String getExistingUpdateOwner() { try { final String packageName = mPkgInfo.packageName; final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName); return sourceInfo.getUpdateOwnerPackageName(); } catch (NameNotFoundException e) { return null; } } private CharSequence getApplicationLabel(String packageName) { try { final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, ApplicationInfoFlags.of(0)); return mPm.getApplicationLabel(appInfo); } catch (NameNotFoundException e) { return null; } } /** * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}. * * @param id The dialog type to add */ private void showDialogInner(int id) { if (mLocalLOGV) Log.i(TAG, "showDialogInner(" + id + ")"); DialogFragment currentDialog = (DialogFragment) getFragmentManager().findFragmentByTag("dialog"); if (currentDialog != null) { currentDialog.dismissAllowingStateLoss(); } DialogFragment newDialog = createDialog(id); if (newDialog != null) { getFragmentManager().beginTransaction() .add(newDialog, "dialog").commitAllowingStateLoss(); } } /** * Create a new dialog. * * @param id The id of the dialog (determines dialog type) * * @return The dialog */ private DialogFragment createDialog(int id) { if (mLocalLOGV) Log.i(TAG, "createDialog(" + id + ")"); switch (id) { case DLG_PACKAGE_ERROR: return PackageUtil.SimpleErrorDialog.newInstance(R.string.Parse_error_dlg_text); case DLG_OUT_OF_SPACE: return OutOfSpaceDialog.newInstance( mPm.getApplicationLabel(mPkgInfo.applicationInfo)); case DLG_INSTALL_ERROR: return InstallErrorDialog.newInstance( mPm.getApplicationLabel(mPkgInfo.applicationInfo)); case DLG_EXTERNAL_SOURCE_BLOCKED: return ExternalSourcesBlockedDialog.newInstance(mOriginatingPackage); case DLG_ANONYMOUS_SOURCE: return AnonymousSourceDialog.newInstance(); } return null; } @Override public void onActivityResult(int request, int result, Intent data) { if (request == REQUEST_TRUST_EXTERNAL_SOURCE) { // Log the fact that the app is requesting an install, and is now allowed to do it // (before this point we could only log that it's requesting an install, but isn't // allowed to do it yet). String appOpStr = AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, mOriginatingPackage, mCallingAttributionTag, "Successfully started package installation activity"); if (appOpMode == AppOpsManager.MODE_ALLOWED) { // The user has just allowed this package to install other packages // (via Settings). mAllowUnknownSources = true; DialogFragment currentDialog = (DialogFragment) getFragmentManager().findFragmentByTag("dialog"); if (currentDialog != null) { currentDialog.dismissAllowingStateLoss(); } initiateInstall(); } else { finish(); } } else { finish(); } } private String getPackageNameForUid(int sourceUid) { // If the sourceUid belongs to the system downloads provider, we explicitly return the // name of the Download Manager package. This is because its UID is shared with multiple // packages, resulting in uncertainty about which package will end up first in the list // of packages associated with this UID ApplicationInfo systemDownloadProviderInfo = PackageUtil.getSystemDownloadsProviderInfo( mPm, sourceUid); if (systemDownloadProviderInfo != null) { return systemDownloadProviderInfo.packageName; } String[] packagesForUid = mPm.getPackagesForUid(sourceUid); if (packagesForUid == null) { return null; } if (packagesForUid.length > 1) { if (mCallingPackage != null) { for (String packageName : packagesForUid) { if (packageName.equals(mCallingPackage)) { return packageName; } } } Log.i(TAG, "Multiple packages found for source uid " + sourceUid); } return packagesForUid[0]; } private void initiateInstall() { final String existingUpdateOwner = getExistingUpdateOwner(); if (mSessionId == SessionInfo.INVALID_ID && !TextUtils.isEmpty(existingUpdateOwner) && !TextUtils.equals(existingUpdateOwner, mOriginatingPackage)) { // Since update ownership is being changed, the system will request another // user confirmation shortly. Thus, we don't need to ask the user to confirm // installation here. startInstall(); return; } // Proceed with user confirmation as we are not changing the update-owner in this install. String pkgName = mPkgInfo.packageName; // Check if there is already a package on the device with this name // but it has been renamed to something else. String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName }); if (oldName != null && oldName.length > 0 && oldName[0] != null) { pkgName = oldName[0]; mPkgInfo.packageName = pkgName; mPkgInfo.applicationInfo.packageName = pkgName; } // Check if package is already installed. display confirmation dialog if replacing pkg try { // This is a little convoluted because we want to get all uninstalled // apps, but this may include apps with just data, and if it is just // data we still want to count it as "installed". mAppInfo = mPm.getApplicationInfo(pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES); // If the package is archived, treat it as update case. if (!mAppInfo.isArchived && (mAppInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { mAppInfo = null; } } catch (NameNotFoundException e) { mAppInfo = null; } startInstallConfirm(); } void setPmResult(int pmResult) { Intent result = new Intent(); result.putExtra(Intent.EXTRA_INSTALL_RESULT, pmResult); setResult(pmResult == PackageManager.INSTALL_SUCCEEDED ? RESULT_OK : RESULT_FIRST_USER, result); } private static PackageInfo generateStubPackageInfo(String packageName) { final PackageInfo info = new PackageInfo(); final ApplicationInfo aInfo = new ApplicationInfo(); info.applicationInfo = aInfo; info.packageName = info.applicationInfo.packageName = packageName; return info; } @Override protected void onCreate(Bundle icicle) { if (mLocalLOGV) Log.i(TAG, "creating for user " + UserHandle.myUserId()); getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); super.onCreate(null); if (icicle != null) { mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY); } setFinishOnTouchOutside(true); mPm = getPackageManager(); mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); mInstaller = mPm.getPackageInstaller(); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); final Intent intent = getIntent(); final String action = intent.getAction(); mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE); mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG); mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO); mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID); mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID) ? getPackageNameForUid(mOriginatingUid) : null; final Object packageSource; if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); final SessionInfo info = mInstaller.getSessionInfo(sessionId); String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null; if (info == null || !info.isSealed() || resolvedPath == null) { Log.w(TAG, "Session " + sessionId + " in funky state; ignoring"); finish(); return; } mSessionId = sessionId; packageSource = Uri.fromFile(new File(resolvedPath)); mOriginatingURI = null; mReferrerURI = null; mPendingUserActionReason = info.getPendingUserActionReason(); } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); final SessionInfo info = mInstaller.getSessionInfo(sessionId); if (info == null || !info.isPreApprovalRequested()) { Log.w(TAG, "Session " + sessionId + " in funky state; ignoring"); finish(); return; } mSessionId = sessionId; packageSource = info; mOriginatingURI = null; mReferrerURI = null; mPendingUserActionReason = info.getPendingUserActionReason(); } else { // Two possible callers: // 1. InstallStart with "SCHEME_PACKAGE". // 2. InstallStaging with "SCHEME_FILE" and EXTRA_STAGED_SESSION_ID with staged // session id. mSessionId = -1; packageSource = intent.getData(); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER); mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; } // if there's nothing to do, quietly slip into the ether if (packageSource == null) { Log.w(TAG, "Unspecified source"); setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); finish(); return; } final boolean wasSetUp = processAppSnippet(packageSource); if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp); if (!wasSetUp) { return; } } @Override protected void onResume() { super.onResume(); if (mLocalLOGV) Log.i(TAG, "onResume(): mAppSnippet=" + mAppSnippet); if (mAppSnippet != null) { // load placeholder layout with OK button disabled until we override this layout in // startInstallConfirm bindUi(); checkIfAllowedAndInitiateInstall(); } if (mOk != null) { mOk.setEnabled(mEnableOk); } } @Override protected void onPause() { super.onPause(); if (mOk != null) { // Don't allow the install button to be clicked as there might be overlays mOk.setEnabled(false); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(ALLOW_UNKNOWN_SOURCES_KEY, mAllowUnknownSources); } @Override protected void onDestroy() { while (!mActiveUnknownSourcesListeners.isEmpty()) { unregister(mActiveUnknownSourcesListeners.get(0)); } if (mDialog != null) { mDialog.dismiss(); } super.onDestroy(); } private void bindUi() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(mAppSnippet.icon); builder.setTitle(mAppSnippet.label); builder.setView(R.layout.install_content_view); builder.setPositiveButton(getString(R.string.install), (ignored, ignored2) -> { if (mOk.isEnabled()) { if (mSessionId != -1) { setActivityResult(RESULT_OK); finish(); } else { startInstall(); } } }); builder.setNegativeButton(getString(R.string.cancel), (ignored, ignored2) -> { // Cancel and finish setActivityResult(RESULT_CANCELED); finish(); }); builder.setOnCancelListener(dialog -> { // Cancel and finish setActivityResult(RESULT_CANCELED); finish(); }); mDialog = builder.create(); mDialog.show(); mOk = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); mOk.setEnabled(false); if (!mOk.isInTouchMode()) { mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus(); } } private void setActivityResult(int resultCode) { mActivityResultCode = resultCode; super.setResult(resultCode); } @Override public void finish() { if (mSessionId != -1) { if (mActivityResultCode == Activity.RESULT_OK) { mInstaller.setPermissionsResult(mSessionId, true); } else { mInstaller.setPermissionsResult(mSessionId, false); } } super.finish(); } /** * Check if it is allowed to install the package and initiate install if allowed. */ private void checkIfAllowedAndInitiateInstall() { if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) { if (mLocalLOGV) Log.i(TAG, "install allowed"); initiateInstall(); } else { handleUnknownSources(); } } private void handleUnknownSources() { if (mOriginatingPackage == null) { Log.i(TAG, "No source found for package " + mPkgInfo.packageName); showDialogInner(DLG_ANONYMOUS_SOURCE); return; } // Shouldn't use static constant directly, see b/65534401. final String appOpStr = AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, mOriginatingPackage, mCallingAttributionTag, "Started package installation activity"); if (mLocalLOGV) Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode); switch (appOpMode) { case AppOpsManager.MODE_DEFAULT: mAppOpsManager.setMode(appOpStr, mOriginatingUid, mOriginatingPackage, AppOpsManager.MODE_ERRORED); // fall through case AppOpsManager.MODE_ERRORED: showDialogInner(DLG_EXTERNAL_SOURCE_BLOCKED); break; case AppOpsManager.MODE_ALLOWED: initiateInstall(); break; default: Log.e(TAG, "Invalid app op mode " + appOpMode + " for OP_REQUEST_INSTALL_PACKAGES found for uid " + mOriginatingUid); finish(); break; } } /** * Parse the Uri and set up the installer for this package. * * @param packageUri The URI to parse * * @return {@code true} iff the installer could be set up */ private boolean processPackageUri(final Uri packageUri) { mPackageURI = packageUri; final String scheme = packageUri.getScheme(); final String packageName = packageUri.getSchemeSpecificPart(); if (mLocalLOGV) Log.i(TAG, "processPackageUri(): uri=" + packageUri + ", scheme=" + scheme); switch (scheme) { case SCHEME_PACKAGE: { for (UserHandle handle : mUserManager.getUserHandles(true)) { PackageManager pmForUser = createContextAsUser(handle, 0) .getPackageManager(); try { if (pmForUser.canPackageQuery(mCallingPackage, packageName)) { mPkgInfo = pmForUser.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS | PackageManager.MATCH_UNINSTALLED_PACKAGES); } } catch (NameNotFoundException e) { } } if (mPkgInfo == null) { Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart() + " not available. Discontinuing installation"); showDialogInner(DLG_PACKAGE_ERROR); setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); return false; } CharSequence label = mPm.getApplicationLabel(mPkgInfo.applicationInfo); if (mLocalLOGV) Log.i(TAG, "creating snippet for " + label); mAppSnippet = new PackageUtil.AppSnippet(label, mPm.getApplicationIcon(mPkgInfo.applicationInfo), getBaseContext()); } break; case ContentResolver.SCHEME_FILE: { File sourceFile = new File(packageUri.getPath()); mPkgInfo = PackageUtil.getPackageInfo(this, sourceFile, PackageManager.GET_PERMISSIONS); // Check for parse errors if (mPkgInfo == null) { Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); showDialogInner(DLG_PACKAGE_ERROR); setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); return false; } if (mLocalLOGV) Log.i(TAG, "creating snippet for local file " + sourceFile); mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile); } break; default: { throw new IllegalArgumentException("Unexpected URI scheme " + packageUri); } } return true; } /** * Use the SessionInfo and set up the installer for pre-commit install session. * * @param info The SessionInfo to compose * * @return {@code true} iff the installer could be set up */ private boolean processSessionInfo(@NonNull SessionInfo info) { mPkgInfo = generateStubPackageInfo(info.getAppPackageName()); mAppSnippet = new PackageUtil.AppSnippet(info.getAppLabel(), info.getAppIcon() != null ? new BitmapDrawable(getResources(), info.getAppIcon()) : getPackageManager().getDefaultActivityIcon(), getBaseContext()); return true; } /** * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install * session) to set up the installer for this install. * * @param source The source of package URI or SessionInfo * * @return {@code true} iff the installer could be set up */ private boolean processAppSnippet(@NonNull Object source) { if (source instanceof Uri) { return processPackageUri((Uri) source); } else if (source instanceof SessionInfo) { return processSessionInfo((SessionInfo) source); } return false; } @Override public void onBackPressed() { if (mSessionId != -1) { setActivityResult(RESULT_CANCELED); } super.onBackPressed(); } private void startInstall() { String installerPackageName = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0); // Start subactivity to actually install the application Intent newIntent = new Intent(); newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mPkgInfo.applicationInfo); newIntent.setData(mPackageURI); newIntent.setClass(this, InstallInstalling.class); if (mOriginatingURI != null) { newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI); } if (mReferrerURI != null) { newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI); } if (mOriginatingUid != Process.INVALID_UID) { newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid); } if (installerPackageName != null) { newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName); } if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); } if (stagedSessionId > 0) { newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId); } if (mAppSnippet != null) { newIntent.putExtra(EXTRA_APP_SNIPPET, mAppSnippet); } newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI); startActivity(newIntent); finish(); } /** * Dialog to show when the source of apk can not be identified */ public static class AnonymousSourceDialog extends DialogFragment { static AnonymousSourceDialog newInstance() { return new AnonymousSourceDialog(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(getActivity()) .setMessage(R.string.anonymous_source_warning) .setPositiveButton(R.string.anonymous_source_continue, ((dialog, which) -> { PackageInstallerActivity activity = ((PackageInstallerActivity) getActivity()); activity.mAllowUnknownSources = true; activity.initiateInstall(); })) .setNegativeButton(R.string.cancel, ((dialog, which) -> getActivity().finish())) .create(); } @Override public void onCancel(DialogInterface dialog) { getActivity().finish(); } } /** * An error dialog shown when the device is out of space */ public static class OutOfSpaceDialog extends AppErrorDialog { static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) { OutOfSpaceDialog dialog = new OutOfSpaceDialog(); dialog.setArgument(applicationLabel); return dialog; } @Override protected Dialog createDialog(@NonNull CharSequence argument) { String dlgText = getString(R.string.out_of_space_dlg_text, argument); return new AlertDialog.Builder(getActivity()) .setMessage(dlgText) .setPositiveButton(R.string.manage_applications, (dialog, which) -> { // launch manage applications Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); getActivity().finish(); }) .setNegativeButton(R.string.cancel, (dialog, which) -> getActivity().finish()) .create(); } } /** * A generic install-error dialog */ public static class InstallErrorDialog extends AppErrorDialog { static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) { InstallErrorDialog dialog = new InstallErrorDialog(); dialog.setArgument(applicationLabel); return dialog; } @Override protected Dialog createDialog(@NonNull CharSequence argument) { return new AlertDialog.Builder(getActivity()) .setNeutralButton(R.string.ok, (dialog, which) -> getActivity().finish()) .setMessage(getString(R.string.install_failed_msg, argument)) .create(); } } private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener { @Override public void onOpChanged(String op, String packageName) { if (!mOriginatingPackage.equals(packageName)) { return; } unregister(this); mActiveUnknownSourcesListeners.remove(this); if (isDestroyed()) { return; } new Handler(Looper.getMainLooper()).postDelayed(() -> { if (!isDestroyed()) { startActivity(getIntent()); // The start flag (FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP) doesn't // work for the multiple user case, i.e. the caller task user and started // Activity user are not the same. To avoid having multiple PIAs in the task, // finish the current PackageInstallerActivity // Because finish() is overridden to set the installation result, we must use // the original finish() method, or the confirmation dialog fails to appear. PackageInstallerActivity.super.finish(); } }, 500); } } private void register(UnknownSourcesListener listener) { mAppOpsManager.startWatchingMode( AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, mOriginatingPackage, listener); mActiveUnknownSourcesListeners.add(listener); } private void unregister(UnknownSourcesListener listener) { mAppOpsManager.stopWatchingMode(listener); mActiveUnknownSourcesListeners.remove(listener); } /** * An error dialog shown when external sources are not allowed */ public static class ExternalSourcesBlockedDialog extends AppErrorDialog { static AppErrorDialog newInstance(@NonNull String originationPkg) { ExternalSourcesBlockedDialog dialog = new ExternalSourcesBlockedDialog(); dialog.setArgument(originationPkg); return dialog; } @Override protected Dialog createDialog(@NonNull CharSequence argument) { final PackageInstallerActivity activity = (PackageInstallerActivity)getActivity(); try { PackageManager pm = activity.getPackageManager(); ApplicationInfo sourceInfo = pm.getApplicationInfo(argument.toString(), 0); return new AlertDialog.Builder(activity) .setTitle(pm.getApplicationLabel(sourceInfo)) .setIcon(pm.getApplicationIcon(sourceInfo)) .setMessage(R.string.untrusted_external_source_warning) .setPositiveButton(R.string.external_sources_settings, (dialog, which) -> { Intent settingsIntent = new Intent(); settingsIntent.setAction( Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); final Uri packageUri = Uri.parse("package:" + argument); settingsIntent.setData(packageUri); settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY); try { activity.register(activity.new UnknownSourcesListener()); activity.startActivityForResult(settingsIntent, REQUEST_TRUST_EXTERNAL_SOURCE); } catch (ActivityNotFoundException exc) { Log.e(TAG, "Settings activity not found for action: " + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); } }) .setNegativeButton(R.string.cancel, (dialog, which) -> activity.finish()) .create(); } catch (NameNotFoundException e) { Log.e(TAG, "Did not find app info for " + argument); activity.finish(); return null; } } } /** * Superclass for all error dialogs. Stores a single CharSequence argument */ public abstract static class AppErrorDialog extends DialogFragment { private static final String ARGUMENT_KEY = AppErrorDialog.class.getName() + "ARGUMENT_KEY"; protected void setArgument(@NonNull CharSequence argument) { Bundle args = new Bundle(); args.putCharSequence(ARGUMENT_KEY, argument); setArguments(args); } protected abstract Dialog createDialog(@NonNull CharSequence argument); @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return createDialog(getArguments().getString(ARGUMENT_KEY)); } @Override public void onCancel(DialogInterface dialog) { getActivity().finish(); } } }