diff options
50 files changed, 3185 insertions, 4001 deletions
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index 25ad9b82189c..98a5a674fcdd 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -35,7 +35,10 @@ android_app { name: "PackageInstaller", defaults: ["platform_app_defaults"], - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], certificate: "platform", privileged: true, @@ -62,7 +65,10 @@ android_app { name: "PackageInstaller_tablet", defaults: ["platform_app_defaults"], - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], certificate: "platform", privileged: true, @@ -91,7 +97,10 @@ android_app { name: "PackageInstaller_tv", defaults: ["platform_app_defaults"], - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], certificate: "platform", privileged: true, diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java deleted file mode 100644 index c8175adc780e..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java +++ /dev/null @@ -1,912 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model; - -import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery; -import static com.android.packageinstaller.v2.model.PackageUtil.generateStubPackageInfo; -import static com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet; -import static com.android.packageinstaller.v2.model.PackageUtil.getPackageInfo; -import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid; -import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner; -import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested; -import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted; -import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_DONE; -import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR; -import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY; -import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.DLG_PACKAGE_ERROR; -import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE; -import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION; -import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE; - -import android.Manifest; -import android.app.Activity; -import android.app.AppOpsManager; -import android.app.PendingIntent; -import android.app.admin.DevicePolicyManager; -import android.content.ContentResolver; -import android.content.Context; -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.content.res.AssetFileDescriptor; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.UserHandle; -import android.os.UserManager; -import android.text.TextUtils; -import android.util.EventLog; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.MutableLiveData; -import com.android.packageinstaller.R; -import com.android.packageinstaller.common.EventResultPersister; -import com.android.packageinstaller.common.InstallEventReceiver; -import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; -import com.android.packageinstaller.v2.model.installstagedata.InstallAborted; -import com.android.packageinstaller.v2.model.installstagedata.InstallFailed; -import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling; -import com.android.packageinstaller.v2.model.installstagedata.InstallReady; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; -import com.android.packageinstaller.v2.model.installstagedata.InstallStaging; -import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess; -import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired; -import java.io.File; -import java.io.IOException; - -public class InstallRepository { - - public static final String EXTRA_STAGED_SESSION_ID = - "com.android.packageinstaller.extra.STAGED_SESSION_ID"; - private static final String SCHEME_PACKAGE = "package"; - private static final String BROADCAST_ACTION = - "com.android.packageinstaller.ACTION_INSTALL_COMMIT"; - private static final String TAG = InstallRepository.class.getSimpleName(); - private final Context mContext; - private final PackageManager mPackageManager; - private final PackageInstaller mPackageInstaller; - private final UserManager mUserManager; - private final DevicePolicyManager mDevicePolicyManager; - private final AppOpsManager mAppOpsManager; - private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>(); - private final MutableLiveData<InstallStage> mInstallResult = new MutableLiveData<>(); - private final boolean mLocalLOGV = false; - private Intent mIntent; - private boolean mIsSessionInstall; - private boolean mIsTrustedSource; - /** - * Session ID for a session created when caller uses PackageInstaller APIs - */ - private int mSessionId; - /** - * Session ID for a session created by this app - */ - private int mStagedSessionId = SessionInfo.INVALID_ID; - private int mCallingUid; - private String mCallingPackage; - private SessionStager mSessionStager; - private AppOpRequestInfo mAppOpRequestInfo; - private AppSnippet mAppSnippet; - /** - * PackageInfo of the app being installed on device. - */ - private PackageInfo mNewPackageInfo; - - public InstallRepository(Context context) { - mContext = context; - mPackageManager = context.getPackageManager(); - mPackageInstaller = mPackageManager.getPackageInstaller(); - mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); - mUserManager = context.getSystemService(UserManager.class); - mAppOpsManager = context.getSystemService(AppOpsManager.class); - } - - /** - * Extracts information from the incoming install intent, checks caller's permission to install - * packages, verifies that the caller is the install session owner (in case of a session based - * install) and checks if the current user has restrictions set that prevent app installation, - * - * @param intent the incoming {@link Intent} object for installing a package - * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName - * @return <p>{@link InstallAborted} if there are errors while performing the checks</p> - * <p>{@link InstallStaging} after successfully performing the checks</p> - */ - public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) { - mIntent = intent; - - String callingAttributionTag = null; - - mIsSessionInstall = - PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()) - || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction()); - - mSessionId = mIsSessionInstall - ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID) - : SessionInfo.INVALID_ID; - - mStagedSessionId = mIntent.getIntExtra(EXTRA_STAGED_SESSION_ID, SessionInfo.INVALID_ID); - - mCallingPackage = callerInfo.getPackageName(); - - if (mCallingPackage == null && mSessionId != SessionInfo.INVALID_ID) { - PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(mSessionId); - mCallingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null; - callingAttributionTag = - (sessionInfo != null) ? sessionInfo.getInstallerAttributionTag() : null; - } - - // Uid of the source package, coming from ActivityManager - mCallingUid = callerInfo.getUid(); - if (mCallingUid == Process.INVALID_UID) { - Log.e(TAG, "Could not determine the launching uid."); - } - final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage); - // Uid of the source package, with a preference to uid from ApplicationInfo - final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid; - mAppOpRequestInfo = new AppOpRequestInfo( - getPackageNameForUid(mContext, originatingUid, mCallingPackage), - originatingUid, callingAttributionTag); - - if (mCallingUid == Process.INVALID_UID && sourceInfo == null) { - // Caller's identity could not be determined. Abort the install - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - - if ((mSessionId != SessionInfo.INVALID_ID - && !isCallerSessionOwner(mPackageInstaller, originatingUid, mSessionId)) - || (mStagedSessionId != SessionInfo.INVALID_ID - && !isCallerSessionOwner(mPackageInstaller, Process.myUid(), mStagedSessionId))) { - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - - mIsTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, mIntent, originatingUid); - - if (!isInstallPermissionGrantedOrRequested(mContext, mCallingUid, originatingUid, - mIsTrustedSource)) { - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - - String restriction = getDevicePolicyRestrictions(); - if (restriction != null) { - InstallAborted.Builder abortedBuilder = - new InstallAborted.Builder(ABORT_REASON_POLICY).setMessage(restriction); - final Intent adminSupportDetailsIntent = - mDevicePolicyManager.createAdminSupportIntent(restriction); - if (adminSupportDetailsIntent != null) { - abortedBuilder.setResultIntent(adminSupportDetailsIntent); - } - return abortedBuilder.build(); - } - - maybeRemoveInvalidInstallerPackageName(callerInfo); - - return new InstallStaging(); - } - - /** - * @return the ApplicationInfo for the installation source (the calling package), if available - */ - @Nullable - private ApplicationInfo getSourceInfo(@Nullable String callingPackage) { - if (callingPackage == null) { - return null; - } - try { - return mPackageManager.getApplicationInfo(callingPackage, 0); - } catch (PackageManager.NameNotFoundException ignored) { - return null; - } - } - - private boolean isInstallRequestFromTrustedSource(ApplicationInfo sourceInfo, Intent intent, - int originatingUid) { - boolean isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false); - return sourceInfo != null && sourceInfo.isPrivilegedApp() - && (isNotUnknownSource - || isPermissionGranted(mContext, Manifest.permission.INSTALL_PACKAGES, originatingUid)); - } - - private String getDevicePolicyRestrictions() { - final String[] restrictions = new String[]{ - UserManager.DISALLOW_INSTALL_APPS, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY - }; - - for (String restriction : restrictions) { - if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) { - continue; - } - return restriction; - } - return null; - } - - private void maybeRemoveInvalidInstallerPackageName(CallerInfo callerInfo) { - final String installerPackageNameFromIntent = - mIntent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME); - if (installerPackageNameFromIntent == null) { - return; - } - if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.getPackageName()) - && !isPermissionGranted(mPackageManager, Manifest.permission.INSTALL_PACKAGES, - callerInfo.getPackageName())) { - Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent - + " is invalid. Remove it."); - EventLog.writeEvent(0x534e4554, "236687884", callerInfo.getUid(), - "Invalid EXTRA_INSTALLER_PACKAGE_NAME"); - mIntent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME); - } - } - - public void stageForInstall() { - Uri uri = mIntent.getData(); - if (mStagedSessionId != SessionInfo.INVALID_ID - || mIsSessionInstall - || (uri != null && SCHEME_PACKAGE.equals(uri.getScheme()))) { - // For a session based install or installing with a package:// URI, there is no file - // for us to stage. - mStagingResult.setValue(new InstallReady()); - return; - } - if (uri != null - && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) - && canPackageQuery(mContext, mCallingUid, uri)) { - - if (mStagedSessionId > 0) { - final PackageInstaller.SessionInfo info = - mPackageInstaller.getSessionInfo(mStagedSessionId); - if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) { - Log.w(TAG, "Session " + mStagedSessionId + " in funky state; ignoring"); - if (info != null) { - cleanupStagingSession(); - } - mStagedSessionId = 0; - } - } - - // Session does not exist, or became invalid. - if (mStagedSessionId <= 0) { - // Create session here to be able to show error. - try (final AssetFileDescriptor afd = - mContext.getContentResolver().openAssetFileDescriptor(uri, "r")) { - ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null; - PackageInstaller.SessionParams params = - createSessionParams(mIntent, pfd, uri.toString()); - mStagedSessionId = mPackageInstaller.createSession(params); - } catch (IOException e) { - Log.w(TAG, "Failed to create a staging session", e); - mStagingResult.setValue( - new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, - PackageManager.INSTALL_FAILED_INVALID_APK)) - .setActivityResultCode(Activity.RESULT_FIRST_USER) - .build()); - return; - } - } - - SessionStageListener listener = new SessionStageListener() { - @Override - public void onStagingSuccess(SessionInfo info) { - //TODO: Verify if the returned sessionInfo should be used anywhere - mStagingResult.setValue(new InstallReady()); - } - - @Override - public void onStagingFailure() { - cleanupStagingSession(); - mStagingResult.setValue( - new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, - PackageManager.INSTALL_FAILED_INVALID_APK)) - .setActivityResultCode(Activity.RESULT_FIRST_USER) - .build()); - } - }; - if (mSessionStager != null) { - mSessionStager.cancel(true); - } - mSessionStager = new SessionStager(mContext, uri, mStagedSessionId, listener); - mSessionStager.execute(); - } - } - - public int getStagedSessionId() { - return mStagedSessionId; - } - - private void cleanupStagingSession() { - if (mStagedSessionId > 0) { - try { - mPackageInstaller.abandonSession(mStagedSessionId); - } catch (SecurityException ignored) { - } - mStagedSessionId = 0; - } - } - - private PackageInstaller.SessionParams createSessionParams(@NonNull Intent intent, - @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) { - PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL); - final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri.class); - params.setPackageSource( - referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE - : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE); - params.setInstallAsInstantApp(false); - params.setReferrerUri(referrerUri); - params.setOriginatingUri( - intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri.class)); - params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, - Process.INVALID_UID)); - params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)); - params.setInstallReason(PackageManager.INSTALL_REASON_USER); - // Disable full screen intent usage by for sideloads. - params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT, - PackageInstaller.SessionParams.PERMISSION_STATE_DENIED); - - if (pfd != null) { - try { - final PackageInstaller.InstallInfo result = mPackageInstaller.readInstallInfo(pfd, - debugPathName, 0); - params.setAppPackageName(result.getPackageName()); - params.setInstallLocation(result.getInstallLocation()); - params.setSize(result.calculateInstalledSize(params, pfd)); - } catch (PackageInstaller.PackageParsingException e) { - Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e); - params.setSize(pfd.getStatSize()); - } catch (IOException e) { - Log.e(TAG, - "Cannot calculate installed size " + debugPathName - + ". Try only apk size.", e); - } - } else { - Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults."); - } - return params; - } - - /** - * Processes Install session, file:// or package:// URI to generate data pertaining to user - * confirmation for an install. This method also checks if the source app has the AppOp granted - * to install unknown apps. If an AppOp is to be requested, cache the user action prompt data to - * be reused once appOp has been granted - * - * @return <ul> - * <li>InstallAborted </li> - * <ul> - * <li> If install session is invalid (not sealed or resolvedBaseApk path - * is invalid) </li> - * <li> Source app doesn't have visibility to target app </li> - * <li> The APK is invalid </li> - * <li> URI is invalid </li> - * <li> Can't get ApplicationInfo for source app, to request AppOp </li> - * </ul> - * <li> InstallUserActionRequired</li> - * <ul> - * <li> If AppOP is granted and user action is required to proceed - * with install </li> - * <li> If AppOp grant is to be requested from the user</li> - * </ul> - * </ul> - */ - public InstallStage requestUserConfirmation() { - if (mIsTrustedSource) { - if (mLocalLOGV) { - Log.i(TAG, "install allowed"); - } - // Returns InstallUserActionRequired stage if install details could be successfully - // computed, else it returns InstallAborted. - return generateConfirmationSnippet(); - } else { - InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo); - if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) { - // Source app already has appOp granted. - return generateConfirmationSnippet(); - } else { - return unknownSourceStage; - } - } - } - - - private InstallStage generateConfirmationSnippet() { - final Object packageSource; - int pendingUserActionReason = -1; - if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(mIntent.getAction())) { - final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId); - String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null; - - if (info == null || !info.isSealed() || resolvedPath == null) { - Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring"); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - packageSource = Uri.fromFile(new File(resolvedPath)); - // TODO: Not sure where is this used yet. PIA.java passes it to - // InstallInstalling if not null - // mOriginatingURI = null; - // mReferrerURI = null; - pendingUserActionReason = info.getPendingUserActionReason(); - } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(mIntent.getAction())) { - final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId); - - if (info == null || !info.isPreApprovalRequested()) { - Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring"); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - packageSource = info; - // mOriginatingURI = null; - // mReferrerURI = null; - pendingUserActionReason = info.getPendingUserActionReason(); - } else { - // Two possible origins: - // 1. Installation with SCHEME_PACKAGE. - // 2. Installation with "file://" for session created by this app - if (mIntent.getData() != null && mIntent.getData().getScheme().equals(SCHEME_PACKAGE)) { - packageSource = mIntent.getData(); - } else { - SessionInfo stagedSessionInfo = mPackageInstaller.getSessionInfo(mStagedSessionId); - packageSource = Uri.fromFile(new File(stagedSessionInfo.getResolvedBaseApkPath())); - } - // mOriginatingURI = mIntent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); - // mReferrerURI = mIntent.getParcelableExtra(Intent.EXTRA_REFERRER); - pendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; - } - - // if there's nothing to do, quietly slip into the ether - if (packageSource == null) { - Log.w(TAG, "Unspecified source"); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, - PackageManager.INSTALL_FAILED_INVALID_URI)) - .setActivityResultCode(Activity.RESULT_FIRST_USER) - .build(); - } - - return processAppSnippet(packageSource, pendingUserActionReason); - } - - /** - * 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 InstallStage processAppSnippet(Object source, int userActionReason) { - if (source instanceof Uri) { - return processPackageUri((Uri) source, userActionReason); - } else if (source instanceof SessionInfo) { - return processSessionInfo((SessionInfo) source, userActionReason); - } - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - - /** - * 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 InstallStage processPackageUri(final Uri packageUri, int userActionReason) { - final String scheme = packageUri.getScheme(); - final String packageName = packageUri.getSchemeSpecificPart(); - - if (scheme == null) { - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - - if (mLocalLOGV) { - Log.i(TAG, "processPackageUri(): uri = " + packageUri + ", scheme = " + scheme); - } - - switch (scheme) { - case SCHEME_PACKAGE -> { - for (UserHandle handle : mUserManager.getUserHandles(true)) { - PackageManager pmForUser = mContext.createContextAsUser(handle, 0) - .getPackageManager(); - try { - if (pmForUser.canPackageQuery(mCallingPackage, packageName)) { - mNewPackageInfo = pmForUser.getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS - | PackageManager.MATCH_UNINSTALLED_PACKAGES); - } - } catch (NameNotFoundException ignored) { - } - } - if (mNewPackageInfo == null) { - Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart() - + " not available. Discontinuing installation"); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .setErrorDialogType(DLG_PACKAGE_ERROR) - .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, - PackageManager.INSTALL_FAILED_INVALID_APK)) - .setActivityResultCode(Activity.RESULT_FIRST_USER) - .build(); - } - mAppSnippet = getAppSnippet(mContext, mNewPackageInfo); - if (mLocalLOGV) { - Log.i(TAG, "Created snippet for " + mAppSnippet.getLabel()); - } - } - case ContentResolver.SCHEME_FILE -> { - File sourceFile = new File(packageUri.getPath()); - mNewPackageInfo = getPackageInfo(mContext, sourceFile, - PackageManager.GET_PERMISSIONS); - - // Check for parse errors - if (mNewPackageInfo == null) { - Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .setErrorDialogType(DLG_PACKAGE_ERROR) - .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, - PackageManager.INSTALL_FAILED_INVALID_APK)) - .setActivityResultCode(Activity.RESULT_FIRST_USER) - .build(); - } - if (mLocalLOGV) { - Log.i(TAG, "Creating snippet for local file " + sourceFile); - } - mAppSnippet = getAppSnippet(mContext, mNewPackageInfo.applicationInfo, sourceFile); - } - default -> { - Log.e(TAG, "Unexpected URI scheme " + packageUri); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - } - - return new InstallUserActionRequired.Builder( - USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet) - .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason)) - .setAppUpdating(isAppUpdating(mNewPackageInfo)) - .build(); - } - - /** - * Use the SessionInfo and set up the installer for pre-commit install session. - * - * @param sessionInfo The SessionInfo to compose - * @return {@code true} iff the installer could be set up - */ - private InstallStage processSessionInfo(@NonNull SessionInfo sessionInfo, - int userActionReason) { - mNewPackageInfo = generateStubPackageInfo(sessionInfo.getAppPackageName()); - - mAppSnippet = getAppSnippet(mContext, sessionInfo); - return new InstallUserActionRequired.Builder( - USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet) - .setAppUpdating(isAppUpdating(mNewPackageInfo)) - .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason)) - .build(); - } - - private String getUpdateMessage(PackageInfo pkgInfo, int userActionReason) { - if (isAppUpdating(pkgInfo)) { - final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo); - final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage); - - if (!TextUtils.isEmpty(existingUpdateOwnerLabel) - && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) { - return mContext.getString(R.string.install_confirm_question_update_owner_reminder, - requestedUpdateOwnerLabel, existingUpdateOwnerLabel); - } - } - return null; - } - - private CharSequence getExistingUpdateOwnerLabel(PackageInfo pkgInfo) { - try { - final String packageName = pkgInfo.packageName; - final InstallSourceInfo sourceInfo = mPackageManager.getInstallSourceInfo(packageName); - final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName(); - return getApplicationLabel(existingUpdateOwner); - } catch (NameNotFoundException e) { - return null; - } - } - - private CharSequence getApplicationLabel(String packageName) { - try { - final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, - ApplicationInfoFlags.of(0)); - return mPackageManager.getApplicationLabel(appInfo); - } catch (NameNotFoundException e) { - return null; - } - } - - private boolean isAppUpdating(PackageInfo newPkgInfo) { - String pkgName = newPkgInfo.packageName; - // Check if there is already a package on the device with this name - // but it has been renamed to something else. - String[] oldName = mPackageManager.canonicalToCurrentPackageNames(new String[]{pkgName}); - if (oldName != null && oldName.length > 0 && oldName[0] != null) { - pkgName = oldName[0]; - newPkgInfo.packageName = pkgName; - newPkgInfo.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". - ApplicationInfo appInfo = mPackageManager.getApplicationInfo(pkgName, - PackageManager.MATCH_UNINSTALLED_PACKAGES); - if ((appInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { - return false; - } - } catch (NameNotFoundException e) { - return false; - } - return true; - } - - /** - * Once the user returns from Settings related to installing from unknown sources, reattempt - * the installation if the source app is granted permission to install other apps. Abort the - * installation if the source app is still not granted installing permission. - * @return {@link InstallUserActionRequired} containing data required to ask user confirmation - * to proceed with the install. - * {@link InstallAborted} if there was an error while recomputing, or the source still - * doesn't have install permission. - */ - public InstallStage reattemptInstall() { - InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo); - if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) { - // Source app now has appOp granted. - return generateConfirmationSnippet(); - } else if (unknownSourceStage.getStageCode() == InstallStage.STAGE_ABORTED) { - // There was some error in determining the AppOp code for the source app. - // Abort installation - return unknownSourceStage; - } else { - // AppOpsManager again returned a MODE_ERRORED or MODE_DEFAULT op code. This was - // unexpected while reattempting the install. Let's abort it. - Log.e(TAG, "AppOp still not granted."); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - } - - private InstallStage handleUnknownSources(AppOpRequestInfo requestInfo) { - if (requestInfo.getCallingPackage() == null) { - Log.i(TAG, "No source found for package " + mNewPackageInfo.packageName); - return new InstallUserActionRequired.Builder( - USER_ACTION_REASON_ANONYMOUS_SOURCE, null) - .build(); - } - // 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, - requestInfo.getOriginatingUid(), - requestInfo.getCallingPackage(), requestInfo.getAttributionTag(), - "Started package installation activity"); - - if (mLocalLOGV) { - Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode); - } - switch (appOpMode) { - case AppOpsManager.MODE_DEFAULT: - mAppOpsManager.setMode(appOpStr, requestInfo.getOriginatingUid(), - requestInfo.getCallingPackage(), AppOpsManager.MODE_ERRORED); - // fall through - case AppOpsManager.MODE_ERRORED: - try { - ApplicationInfo sourceInfo = - mPackageManager.getApplicationInfo(requestInfo.getCallingPackage(), 0); - AppSnippet sourceAppSnippet = getAppSnippet(mContext, sourceInfo); - return new InstallUserActionRequired.Builder( - USER_ACTION_REASON_UNKNOWN_SOURCE, sourceAppSnippet) - .setDialogMessage(requestInfo.getCallingPackage()) - .build(); - } catch (NameNotFoundException e) { - Log.e(TAG, "Did not find appInfo for " + requestInfo.getCallingPackage()); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - case AppOpsManager.MODE_ALLOWED: - return new InstallReady(); - default: - Log.e(TAG, "Invalid app op mode " + appOpMode - + " for OP_REQUEST_INSTALL_PACKAGES found for uid " - + requestInfo.getOriginatingUid()); - return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build(); - } - } - - - /** - * Kick off the installation. Register a broadcast listener to get the result of the - * installation and commit the staged session here. If the installation was session based, - * signal the PackageInstaller that the user has granted permission to proceed with the install - */ - public void initiateInstall() { - if (mSessionId > 0) { - mPackageInstaller.setPermissionsResult(mSessionId, true); - mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_DONE) - .setActivityResultCode(Activity.RESULT_OK).build()); - return; - } - - Uri uri = mIntent.getData(); - if (uri != null && SCHEME_PACKAGE.equals(uri.getScheme())) { - try { - mPackageManager.installExistingPackage(mNewPackageInfo.packageName); - setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null, -1); - } catch (PackageManager.NameNotFoundException e) { - setStageBasedOnResult(PackageInstaller.STATUS_FAILURE, - PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1); - } - return; - } - - if (mStagedSessionId <= 0) { - // How did we even land here? - Log.e(TAG, "Invalid local session and caller initiated session"); - mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR) - .build()); - return; - } - - int installId; - try { - mInstallResult.setValue(new InstallInstalling(mAppSnippet)); - installId = InstallEventReceiver.addObserver(mContext, - EventResultPersister.GENERATE_NEW_ID, this::setStageBasedOnResult); - } catch (EventResultPersister.OutOfIdsException e) { - setStageBasedOnResult(PackageInstaller.STATUS_FAILURE, - PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1); - return; - } - - Intent broadcastIntent = new Intent(BROADCAST_ACTION); - broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - broadcastIntent.setPackage(mContext.getPackageName()); - broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, installId); - - PendingIntent pendingIntent = PendingIntent.getBroadcast( - mContext, installId, broadcastIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); - - try { - PackageInstaller.Session session = mPackageInstaller.openSession(mStagedSessionId); - session.commit(pendingIntent.getIntentSender()); - } catch (Exception e) { - Log.e(TAG, "Session " + mStagedSessionId + " could not be opened.", e); - mPackageInstaller.abandonSession(mStagedSessionId); - setStageBasedOnResult(PackageInstaller.STATUS_FAILURE, - PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1); - } - } - - private void setStageBasedOnResult(int statusCode, int legacyStatus, String message, - int serviceId) { - if (statusCode == PackageInstaller.STATUS_SUCCESS) { - boolean shouldReturnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); - - InstallSuccess.Builder successBuilder = new InstallSuccess.Builder(mAppSnippet) - .setShouldReturnResult(shouldReturnResult); - Intent resultIntent; - if (shouldReturnResult) { - resultIntent = new Intent() - .putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED); - } else { - resultIntent = mPackageManager - .getLaunchIntentForPackage(mNewPackageInfo.packageName); - } - successBuilder.setResultIntent(resultIntent); - - mInstallResult.setValue(successBuilder.build()); - } else { - mInstallResult.setValue( - new InstallFailed(mAppSnippet, statusCode, legacyStatus, message)); - } - } - - public MutableLiveData<InstallStage> getInstallResult() { - return mInstallResult; - } - - /** - * Cleanup the staged session. Also signal the packageinstaller that an install session is to - * be aborted - */ - public void cleanupInstall() { - if (mSessionId > 0) { - mPackageInstaller.setPermissionsResult(mSessionId, false); - } else if (mStagedSessionId > 0) { - cleanupStagingSession(); - } - } - - /** - * When the identity of the install source could not be determined, user can skip checking the - * source and directly proceed with the install. - */ - public InstallStage forcedSkipSourceCheck() { - return generateConfirmationSnippet(); - } - - public MutableLiveData<Integer> getStagingProgress() { - if (mSessionStager != null) { - return mSessionStager.getProgress(); - } - return new MutableLiveData<>(0); - } - - public MutableLiveData<InstallStage> getStagingResult() { - return mStagingResult; - } - - public interface SessionStageListener { - - void onStagingSuccess(SessionInfo info); - - void onStagingFailure(); - } - - public static class CallerInfo { - - private final String mPackageName; - private final int mUid; - - public CallerInfo(String packageName, int uid) { - mPackageName = packageName; - mUid = uid; - } - - public String getPackageName() { - return mPackageName; - } - - public int getUid() { - return mUid; - } - } - - public static class AppOpRequestInfo { - - private String mCallingPackage; - private String mAttributionTag; - private int mOrginatingUid; - - public AppOpRequestInfo(String callingPackage, int orginatingUid, String attributionTag) { - mCallingPackage = callingPackage; - mOrginatingUid = orginatingUid; - mAttributionTag = attributionTag; - } - - public String getCallingPackage() { - return mCallingPackage; - } - - public String getAttributionTag() { - return mAttributionTag; - } - - public int getOriginatingUid() { - return mOrginatingUid; - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt new file mode 100644 index 000000000000..326e533df0d8 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -0,0 +1,867 @@ +/* + * Copyright (C) 2023 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.v2.model + +import android.Manifest +import android.app.Activity +import android.app.AppOpsManager +import android.app.PendingIntent +import android.app.admin.DevicePolicyManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionInfo +import android.content.pm.PackageInstaller.SessionParams +import android.content.pm.PackageManager +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.UserManager +import android.text.TextUtils +import android.util.EventLog +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.android.packageinstaller.R +import com.android.packageinstaller.common.EventResultPersister +import com.android.packageinstaller.common.EventResultPersister.OutOfIdsException +import com.android.packageinstaller.common.InstallEventReceiver +import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_DONE +import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_INTERNAL_ERROR +import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_POLICY +import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_PACKAGE_ERROR +import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_ANONYMOUS_SOURCE +import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_INSTALL_CONFIRMATION +import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_UNKNOWN_SOURCE +import com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery +import com.android.packageinstaller.v2.model.PackageUtil.generateStubPackageInfo +import com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet +import com.android.packageinstaller.v2.model.PackageUtil.getPackageInfo +import com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid +import com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner +import com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested +import com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted +import java.io.File +import java.io.IOException +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class InstallRepository(private val context: Context) { + + private val packageManager: PackageManager = context.packageManager + private val packageInstaller: PackageInstaller = packageManager.packageInstaller + private val userManager: UserManager? = context.getSystemService(UserManager::class.java) + private val devicePolicyManager: DevicePolicyManager? = + context.getSystemService(DevicePolicyManager::class.java) + private val appOpsManager: AppOpsManager? = context.getSystemService(AppOpsManager::class.java) + private val localLOGV = false + private var isSessionInstall = false + private var isTrustedSource = false + private val _stagingResult = MutableLiveData<InstallStage>() + val stagingResult: LiveData<InstallStage> + get() = _stagingResult + private val _installResult = MutableLiveData<InstallStage>() + val installResult: LiveData<InstallStage> + get() = _installResult + + /** + * Session ID for a session created when caller uses PackageInstaller APIs + */ + private var sessionId = SessionInfo.INVALID_ID + + /** + * Session ID for a session created by this app + */ + var stagedSessionId = SessionInfo.INVALID_ID + private set + private var callingUid = Process.INVALID_UID + private var callingPackage: String? = null + private var sessionStager: SessionStager? = null + private lateinit var intent: Intent + private lateinit var appOpRequestInfo: AppOpRequestInfo + private lateinit var appSnippet: PackageUtil.AppSnippet + + /** + * PackageInfo of the app being installed on device. + */ + private var newPackageInfo: PackageInfo? = null + + /** + * Extracts information from the incoming install intent, checks caller's permission to install + * packages, verifies that the caller is the install session owner (in case of a session based + * install) and checks if the current user has restrictions set that prevent app installation, + * + * @param intent the incoming [Intent] object for installing a package + * @param callerInfo [CallerInfo] that holds the callingUid and callingPackageName + * @return + * * [InstallAborted] if there are errors while performing the checks + * * [InstallStaging] after successfully performing the checks + */ + fun performPreInstallChecks(intent: Intent, callerInfo: CallerInfo): InstallStage { + this.intent = intent + + var callingAttributionTag: String? = null + + isSessionInstall = + PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL == intent.action + || PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action + + sessionId = if (isSessionInstall) + intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID) + else SessionInfo.INVALID_ID + + stagedSessionId = intent.getIntExtra(EXTRA_STAGED_SESSION_ID, SessionInfo.INVALID_ID) + + callingPackage = callerInfo.packageName + + if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) { + val sessionInfo: SessionInfo? = packageInstaller.getSessionInfo(sessionId) + callingPackage = sessionInfo?.getInstallerPackageName() + callingAttributionTag = sessionInfo?.getInstallerAttributionTag() + } + + // Uid of the source package, coming from ActivityManager + callingUid = callerInfo.uid + if (callingUid == Process.INVALID_UID) { + Log.e(LOG_TAG, "Could not determine the launching uid.") + } + val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) + // Uid of the source package, with a preference to uid from ApplicationInfo + val originatingUid = sourceInfo?.uid ?: callingUid + appOpRequestInfo = AppOpRequestInfo( + getPackageNameForUid(context, originatingUid, callingPackage), + originatingUid, callingAttributionTag + ) + + if (callingUid == Process.INVALID_UID && sourceInfo == null) { + // Caller's identity could not be determined. Abort the install + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + + if ((sessionId != SessionInfo.INVALID_ID + && !isCallerSessionOwner(packageInstaller, originatingUid, sessionId)) + || (stagedSessionId != SessionInfo.INVALID_ID + && !isCallerSessionOwner(packageInstaller, Process.myUid(), stagedSessionId)) + ) { + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + + isTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, this.intent, originatingUid) + if (!isInstallPermissionGrantedOrRequested( + context, callingUid, originatingUid, isTrustedSource + ) + ) { + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + + val restriction = getDevicePolicyRestrictions() + if (restriction != null) { + val adminSupportDetailsIntent = + devicePolicyManager!!.createAdminSupportIntent(restriction) + return InstallAborted( + ABORT_REASON_POLICY, message = restriction, resultIntent = adminSupportDetailsIntent + ) + } + + maybeRemoveInvalidInstallerPackageName(callerInfo) + + return InstallStaging() + } + + /** + * @return the ApplicationInfo for the installation source (the calling package), if available + */ + private fun getSourceInfo(callingPackage: String?): ApplicationInfo? { + return try { + callingPackage?.let { packageManager.getApplicationInfo(it, 0) } + } catch (ignored: PackageManager.NameNotFoundException) { + null + } + } + + private fun isInstallRequestFromTrustedSource( + sourceInfo: ApplicationInfo?, + intent: Intent, + originatingUid: Int, + ): Boolean { + val isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) + return (sourceInfo != null && sourceInfo.isPrivilegedApp + && (isNotUnknownSource + || isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, originatingUid))) + } + + private fun getDevicePolicyRestrictions(): String? { + val restrictions = arrayOf( + UserManager.DISALLOW_INSTALL_APPS, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY + ) + for (restriction in restrictions) { + if (!userManager!!.hasUserRestrictionForUser(restriction, Process.myUserHandle())) { + continue + } + return restriction + } + return null + } + + private fun maybeRemoveInvalidInstallerPackageName(callerInfo: CallerInfo) { + val installerPackageNameFromIntent = + intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME) ?: return + + if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.packageName) + && callerInfo.packageName != null + && isPermissionGranted( + packageManager, Manifest.permission.INSTALL_PACKAGES, callerInfo.packageName + ) + ) { + Log.e( + LOG_TAG, "The given installer package name $installerPackageNameFromIntent" + + " is invalid. Remove it." + ) + EventLog.writeEvent( + 0x534e4554, "236687884", callerInfo.uid, + "Invalid EXTRA_INSTALLER_PACKAGE_NAME" + ) + intent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME) + } + } + + @OptIn(DelicateCoroutinesApi::class) + fun stageForInstall() { + val uri = intent.data + if (stagedSessionId != SessionInfo.INVALID_ID + || isSessionInstall + || (uri != null && SCHEME_PACKAGE == uri.scheme) + ) { + // For a session based install or installing with a package:// URI, there is no file + // for us to stage. + _stagingResult.value = InstallReady() + return + } + if (uri != null + && ContentResolver.SCHEME_CONTENT == uri.scheme + && canPackageQuery(context, callingUid, uri) + ) { + if (stagedSessionId > 0) { + val info: SessionInfo? = packageInstaller.getSessionInfo(stagedSessionId) + if (info == null || !info.isActive || info.resolvedBaseApkPath == null) { + Log.w(LOG_TAG, "Session $stagedSessionId in funky state; ignoring") + if (info != null) { + cleanupStagingSession() + } + stagedSessionId = 0 + } + } + + // Session does not exist, or became invalid. + if (stagedSessionId <= 0) { + // Create session here to be able to show error. + try { + context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd -> + val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor + val params: SessionParams = + createSessionParams(intent, pfd, uri.toString()) + stagedSessionId = packageInstaller.createSession(params) + } + } catch (e: IOException) { + Log.w(LOG_TAG, "Failed to create a staging session", e) + _stagingResult.value = InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) + return + } + } + + sessionStager = SessionStager(context, uri, stagedSessionId) + GlobalScope.launch(Dispatchers.Main) { + val wasFileStaged = sessionStager!!.execute() + + if (wasFileStaged) { + _stagingResult.value = InstallReady() + } else { + cleanupStagingSession() + _stagingResult.value = InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) + } + } + } + } + + private fun cleanupStagingSession() { + if (stagedSessionId > 0) { + try { + packageInstaller.abandonSession(stagedSessionId) + } catch (ignored: SecurityException) { + } + stagedSessionId = 0 + } + } + + private fun createSessionParams( + intent: Intent, + pfd: ParcelFileDescriptor?, + debugPathName: String, + ): SessionParams { + val params = SessionParams(SessionParams.MODE_FULL_INSTALL) + val referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java) + params.setPackageSource( + if (referrerUri != null) + PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE + else PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE + ) + params.setInstallAsInstantApp(false) + params.setReferrerUri(referrerUri) + params.setOriginatingUri( + intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java) + ) + params.setOriginatingUid( + intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID) + ) + params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)) + params.setInstallReason(PackageManager.INSTALL_REASON_USER) + // Disable full screen intent usage by for sideloads. + params.setPermissionState( + Manifest.permission.USE_FULL_SCREEN_INTENT, SessionParams.PERMISSION_STATE_DENIED + ) + if (pfd != null) { + try { + val installInfo = packageInstaller.readInstallInfo(pfd, debugPathName, 0) + params.setAppPackageName(installInfo.packageName) + params.setInstallLocation(installInfo.installLocation) + params.setSize(installInfo.calculateInstalledSize(params, pfd)) + } catch (e: PackageInstaller.PackageParsingException) { + Log.e(LOG_TAG, "Cannot parse package $debugPathName. Assuming defaults.", e) + params.setSize(pfd.statSize) + } catch (e: IOException) { + Log.e(LOG_TAG, "Cannot calculate installed size $debugPathName. " + + "Try only apk size.", e + ) + } + } else { + Log.e(LOG_TAG, "Cannot parse package $debugPathName. Assuming defaults.") + } + return params + } + + /** + * Processes Install session, file:// or package:// URI to generate data pertaining to user + * confirmation for an install. This method also checks if the source app has the AppOp granted + * to install unknown apps. If an AppOp is to be requested, cache the user action prompt data to + * be reused once appOp has been granted + * + * @return + * * [InstallAborted] + * * If install session is invalid (not sealed or resolvedBaseApk path is invalid) + * * Source app doesn't have visibility to target app + * * The APK is invalid + * * URI is invalid + * * Can't get ApplicationInfo for source app, to request AppOp + * + * * [InstallUserActionRequired] + * * If AppOP is granted and user action is required to proceed with install + * * If AppOp grant is to be requested from the user + */ + fun requestUserConfirmation(): InstallStage { + return if (isTrustedSource) { + if (localLOGV) { + Log.i(LOG_TAG, "install allowed") + } + // Returns InstallUserActionRequired stage if install details could be successfully + // computed, else it returns InstallAborted. + generateConfirmationSnippet() + } else { + val unknownSourceStage = handleUnknownSources(appOpRequestInfo) + if (unknownSourceStage.stageCode == InstallStage.STAGE_READY) { + // Source app already has appOp granted. + generateConfirmationSnippet() + } else { + unknownSourceStage + } + } + } + + private fun generateConfirmationSnippet(): InstallStage { + val packageSource: Any? + val pendingUserActionReason: Int + + if (PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action) { + val info = packageInstaller.getSessionInfo(sessionId) + val resolvedPath = info?.resolvedBaseApkPath + if (info == null || !info.isSealed || resolvedPath == null) { + Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring") + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + packageSource = Uri.fromFile(File(resolvedPath)) + // TODO: Not sure where is this used yet. PIA.java passes it to + // InstallInstalling if not null + // mOriginatingURI = null; + // mReferrerURI = null; + pendingUserActionReason = info.getPendingUserActionReason() + } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL == intent.action) { + val info = packageInstaller.getSessionInfo(sessionId) + if (info == null || !info.isPreApprovalRequested) { + Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring") + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + packageSource = info + // mOriginatingURI = null; + // mReferrerURI = null; + pendingUserActionReason = info.getPendingUserActionReason() + } else { + // Two possible origins: + // 1. Installation with SCHEME_PACKAGE. + // 2. Installation with "file://" for session created by this app + packageSource = + if (intent.data?.scheme == SCHEME_PACKAGE) { + intent.data + } else { + val stagedSessionInfo = packageInstaller.getSessionInfo(stagedSessionId) + Uri.fromFile(File(stagedSessionInfo?.resolvedBaseApkPath!!)) + } + // mOriginatingURI = mIntent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); + // mReferrerURI = mIntent.getParcelableExtra(Intent.EXTRA_REFERRER); + pendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE + } + + // if there's nothing to do, quietly slip into the ether + if (packageSource == null) { + Log.w(LOG_TAG, "Unspecified source") + return InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, + PackageManager.INSTALL_FAILED_INVALID_URI + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) + } + return processAppSnippet(packageSource, pendingUserActionReason) + } + + /** + * 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 + * * [InstallUserActionRequired] if source could be processed + * * [InstallAborted] if source is invalid or there was an error is processing a source + */ + private fun processAppSnippet(source: Any, userActionReason: Int): InstallStage { + return when (source) { + is Uri -> processPackageUri(source, userActionReason) + is SessionInfo -> processSessionInfo(source, userActionReason) + else -> InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + } + + /** + * Parse the Uri and set up the installer for this package. + * + * @param packageUri The URI to parse + * @return + * * [InstallUserActionRequired] if source could be processed + * * [InstallAborted] if source is invalid or there was an error is processing a source + */ + private fun processPackageUri(packageUri: Uri, userActionReason: Int): InstallStage { + val scheme = packageUri.scheme + val packageName = packageUri.schemeSpecificPart + if (scheme == null) { + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + if (localLOGV) { + Log.i(LOG_TAG, "processPackageUri(): uri = $packageUri, scheme = $scheme") + } + when (scheme) { + SCHEME_PACKAGE -> { + for (handle in userManager!!.getUserHandles(true)) { + val pmForUser = context.createContextAsUser(handle, 0).packageManager + try { + if (pmForUser.canPackageQuery(callingPackage!!, packageName)) { + newPackageInfo = pmForUser.getPackageInfo( + packageName, + PackageManager.GET_PERMISSIONS + or PackageManager.MATCH_UNINSTALLED_PACKAGES + ) + } + } catch (ignored: PackageManager.NameNotFoundException) { + } + } + if (newPackageInfo == null) { + Log.w( + LOG_TAG, "Requested package " + packageUri.schemeSpecificPart + + " not available. Discontinuing installation" + ) + return InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + errorDialogType = DLG_PACKAGE_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) + } + appSnippet = getAppSnippet(context, newPackageInfo!!) + if (localLOGV) { + Log.i(LOG_TAG, "Created snippet for " + appSnippet.label) + } + } + + ContentResolver.SCHEME_FILE -> { + val sourceFile = packageUri.path?.let { File(it) } + newPackageInfo = sourceFile?.let { + getPackageInfo(context, it, PackageManager.GET_PERMISSIONS) + } + + // Check for parse errors + if (newPackageInfo == null) { + Log.w( + LOG_TAG, "Parse error when parsing manifest. " + + "Discontinuing installation" + ) + return InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + errorDialogType = DLG_PACKAGE_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, + PackageManager.INSTALL_FAILED_INVALID_APK + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) + } + if (localLOGV) { + Log.i(LOG_TAG, "Creating snippet for local file $sourceFile") + } + appSnippet = getAppSnippet(context, newPackageInfo!!, sourceFile!!) + } + + else -> { + Log.e(LOG_TAG, "Unexpected URI scheme $packageUri") + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + } + return InstallUserActionRequired( + USER_ACTION_REASON_INSTALL_CONFIRMATION, appSnippet, isAppUpdating(newPackageInfo!!), + getUpdateMessage(newPackageInfo!!, userActionReason) + ) + } + + /** + * Use the SessionInfo and set up the installer for pre-commit install session. + * + * @param sessionInfo The SessionInfo to compose + * @return + * * [InstallUserActionRequired] if source could be processed + * * [InstallAborted] if source is invalid or there was an error is processing a source + */ + private fun processSessionInfo(sessionInfo: SessionInfo, userActionReason: Int): InstallStage { + newPackageInfo = generateStubPackageInfo(sessionInfo.getAppPackageName()) + appSnippet = getAppSnippet(context, sessionInfo) + + return InstallUserActionRequired( + USER_ACTION_REASON_INSTALL_CONFIRMATION, appSnippet, isAppUpdating(newPackageInfo!!), + getUpdateMessage(newPackageInfo!!, userActionReason) + + ) + } + + private fun getUpdateMessage(pkgInfo: PackageInfo, userActionReason: Int): String? { + if (isAppUpdating(pkgInfo)) { + val existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo) + val requestedUpdateOwnerLabel = getApplicationLabel(callingPackage) + if (!TextUtils.isEmpty(existingUpdateOwnerLabel) + && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP + ) { + return context.getString( + R.string.install_confirm_question_update_owner_reminder, + requestedUpdateOwnerLabel, existingUpdateOwnerLabel + ) + } + } + return null + } + + private fun getExistingUpdateOwnerLabel(pkgInfo: PackageInfo): CharSequence? { + return try { + val packageName = pkgInfo.packageName + val sourceInfo = packageManager.getInstallSourceInfo(packageName) + val existingUpdateOwner = sourceInfo.updateOwnerPackageName + getApplicationLabel(existingUpdateOwner) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + private fun getApplicationLabel(packageName: String?): CharSequence? { + return try { + val appInfo = packageName?.let { + packageManager.getApplicationInfo( + it, PackageManager.ApplicationInfoFlags.of(0) + ) + } + appInfo?.let { packageManager.getApplicationLabel(it) } + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + private fun isAppUpdating(newPkgInfo: PackageInfo): Boolean { + var pkgName = newPkgInfo.packageName + // Check if there is already a package on the device with this name + // but it has been renamed to something else. + val oldName = packageManager.canonicalToCurrentPackageNames(arrayOf(pkgName)) + if (oldName != null && oldName.isNotEmpty() && oldName[0] != null) { + pkgName = oldName[0] + newPkgInfo.packageName = pkgName + newPkgInfo.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". + val appInfo = packageManager.getApplicationInfo( + pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES + ) + if (appInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0) { + return false + } + } catch (e: PackageManager.NameNotFoundException) { + return false + } + return true + } + + /** + * Once the user returns from Settings related to installing from unknown sources, reattempt + * the installation if the source app is granted permission to install other apps. Abort the + * installation if the source app is still not granted installing permission. + * + * @return + * * [InstallUserActionRequired] containing data required to ask user confirmation + * to proceed with the install. + * * [InstallAborted] if there was an error while recomputing, or the source still + * doesn't have install permission. + */ + fun reattemptInstall(): InstallStage { + val unknownSourceStage = handleUnknownSources(appOpRequestInfo) + return when (unknownSourceStage.stageCode) { + InstallStage.STAGE_READY -> { + // Source app now has appOp granted. + generateConfirmationSnippet() + } + + InstallStage.STAGE_ABORTED -> { + // There was some error in determining the AppOp code for the source app. + // Abort installation + unknownSourceStage + } + + else -> { + // AppOpsManager again returned a MODE_ERRORED or MODE_DEFAULT op code. This was + // unexpected while reattempting the install. Let's abort it. + Log.e(LOG_TAG, "AppOp still not granted.") + InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + } + } + + private fun handleUnknownSources(requestInfo: AppOpRequestInfo): InstallStage { + if (requestInfo.callingPackage == null) { + Log.i(LOG_TAG, "No source found for package " + newPackageInfo?.packageName) + return InstallUserActionRequired(USER_ACTION_REASON_ANONYMOUS_SOURCE) + } + // Shouldn't use static constant directly, see b/65534401. + val appOpStr = AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES) + val appOpMode = appOpsManager!!.noteOpNoThrow( + appOpStr!!, requestInfo.originatingUid, requestInfo.callingPackage, + requestInfo.attributionTag, "Started package installation activity" + ) + if (localLOGV) { + Log.i(LOG_TAG, "handleUnknownSources(): appMode=$appOpMode") + } + + return when (appOpMode) { + AppOpsManager.MODE_DEFAULT, AppOpsManager.MODE_ERRORED -> { + if (appOpMode == AppOpsManager.MODE_DEFAULT) { + appOpsManager.setMode( + appOpStr, requestInfo.originatingUid, requestInfo.callingPackage, + AppOpsManager.MODE_ERRORED + ) + } + try { + val sourceInfo = + packageManager.getApplicationInfo(requestInfo.callingPackage, 0) + val sourceAppSnippet = getAppSnippet(context, sourceInfo) + InstallUserActionRequired( + USER_ACTION_REASON_UNKNOWN_SOURCE, appSnippet = sourceAppSnippet, + dialogMessage = requestInfo.callingPackage + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(LOG_TAG, "Did not find appInfo for " + requestInfo.callingPackage) + InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + } + + AppOpsManager.MODE_ALLOWED -> InstallReady() + + else -> { + Log.e( + LOG_TAG, "Invalid app op mode $appOpMode for " + + "OP_REQUEST_INSTALL_PACKAGES found for uid $requestInfo.originatingUid" + ) + InstallAborted(ABORT_REASON_INTERNAL_ERROR) + } + } + } + + /** + * Kick off the installation. Register a broadcast listener to get the result of the + * installation and commit the staged session here. If the installation was session based, + * signal the PackageInstaller that the user has granted permission to proceed with the install + */ + fun initiateInstall() { + if (sessionId > 0) { + packageInstaller.setPermissionsResult(sessionId, true) + _installResult.value = InstallAborted( + ABORT_REASON_DONE, activityResultCode = Activity.RESULT_OK + ) + return + } + val uri = intent.data + if (SCHEME_PACKAGE == uri?.scheme) { + try { + packageManager.installExistingPackage( + newPackageInfo!!.packageName, PackageManager.INSTALL_REASON_USER + ) + setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null) + } catch (e: PackageManager.NameNotFoundException) { + setStageBasedOnResult( + PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + null) + } + return + } + if (stagedSessionId <= 0) { + // How did we even land here? + Log.e(LOG_TAG, "Invalid local session and caller initiated session") + _installResult.value = InstallAborted(ABORT_REASON_INTERNAL_ERROR) + return + } + val installId: Int + try { + _installResult.value = InstallInstalling(appSnippet) + installId = InstallEventReceiver.addObserver( + context, EventResultPersister.GENERATE_NEW_ID + ) { statusCode: Int, legacyStatus: Int, message: String?, serviceId: Int -> + setStageBasedOnResult(statusCode, legacyStatus, message) + } + } catch (e: OutOfIdsException) { + setStageBasedOnResult( + PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null) + return + } + val broadcastIntent = Intent(BROADCAST_ACTION) + broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + broadcastIntent.setPackage(context.packageName) + broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, installId) + val pendingIntent = PendingIntent.getBroadcast( + context, installId, broadcastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + try { + val session = packageInstaller.openSession(stagedSessionId) + session.commit(pendingIntent.intentSender) + } catch (e: Exception) { + Log.e(LOG_TAG, "Session $stagedSessionId could not be opened.", e) + packageInstaller.abandonSession(stagedSessionId) + setStageBasedOnResult( + PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null) + } + } + + private fun setStageBasedOnResult( + statusCode: Int, + legacyStatus: Int, + message: String? + ) { + if (statusCode == PackageInstaller.STATUS_SUCCESS) { + val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) + val resultIntent = if (shouldReturnResult) { + Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED) + } else { + packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName) + } + _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) + } else { + _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message)) + } + } + + /** + * Cleanup the staged session. Also signal the packageinstaller that an install session is to + * be aborted + */ + fun cleanupInstall() { + if (sessionId > 0) { + packageInstaller.setPermissionsResult(sessionId, false) + } else if (stagedSessionId > 0) { + cleanupStagingSession() + } + } + + /** + * When the identity of the install source could not be determined, user can skip checking the + * source and directly proceed with the install. + */ + fun forcedSkipSourceCheck(): InstallStage { + return generateConfirmationSnippet() + } + + val stagingProgress: LiveData<Int> + get() = sessionStager?.progress ?: MutableLiveData(0) + + companion object { + const val EXTRA_STAGED_SESSION_ID = "com.android.packageinstaller.extra.STAGED_SESSION_ID" + const val SCHEME_PACKAGE = "package" + const val BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT" + private val LOG_TAG = InstallRepository::class.java.simpleName + } + + data class CallerInfo(val packageName: String?, val uid: Int) + data class AppOpRequestInfo( + val callingPackage: String?, + val originatingUid: Int, + val attributionTag: String?, + ) +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt new file mode 100644 index 000000000000..be49b39e9a48 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023 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 + * + * https://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.v2.model + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable + +sealed class InstallStage(val stageCode: Int) { + + companion object { + const val STAGE_DEFAULT = -1 + const val STAGE_ABORTED = 0 + const val STAGE_STAGING = 1 + const val STAGE_READY = 2 + const val STAGE_USER_ACTION_REQUIRED = 3 + const val STAGE_INSTALLING = 4 + const val STAGE_SUCCESS = 5 + const val STAGE_FAILED = 6 + } +} + +class InstallStaging : InstallStage(STAGE_STAGING) + +class InstallReady : InstallStage(STAGE_READY) + +data class InstallUserActionRequired( + val actionReason: Int, + private val appSnippet: PackageUtil.AppSnippet? = null, + val isAppUpdating: Boolean = false, + val dialogMessage: String? = null, +) : InstallStage(STAGE_USER_ACTION_REQUIRED) { + + val appIcon: Drawable? + get() = appSnippet?.icon + + val appLabel: String? + get() = appSnippet?.let { appSnippet.label as String? } + + companion object { + const val USER_ACTION_REASON_UNKNOWN_SOURCE = 0 + const val USER_ACTION_REASON_ANONYMOUS_SOURCE = 1 + const val USER_ACTION_REASON_INSTALL_CONFIRMATION = 2 + } +} + +data class InstallInstalling(private val appSnippet: PackageUtil.AppSnippet) : + InstallStage(STAGE_INSTALLING) { + + val appIcon: Drawable? + get() = appSnippet.icon + + val appLabel: String? + get() = appSnippet.label as String? +} + +data class InstallSuccess( + private val appSnippet: PackageUtil.AppSnippet, + val shouldReturnResult: Boolean = false, + /** + * + * * If the caller is requesting a result back, this will hold the Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent + * back to the caller. + * + * * If the caller doesn't want the result back, this will hold the Intent that launches + * the newly installed / updated app if a launchable activity exists. + */ + val resultIntent: Intent? = null, +) : InstallStage(STAGE_SUCCESS) { + + val appIcon: Drawable? + get() = appSnippet.icon + + val appLabel: String? + get() = appSnippet.label as String? +} + +data class InstallFailed( + private val appSnippet: PackageUtil.AppSnippet, + val legacyCode: Int, + val statusCode: Int, + val message: String?, +) : InstallStage(STAGE_FAILED) { + + val appIcon: Drawable? + get() = appSnippet.icon + + val appLabel: String? + get() = appSnippet.label as String? +} + +data class InstallAborted( + val abortReason: Int, + /** + * It will hold the restriction name, when the restriction was enforced by the system, and not + * a device admin. + */ + val message: String? = null, + /** + * * If abort reason is [ABORT_REASON_POLICY], then this will hold the Intent + * to display a support dialog when a feature was disabled by an admin. It will be + * `null` if the feature is disabled by the system. In this case, the restriction name + * will be set in [message] + * * If the abort reason is [ABORT_REASON_INTERNAL_ERROR], it **may** hold an + * intent to be sent as a result to the calling activity. + */ + val resultIntent: Intent? = null, + val activityResultCode: Int = Activity.RESULT_CANCELED, + val errorDialogType: Int? = 0, +) : InstallStage(STAGE_ABORTED) { + + companion object { + const val ABORT_REASON_INTERNAL_ERROR = 0 + const val ABORT_REASON_POLICY = 1 + const val ABORT_REASON_DONE = 2 + const val DLG_PACKAGE_ERROR = 1 + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java deleted file mode 100644 index fe05237bdc57..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model; - -import android.Manifest; -import android.content.Context; -import android.content.pm.ApplicationInfo; -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.ProviderInfo; -import android.content.res.Resources; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.Process; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; -import androidx.annotation.NonNull; -import java.io.File; -import java.util.Arrays; -import java.util.Objects; - -public class PackageUtil { - - private static final String TAG = InstallRepository.class.getSimpleName(); - private static final String DOWNLOADS_AUTHORITY = "downloads"; - private static final String SPLIT_BASE_APK_END_WITH = "base.apk"; - - /** - * Determines if the UID belongs to the system downloads provider and returns the - * {@link ApplicationInfo} of the provider - * - * @param uid UID of the caller - * @return {@link ApplicationInfo} of the provider if a downloads provider exists, it is a - * system app, and its UID matches with the passed UID, null otherwise. - */ - public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) { - final ProviderInfo providerInfo = pm.resolveContentProvider( - DOWNLOADS_AUTHORITY, 0); - if (providerInfo == null) { - // There seems to be no currently enabled downloads provider on the system. - return null; - } - ApplicationInfo appInfo = providerInfo.applicationInfo; - if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) { - return appInfo; - } - return null; - } - - /** - * Get the maximum target sdk for a UID. - * - * @param context The context to use - * @param uid The UID requesting the install/uninstall - * @return The maximum target SDK or -1 if the uid does not match any packages. - */ - public static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) { - PackageManager pm = context.getPackageManager(); - final String[] packages = pm.getPackagesForUid(uid); - int targetSdkVersion = -1; - if (packages != null) { - for (String packageName : packages) { - try { - ApplicationInfo info = pm.getApplicationInfo(packageName, 0); - targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion); - } catch (PackageManager.NameNotFoundException e) { - // Ignore and try the next package - } - } - } - return targetSdkVersion; - } - - public static boolean canPackageQuery(Context context, int callingUid, Uri packageUri) { - PackageManager pm = context.getPackageManager(); - ProviderInfo info = pm.resolveContentProvider(packageUri.getAuthority(), - PackageManager.ComponentInfoFlags.of(0)); - if (info == null) { - return false; - } - String targetPackage = info.packageName; - - String[] callingPackages = pm.getPackagesForUid(callingUid); - if (callingPackages == null) { - return false; - } - for (String callingPackage : callingPackages) { - try { - if (pm.canPackageQuery(callingPackage, targetPackage)) { - return true; - } - } catch (PackageManager.NameNotFoundException e) { - // no-op - } - } - return false; - } - - /** - * @param context the {@link Context} object - * @param permission the permission name to check - * @param callingUid the UID of the caller who's permission is being checked - * @return {@code true} if the callingUid is granted the said permission - */ - public static boolean isPermissionGranted(Context context, String permission, int callingUid) { - return context.checkPermission(permission, -1, callingUid) - == PackageManager.PERMISSION_GRANTED; - } - - /** - * @param pm the {@link PackageManager} object - * @param permission the permission name to check - * @param packageName the name of the package who's permission is being checked - * @return {@code true} if the package is granted the said permission - */ - public static boolean isPermissionGranted(PackageManager pm, String permission, - String packageName) { - return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED; - } - - /** - * @param context the {@link Context} object - * @param callingUid the UID of the caller who's permission is being checked - * @param originatingUid the UID from where install is being originated. This could be same as - * callingUid or it will be the UID of the package performing a session based install - * @param isTrustedSource whether install request is coming from a privileged app or an app that - * has {@link Manifest.permission.INSTALL_PACKAGES} permission granted - * @return {@code true} if the package is granted the said permission - */ - public static boolean isInstallPermissionGrantedOrRequested(Context context, int callingUid, - int originatingUid, boolean isTrustedSource) { - boolean isDocumentsManager = - isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid); - boolean isSystemDownloadsProvider = - getSystemDownloadsProviderInfo(context.getPackageManager(), callingUid) != null; - - if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) { - - final int targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid); - if (targetSdkVersion < 0) { - // Invalid originating uid supplied. Abort install. - Log.w(TAG, "Cannot get target sdk version for uid " + originatingUid); - return false; - } else if (targetSdkVersion >= Build.VERSION_CODES.O - && !isUidRequestingPermission(context.getPackageManager(), originatingUid, - Manifest.permission.REQUEST_INSTALL_PACKAGES)) { - Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission " - + Manifest.permission.REQUEST_INSTALL_PACKAGES); - return false; - } - } - return true; - } - - /** - * @param pm the {@link PackageManager} object - * @param uid the UID of the caller who's permission is being checked - * @param permission the permission name to check - * @return {@code true} if the caller is requesting the said permission in its Manifest - */ - public static boolean isUidRequestingPermission(PackageManager pm, int uid, String permission) { - final String[] packageNames = pm.getPackagesForUid(uid); - if (packageNames == null) { - return false; - } - for (final String packageName : packageNames) { - final PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS); - } catch (PackageManager.NameNotFoundException e) { - // Ignore and try the next package - continue; - } - if (packageInfo.requestedPermissions != null - && Arrays.asList(packageInfo.requestedPermissions).contains(permission)) { - return true; - } - } - return false; - } - - /** - * @param pi the {@link PackageInstaller} object to use - * @param originatingUid the UID of the package performing a session based install - * @param sessionId ID of the install session - * @return {@code true} if the caller is the session owner - */ - public static boolean isCallerSessionOwner(PackageInstaller pi, int originatingUid, - int sessionId) { - if (originatingUid == Process.ROOT_UID) { - return true; - } - PackageInstaller.SessionInfo sessionInfo = pi.getSessionInfo(sessionId); - if (sessionInfo == null) { - return false; - } - int installerUid = sessionInfo.getInstallerUid(); - return originatingUid == installerUid; - } - - /** - * Generates a stub {@link PackageInfo} object for the given packageName - */ - public 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; - } - - /** - * Generates an {@link AppSnippet} containing an appIcon and appLabel from the - * {@link SessionInfo} object - */ - public static AppSnippet getAppSnippet(Context context, SessionInfo info) { - PackageManager pm = context.getPackageManager(); - CharSequence label = info.getAppLabel(); - Drawable icon = info.getAppIcon() != null ? - new BitmapDrawable(context.getResources(), info.getAppIcon()) - : pm.getDefaultActivityIcon(); - return new AppSnippet(label, icon); - } - - /** - * Generates an {@link AppSnippet} containing an appIcon and appLabel from the - * {@link PackageInfo} object - */ - public static AppSnippet getAppSnippet(Context context, PackageInfo pkgInfo) { - return getAppSnippet(context, pkgInfo.applicationInfo); - } - - /** - * Generates an {@link AppSnippet} containing an appIcon and appLabel from the - * {@link ApplicationInfo} object - */ - public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo) { - PackageManager pm = context.getPackageManager(); - CharSequence label = pm.getApplicationLabel(appInfo); - Drawable icon = pm.getApplicationIcon(appInfo); - return new AppSnippet(label, icon); - } - - /** - * Generates an {@link AppSnippet} containing an appIcon and appLabel from the - * supplied APK file - */ - public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo, - File sourceFile) { - ApplicationInfo appInfoFromFile = processAppInfoForFile(appInfo, sourceFile); - CharSequence label = getAppLabelFromFile(context, appInfoFromFile); - Drawable icon = getAppIconFromFile(context, appInfoFromFile); - return new AppSnippet(label, icon); - } - - /** - * Utility method to load application label - * - * @param context context of package that can load the resources - * @param appInfo ApplicationInfo object of package whose resources are to be loaded - */ - public static CharSequence getAppLabelFromFile(Context context, ApplicationInfo appInfo) { - PackageManager pm = context.getPackageManager(); - CharSequence label = null; - // Try to load the label from the package's resources. If an app has not explicitly - // specified any label, just use the package name. - if (appInfo.labelRes != 0) { - try { - label = appInfo.loadLabel(pm); - } catch (Resources.NotFoundException e) { - } - } - if (label == null) { - label = (appInfo.nonLocalizedLabel != null) ? - appInfo.nonLocalizedLabel : appInfo.packageName; - } - return label; - } - - /** - * Utility method to load application icon - * - * @param context context of package that can load the resources - * @param appInfo ApplicationInfo object of package whose resources are to be loaded - */ - public static Drawable getAppIconFromFile(Context context, ApplicationInfo appInfo) { - PackageManager pm = context.getPackageManager(); - Drawable icon = null; - // Try to load the icon from the package's resources. If an app has not explicitly - // specified any resource, just use the default icon for now. - try { - if (appInfo.icon != 0) { - try { - icon = appInfo.loadIcon(pm); - } catch (Resources.NotFoundException e) { - } - } - if (icon == null) { - icon = context.getPackageManager().getDefaultActivityIcon(); - } - } catch (OutOfMemoryError e) { - Log.i(TAG, "Could not load app icon", e); - } - return icon; - } - - private static ApplicationInfo processAppInfoForFile(ApplicationInfo appInfo, File sourceFile) { - final String archiveFilePath = sourceFile.getAbsolutePath(); - appInfo.publicSourceDir = archiveFilePath; - - if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) { - final File[] files = sourceFile.getParentFile().listFiles(); - final String[] splits = Arrays.stream(appInfo.splitNames) - .map(i -> findFilePath(files, i + ".apk")) - .filter(Objects::nonNull) - .toArray(String[]::new); - - appInfo.splitSourceDirs = splits; - appInfo.splitPublicSourceDirs = splits; - } - return appInfo; - } - - private static String findFilePath(File[] files, String postfix) { - for (File file : files) { - final String path = file.getAbsolutePath(); - if (path.endsWith(postfix)) { - return path; - } - } - return null; - } - - /** - * @return the packageName corresponding to a UID. - */ - public static String getPackageNameForUid(Context context, int sourceUid, - String callingPackage) { - if (sourceUid == Process.INVALID_UID) { - return null; - } - // 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 - PackageManager pm = context.getPackageManager(); - ApplicationInfo systemDownloadProviderInfo = getSystemDownloadsProviderInfo( - pm, sourceUid); - if (systemDownloadProviderInfo != null) { - return systemDownloadProviderInfo.packageName; - } - String[] packagesForUid = pm.getPackagesForUid(sourceUid); - if (packagesForUid == null) { - return null; - } - if (packagesForUid.length > 1) { - if (callingPackage != null) { - for (String packageName : packagesForUid) { - if (packageName.equals(callingPackage)) { - return packageName; - } - } - } - Log.i(TAG, "Multiple packages found for source uid " + sourceUid); - } - return packagesForUid[0]; - } - - /** - * Utility method to get package information for a given {@link File} - */ - public static PackageInfo getPackageInfo(Context context, File sourceFile, int flags) { - String filePath = sourceFile.getAbsolutePath(); - if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) { - File dir = sourceFile.getParentFile(); - if (dir.listFiles().length > 1) { - // split apks, use file directory to get archive info - filePath = dir.getPath(); - } - } - try { - return context.getPackageManager().getPackageArchiveInfo(filePath, flags); - } catch (Exception ignored) { - return null; - } - } - - /** - * Is a profile part of a user? - * - * @param userManager The user manager - * @param userHandle The handle of the user - * @param profileHandle The handle of the profile - * - * @return If the profile is part of the user or the profile parent of the user - */ - public static boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle, - UserHandle profileHandle) { - if (userHandle.equals(profileHandle)) { - return true; - } - return userManager.getProfileParent(profileHandle) != null - && userManager.getProfileParent(profileHandle).equals(userHandle); - } - - /** - * The class to hold an incoming package's icon and label. - * See {@link #getAppSnippet(Context, SessionInfo)}, - * {@link #getAppSnippet(Context, PackageInfo)}, - * {@link #getAppSnippet(Context, ApplicationInfo)}, - * {@link #getAppSnippet(Context, ApplicationInfo, File)} - */ - public static class AppSnippet { - - private CharSequence mLabel; - private Drawable mIcon; - - public AppSnippet(CharSequence label, Drawable icon) { - mLabel = label; - mIcon = icon; - } - - public AppSnippet() { - } - - public CharSequence getLabel() { - return mLabel; - } - - public void setLabel(CharSequence mLabel) { - this.mLabel = mLabel; - } - - public Drawable getIcon() { - return mIcon; - } - - public void setIcon(Drawable mIcon) { - this.mIcon = mIcon; - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt new file mode 100644 index 000000000000..8d8c2f1d8171 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2023 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.v2.model + +import android.Manifest +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import java.io.File + +object PackageUtil { + private val LOG_TAG = InstallRepository::class.java.simpleName + private const val DOWNLOADS_AUTHORITY = "downloads" + private const val SPLIT_BASE_APK_END_WITH = "base.apk" + + /** + * Determines if the UID belongs to the system downloads provider and returns the + * [ApplicationInfo] of the provider + * + * @param uid UID of the caller + * @return [ApplicationInfo] of the provider if a downloads provider exists, it is a + * system app, and its UID matches with the passed UID, null otherwise. + */ + private fun getSystemDownloadsProviderInfo(pm: PackageManager, uid: Int): ApplicationInfo? { + // Check if there are currently enabled downloads provider on the system. + val providerInfo = pm.resolveContentProvider(DOWNLOADS_AUTHORITY, 0) + ?: return null + val appInfo = providerInfo.applicationInfo + return if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) && uid == appInfo.uid) { + appInfo + } else null + } + + /** + * Get the maximum target sdk for a UID. + * + * @param context The context to use + * @param uid The UID requesting the install/uninstall + * @return The maximum target SDK or -1 if the uid does not match any packages. + */ + @JvmStatic + fun getMaxTargetSdkVersionForUid(context: Context, uid: Int): Int { + val pm = context.packageManager + val packages = pm.getPackagesForUid(uid) + var targetSdkVersion = -1 + if (packages != null) { + for (packageName in packages) { + try { + val info = pm.getApplicationInfo(packageName!!, 0) + targetSdkVersion = maxOf(targetSdkVersion, info.targetSdkVersion) + } catch (e: PackageManager.NameNotFoundException) { + // Ignore and try the next package + } + } + } + return targetSdkVersion + } + + @JvmStatic + fun canPackageQuery(context: Context, callingUid: Int, packageUri: Uri): Boolean { + val pm = context.packageManager + val info = pm.resolveContentProvider( + packageUri.authority!!, + PackageManager.ComponentInfoFlags.of(0) + ) ?: return false + val targetPackage = info.packageName + val callingPackages = pm.getPackagesForUid(callingUid) ?: return false + for (callingPackage in callingPackages) { + try { + if (pm.canPackageQuery(callingPackage!!, targetPackage)) { + return true + } + } catch (e: PackageManager.NameNotFoundException) { + // no-op + } + } + return false + } + + /** + * @param context the [Context] object + * @param permission the permission name to check + * @param callingUid the UID of the caller who's permission is being checked + * @return `true` if the callingUid is granted the said permission + */ + @JvmStatic + fun isPermissionGranted(context: Context, permission: String, callingUid: Int): Boolean { + return (context.checkPermission(permission, -1, callingUid) + == PackageManager.PERMISSION_GRANTED) + } + + /** + * @param pm the [PackageManager] object + * @param permission the permission name to check + * @param packageName the name of the package who's permission is being checked + * @return `true` if the package is granted the said permission + */ + @JvmStatic + fun isPermissionGranted(pm: PackageManager, permission: String, packageName: String): Boolean { + return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED + } + + /** + * @param context the [Context] object + * @param callingUid the UID of the caller who's permission is being checked + * @param originatingUid the UID from where install is being originated. This could be same as + * callingUid or it will be the UID of the package performing a session based install + * @param isTrustedSource whether install request is coming from a privileged app or an app that + * has [Manifest.permission.INSTALL_PACKAGES] permission granted + * @return `true` if the package is granted the said permission + */ + @JvmStatic + fun isInstallPermissionGrantedOrRequested( + context: Context, + callingUid: Int, + originatingUid: Int, + isTrustedSource: Boolean, + ): Boolean { + val isDocumentsManager = + isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid) + val isSystemDownloadsProvider = + getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null + + if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) { + val targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid) + if (targetSdkVersion < 0) { + // Invalid originating uid supplied. Abort install. + Log.w(LOG_TAG, "Cannot get target sdk version for uid $originatingUid") + return false + } else if (targetSdkVersion >= Build.VERSION_CODES.O + && !isUidRequestingPermission( + context.packageManager, originatingUid, + Manifest.permission.REQUEST_INSTALL_PACKAGES + ) + ) { + Log.e( + LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission " + + Manifest.permission.REQUEST_INSTALL_PACKAGES + ) + return false + } + } + return true + } + + /** + * @param pm the [PackageManager] object + * @param uid the UID of the caller who's permission is being checked + * @param permission the permission name to check + * @return `true` if the caller is requesting the said permission in its Manifest + */ + private fun isUidRequestingPermission( + pm: PackageManager, + uid: Int, + permission: String, + ): Boolean { + val packageNames = pm.getPackagesForUid(uid) ?: return false + for (packageName in packageNames) { + val packageInfo: PackageInfo = try { + pm.getPackageInfo(packageName!!, PackageManager.GET_PERMISSIONS) + } catch (e: PackageManager.NameNotFoundException) { + // Ignore and try the next package + continue + } + if (packageInfo.requestedPermissions != null + && listOf(*packageInfo.requestedPermissions!!).contains(permission) + ) { + return true + } + } + return false + } + + /** + * @param pi the [PackageInstaller] object to use + * @param originatingUid the UID of the package performing a session based install + * @param sessionId ID of the install session + * @return `true` if the caller is the session owner + */ + @JvmStatic + fun isCallerSessionOwner(pi: PackageInstaller, originatingUid: Int, sessionId: Int): Boolean { + if (originatingUid == Process.ROOT_UID) { + return true + } + val sessionInfo = pi.getSessionInfo(sessionId) ?: return false + val installerUid = sessionInfo.getInstallerUid() + return originatingUid == installerUid + } + + /** + * Generates a stub [PackageInfo] object for the given packageName + */ + @JvmStatic + fun generateStubPackageInfo(packageName: String?): PackageInfo { + val info = PackageInfo() + val aInfo = ApplicationInfo() + info.applicationInfo = aInfo + info.applicationInfo!!.packageName = packageName + info.packageName = info.applicationInfo!!.packageName + return info + } + + /** + * Generates an [AppSnippet] containing an appIcon and appLabel from the + * [PackageInstaller.SessionInfo] object + */ + @JvmStatic + fun getAppSnippet(context: Context, info: PackageInstaller.SessionInfo): AppSnippet { + val pm = context.packageManager + val label = info.getAppLabel() + val icon = if (info.getAppIcon() != null) BitmapDrawable( + context.resources, + info.getAppIcon() + ) else pm.defaultActivityIcon + return AppSnippet(label, icon) + } + + /** + * Generates an [AppSnippet] containing an appIcon and appLabel from the + * [PackageInfo] object + */ + @JvmStatic + fun getAppSnippet(context: Context, pkgInfo: PackageInfo): AppSnippet { + return pkgInfo.applicationInfo?.let { getAppSnippet(context, it) } ?: run { + AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon) + } + } + + /** + * Generates an [AppSnippet] containing an appIcon and appLabel from the + * [ApplicationInfo] object + */ + @JvmStatic + fun getAppSnippet(context: Context, appInfo: ApplicationInfo): AppSnippet { + val pm = context.packageManager + val label = pm.getApplicationLabel(appInfo) + val icon = pm.getApplicationIcon(appInfo) + return AppSnippet(label, icon) + } + + /** + * Generates an [AppSnippet] containing an appIcon and appLabel from the + * supplied APK file + */ + @JvmStatic + fun getAppSnippet(context: Context, pkgInfo: PackageInfo, sourceFile: File): AppSnippet { + pkgInfo.applicationInfo?.let { + val appInfoFromFile = processAppInfoForFile(it, sourceFile) + val label = getAppLabelFromFile(context, appInfoFromFile) + val icon = getAppIconFromFile(context, appInfoFromFile) + return AppSnippet(label, icon) + } ?: run { + return AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon) + } + } + + /** + * Utility method to load application label + * + * @param context context of package that can load the resources + * @param appInfo ApplicationInfo object of package whose resources are to be loaded + */ + private fun getAppLabelFromFile(context: Context, appInfo: ApplicationInfo): CharSequence? { + val pm = context.packageManager + var label: CharSequence? = null + // Try to load the label from the package's resources. If an app has not explicitly + // specified any label, just use the package name. + if (appInfo.labelRes != 0) { + try { + label = appInfo.loadLabel(pm) + } catch (e: Resources.NotFoundException) { + } + } + if (label == null) { + label = if (appInfo.nonLocalizedLabel != null) appInfo.nonLocalizedLabel + else appInfo.packageName + } + return label + } + + /** + * Utility method to load application icon + * + * @param context context of package that can load the resources + * @param appInfo ApplicationInfo object of package whose resources are to be loaded + */ + private fun getAppIconFromFile(context: Context, appInfo: ApplicationInfo): Drawable? { + val pm = context.packageManager + var icon: Drawable? = null + // Try to load the icon from the package's resources. If an app has not explicitly + // specified any resource, just use the default icon for now. + try { + if (appInfo.icon != 0) { + try { + icon = appInfo.loadIcon(pm) + } catch (e: Resources.NotFoundException) { + } + } + if (icon == null) { + icon = context.packageManager.defaultActivityIcon + } + } catch (e: OutOfMemoryError) { + Log.i(LOG_TAG, "Could not load app icon", e) + } + return icon + } + + private fun processAppInfoForFile(appInfo: ApplicationInfo, sourceFile: File): ApplicationInfo { + val archiveFilePath = sourceFile.absolutePath + appInfo.publicSourceDir = archiveFilePath + if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) { + val files = sourceFile.parentFile?.listFiles() + val splits = appInfo.splitNames!! + .mapNotNull { findFilePath(files, "$it.apk") } + .toTypedArray() + + appInfo.splitSourceDirs = splits + appInfo.splitPublicSourceDirs = splits + } + return appInfo + } + + private fun findFilePath(files: Array<File>?, postfix: String): String? { + files?.let { + for (file in it) { + val path = file.absolutePath + if (path.endsWith(postfix)) { + return path + } + } + } + return null + } + + /** + * @return the packageName corresponding to a UID. + */ + @JvmStatic + fun getPackageNameForUid(context: Context, sourceUid: Int, callingPackage: String?): String? { + if (sourceUid == Process.INVALID_UID) { + return null + } + // 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 + val pm = context.packageManager + val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, sourceUid) + if (systemDownloadProviderInfo != null) { + return systemDownloadProviderInfo.packageName + } + val packagesForUid = pm.getPackagesForUid(sourceUid) ?: return null + if (packagesForUid.size > 1) { + if (callingPackage != null) { + for (packageName in packagesForUid) { + if (packageName == callingPackage) { + return packageName + } + } + } + Log.i(LOG_TAG, "Multiple packages found for source uid $sourceUid") + } + return packagesForUid[0] + } + + /** + * Utility method to get package information for a given [File] + */ + @JvmStatic + fun getPackageInfo(context: Context, sourceFile: File, flags: Int): PackageInfo? { + var filePath = sourceFile.absolutePath + if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) { + val dir = sourceFile.parentFile + if ((dir?.listFiles()?.size ?: 0) > 1) { + // split apks, use file directory to get archive info + filePath = dir.path + } + } + return try { + context.packageManager.getPackageArchiveInfo(filePath, flags) + } catch (ignored: Exception) { + null + } + } + + /** + * Is a profile part of a user? + * + * @param userManager The user manager + * @param userHandle The handle of the user + * @param profileHandle The handle of the profile + * + * @return If the profile is part of the user or the profile parent of the user + */ + @JvmStatic + fun isProfileOfOrSame( + userManager: UserManager, + userHandle: UserHandle, + profileHandle: UserHandle?, + ): Boolean { + if (profileHandle == null) { + return false + } + return if (userHandle == profileHandle) { + true + } else userManager.getProfileParent(profileHandle) != null + && userManager.getProfileParent(profileHandle) == userHandle + } + + /** + * The class to hold an incoming package's icon and label. + * See [getAppSnippet] + */ + data class AppSnippet(var label: CharSequence?, var icon: Drawable?) +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java deleted file mode 100644 index a2c81f11cf68..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model; - -import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH; - -import android.content.Context; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageInstaller.SessionInfo; -import android.content.res.AssetFileDescriptor; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; -import androidx.lifecycle.MutableLiveData; -import com.android.packageinstaller.v2.model.InstallRepository.SessionStageListener; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class SessionStager extends AsyncTask<Void, Integer, SessionInfo> { - - private static final String TAG = SessionStager.class.getSimpleName(); - private final Context mContext; - private final Uri mUri; - private final int mStagedSessionId; - private final MutableLiveData<Integer> mProgressLiveData = new MutableLiveData<>(0); - private final SessionStageListener mListener; - - SessionStager(Context context, Uri uri, int stagedSessionId, SessionStageListener listener) { - mContext = context; - mUri = uri; - mStagedSessionId = stagedSessionId; - mListener = listener; - } - - @Override - protected PackageInstaller.SessionInfo doInBackground(Void... params) { - PackageInstaller pi = mContext.getPackageManager().getPackageInstaller(); - try (PackageInstaller.Session session = pi.openSession(mStagedSessionId); - InputStream in = mContext.getContentResolver().openInputStream(mUri)) { - session.setStagingProgress(0); - - if (in == null) { - return null; - } - final long sizeBytes = getContentSizeBytes(); - mProgressLiveData.postValue(sizeBytes > 0 ? 0 : -1); - - long totalRead = 0; - try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) { - byte[] buffer = new byte[1024 * 1024]; - while (true) { - int numRead = in.read(buffer); - - if (numRead == -1) { - session.fsync(out); - break; - } - - if (isCancelled()) { - break; - } - - out.write(buffer, 0, numRead); - if (sizeBytes > 0) { - totalRead += numRead; - float fraction = ((float) totalRead / (float) sizeBytes); - session.setStagingProgress(fraction); - publishProgress((int) (fraction * 100.0)); - } - } - } - return pi.getSessionInfo(mStagedSessionId); - } catch (IOException | SecurityException | IllegalStateException - | IllegalArgumentException e) { - Log.w(TAG, "Error staging apk from content URI", e); - return null; - } - } - - private long getContentSizeBytes() { - try (AssetFileDescriptor afd = mContext.getContentResolver() - .openAssetFileDescriptor(mUri, "r")) { - return afd != null ? afd.getLength() : UNKNOWN_LENGTH; - } catch (IOException e) { - Log.w(TAG, "Failed to open asset file descriptor", e); - return UNKNOWN_LENGTH; - } - } - - public MutableLiveData<Integer> getProgress() { - return mProgressLiveData; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - if (progress != null && progress.length > 0) { - mProgressLiveData.setValue(progress[0]); - } - } - - @Override - protected void onPostExecute(SessionInfo sessionInfo) { - if (sessionInfo == null || !sessionInfo.isActive() - || sessionInfo.getResolvedBaseApkPath() == null) { - Log.w(TAG, "Session info is invalid: " + sessionInfo); - mListener.onStagingFailure(); - return; - } - mListener.onStagingSuccess(sessionInfo); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt new file mode 100644 index 000000000000..c9bfa17d80dc --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 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.v2.model + +import android.content.Context +import android.content.pm.PackageInstaller +import android.content.res.AssetFileDescriptor +import android.net.Uri +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SessionStager internal constructor( + private val context: Context, + private val uri: Uri, + private val stagedSessionId: Int +) { + + companion object { + private val LOG_TAG = SessionStager::class.java.simpleName + } + + private val _progress = MutableLiveData(0) + val progress: LiveData<Int> + get() = _progress + + suspend fun execute(): Boolean = withContext(Dispatchers.IO) { + val pi: PackageInstaller = context.packageManager.packageInstaller + var sessionInfo: PackageInstaller.SessionInfo? + try { + val session = pi.openSession(stagedSessionId) + context.contentResolver.openInputStream(uri).use { instream -> + session.setStagingProgress(0f) + + if (instream == null) { + return@withContext false + } + + val sizeBytes = getContentSizeBytes() + publishProgress(if (sizeBytes > 0) 0 else -1) + + var totalRead: Long = 0 + session.openWrite("PackageInstaller", 0, sizeBytes).use { out -> + val buffer = ByteArray(1024 * 1024) + while (true) { + val numRead = instream.read(buffer) + if (numRead == -1) { + session.fsync(out) + break + } + out.write(buffer, 0, numRead) + + if (sizeBytes > 0) { + totalRead += numRead.toLong() + val fraction = totalRead.toFloat() / sizeBytes.toFloat() + session.setStagingProgress(fraction) + publishProgress((fraction * 100.0).toInt()) + } + } + } + sessionInfo = pi.getSessionInfo(stagedSessionId) + } + } catch (e: Exception) { + Log.w(LOG_TAG, "Error staging apk from content URI", e) + sessionInfo = null + } + + return@withContext if (sessionInfo == null + || !sessionInfo?.isActive!! + || sessionInfo?.resolvedBaseApkPath == null + ) { + Log.w(LOG_TAG, "Session info is invalid: $sessionInfo") + false + } else { + true + } + } + + private fun getContentSizeBytes(): Long { + return try { + context.contentResolver + .openAssetFileDescriptor(uri, "r") + .use { afd -> afd?.length ?: AssetFileDescriptor.UNKNOWN_LENGTH } + } catch (e: IOException) { + Log.w(LOG_TAG, "Failed to open asset file descriptor", e) + AssetFileDescriptor.UNKNOWN_LENGTH + } + } + + private suspend fun publishProgress(progressValue: Int) = withContext(Dispatchers.Main) { + _progress.value = progressValue + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java deleted file mode 100644 index a07c5326fa11..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java +++ /dev/null @@ -1,716 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; -import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; -import static com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid; -import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid; -import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted; -import static com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame; -import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_APP_UNAVAILABLE; -import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_GENERIC_ERROR; -import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED; - -import android.Manifest; -import android.app.Activity; -import android.app.AppOpsManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.admin.DevicePolicyManager; -import android.app.usage.StorageStats; -import android.app.usage.StorageStatsManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.UninstallCompleteCallback; -import android.content.pm.VersionedPackage; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Process; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.MutableLiveData; -import com.android.packageinstaller.R; -import com.android.packageinstaller.common.EventResultPersister; -import com.android.packageinstaller.common.UninstallEventReceiver; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallReady; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired; -import java.io.IOException; -import java.util.List; - -public class UninstallRepository { - - private static final String TAG = UninstallRepository.class.getSimpleName(); - private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall_failure"; - private static final String BROADCAST_ACTION = - "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; - - private static final String EXTRA_UNINSTALL_ID = - "com.android.packageinstaller.extra.UNINSTALL_ID"; - private static final String EXTRA_APP_LABEL = - "com.android.packageinstaller.extra.APP_LABEL"; - private static final String EXTRA_IS_CLONE_APP = - "com.android.packageinstaller.extra.IS_CLONE_APP"; - private static final String EXTRA_PACKAGE_NAME = - "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME"; - - private final Context mContext; - private final AppOpsManager mAppOpsManager; - private final PackageManager mPackageManager; - private final UserManager mUserManager; - private final NotificationManager mNotificationManager; - private final MutableLiveData<UninstallStage> mUninstallResult = new MutableLiveData<>(); - public UserHandle mUninstalledUser; - public UninstallCompleteCallback mCallback; - private ApplicationInfo mTargetAppInfo; - private ActivityInfo mTargetActivityInfo; - private Intent mIntent; - private CharSequence mTargetAppLabel; - private String mTargetPackageName; - private String mCallingActivity; - private boolean mUninstallFromAllUsers; - private boolean mIsClonedApp; - private int mUninstallId; - - public UninstallRepository(Context context) { - mContext = context; - mAppOpsManager = context.getSystemService(AppOpsManager.class); - mPackageManager = context.getPackageManager(); - mUserManager = context.getSystemService(UserManager.class); - mNotificationManager = context.getSystemService(NotificationManager.class); - } - - public UninstallStage performPreUninstallChecks(Intent intent, CallerInfo callerInfo) { - mIntent = intent; - - int callingUid = callerInfo.getUid(); - mCallingActivity = callerInfo.getActivityName(); - - if (callingUid == Process.INVALID_UID) { - Log.e(TAG, "Could not determine the launching uid."); - return new UninstallAborted(ABORT_REASON_GENERIC_ERROR); - // TODO: should we give any indication to the user? - } - - String callingPackage = getPackageNameForUid(mContext, callingUid, null); - if (callingPackage == null) { - Log.e(TAG, "Package not found for originating uid " + callingUid); - return new UninstallAborted(ABORT_REASON_GENERIC_ERROR); - } else { - if (mAppOpsManager.noteOpNoThrow( - AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage) - != MODE_ALLOWED) { - Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps"); - return new UninstallAborted(ABORT_REASON_GENERIC_ERROR); - } - } - - if (getMaxTargetSdkVersionForUid(mContext, callingUid) >= Build.VERSION_CODES.P - && !isPermissionGranted(mContext, Manifest.permission.REQUEST_DELETE_PACKAGES, - callingUid) - && !isPermissionGranted(mContext, Manifest.permission.DELETE_PACKAGES, callingUid)) { - Log.e(TAG, "Uid " + callingUid + " does not have " - + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " - + Manifest.permission.DELETE_PACKAGES); - - return new UninstallAborted(ABORT_REASON_GENERIC_ERROR); - } - - // Get intent information. - // We expect an intent with URI of the form package:<packageName>#<className> - // className is optional; if specified, it is the activity the user chose to uninstall - final Uri packageUri = intent.getData(); - if (packageUri == null) { - Log.e(TAG, "No package URI in intent"); - return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE); - } - mTargetPackageName = packageUri.getEncodedSchemeSpecificPart(); - if (mTargetPackageName == null) { - Log.e(TAG, "Invalid package name in URI: " + packageUri); - return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE); - } - - mUninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, - false); - if (mUninstallFromAllUsers && !mUserManager.isAdminUser()) { - Log.e(TAG, "Only admin user can request uninstall for all users"); - return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED); - } - - mUninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class); - if (mUninstalledUser == null) { - mUninstalledUser = Process.myUserHandle(); - } else { - List<UserHandle> profiles = mUserManager.getUserProfiles(); - if (!profiles.contains(mUninstalledUser)) { - Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall " - + "for user " + mUninstalledUser); - return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED); - } - } - - mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK, - PackageManager.UninstallCompleteCallback.class); - - try { - mTargetAppInfo = mPackageManager.getApplicationInfo(mTargetPackageName, - PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Unable to get packageName"); - } - - if (mTargetAppInfo == null) { - Log.e(TAG, "Invalid packageName: " + mTargetPackageName); - return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE); - } - - // The class name may have been specified (e.g. when deleting an app from all apps) - final String className = packageUri.getFragment(); - if (className != null) { - try { - mTargetActivityInfo = mPackageManager.getActivityInfo( - new ComponentName(mTargetPackageName, className), - PackageManager.ComponentInfoFlags.of(0)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Unable to get className"); - // Continue as the ActivityInfo isn't critical. - } - } - - return new UninstallReady(); - } - - public UninstallStage generateUninstallDetails() { - UninstallUserActionRequired.Builder uarBuilder = new UninstallUserActionRequired.Builder(); - StringBuilder messageBuilder = new StringBuilder(); - - mTargetAppLabel = mTargetAppInfo.loadSafeLabel(mPackageManager); - - // If the Activity label differs from the App label, then make sure the user - // knows the Activity belongs to the App being uninstalled. - if (mTargetActivityInfo != null) { - final CharSequence activityLabel = mTargetActivityInfo.loadSafeLabel(mPackageManager); - if (CharSequence.compare(activityLabel, mTargetAppLabel) != 0) { - messageBuilder.append( - mContext.getString(R.string.uninstall_activity_text, activityLabel)); - messageBuilder.append(" ").append(mTargetAppLabel).append(".\n\n"); - } - } - - final boolean isUpdate = - (mTargetAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; - final UserHandle myUserHandle = Process.myUserHandle(); - boolean isSingleUser = isSingleUser(); - - if (isUpdate) { - messageBuilder.append(mContext.getString( - isSingleUser ? R.string.uninstall_update_text : - R.string.uninstall_update_text_multiuser)); - } else if (mUninstallFromAllUsers && !isSingleUser) { - messageBuilder.append(mContext.getString( - R.string.uninstall_application_text_all_users)); - } else if (!mUninstalledUser.equals(myUserHandle)) { - // Uninstalling user is issuing uninstall for another user - UserManager customUserManager = mContext.createContextAsUser(mUninstalledUser, 0) - .getSystemService(UserManager.class); - String userName = customUserManager.getUserName(); - - String uninstalledUserType = getUninstalledUserType(myUserHandle, mUninstalledUser); - String messageString; - if (USER_TYPE_PROFILE_MANAGED.equals(uninstalledUserType)) { - messageString = mContext.getString( - R.string.uninstall_application_text_current_user_work_profile, userName); - } else if (USER_TYPE_PROFILE_CLONE.equals(uninstalledUserType)) { - mIsClonedApp = true; - messageString = mContext.getString( - R.string.uninstall_application_text_current_user_clone_profile); - } else { - messageString = mContext.getString( - R.string.uninstall_application_text_user, userName); - } - messageBuilder.append(messageString); - } else if (isCloneProfile(mUninstalledUser)) { - mIsClonedApp = true; - messageBuilder.append(mContext.getString( - R.string.uninstall_application_text_current_user_clone_profile)); - } else if (myUserHandle.equals(UserHandle.SYSTEM) - && hasClonedInstance(mTargetAppInfo.packageName)) { - messageBuilder.append(mContext.getString( - R.string.uninstall_application_text_with_clone_instance, mTargetAppLabel)); - } else { - messageBuilder.append(mContext.getString(R.string.uninstall_application_text)); - } - - uarBuilder.setMessage(messageBuilder.toString()); - - if (mIsClonedApp) { - uarBuilder.setTitle(mContext.getString(R.string.cloned_app_label, mTargetAppLabel)); - } else { - uarBuilder.setTitle(mTargetAppLabel.toString()); - } - - boolean suggestToKeepAppData = false; - try { - PackageInfo pkgInfo = mPackageManager.getPackageInfo(mTargetPackageName, 0); - suggestToKeepAppData = - pkgInfo.applicationInfo != null && pkgInfo.applicationInfo.hasFragileUserData(); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Cannot check hasFragileUserData for " + mTargetPackageName, e); - } - - long appDataSize = 0; - if (suggestToKeepAppData) { - appDataSize = getAppDataSize(mTargetPackageName, - mUninstallFromAllUsers ? null : mUninstalledUser); - } - uarBuilder.setAppDataSize(appDataSize); - - return uarBuilder.build(); - } - - /** - * Returns whether there is only one "full" user on this device. - * - * <p><b>Note:</b> on devices that use {@link android.os.UserManager#isHeadlessSystemUserMode() - * headless system user mode}, the system user is not "full", so it's not be considered in the - * calculation.</p> - */ - private boolean isSingleUser() { - final int userCount = mUserManager.getUserCount(); - return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2); - } - - /** - * Returns the type of the user from where an app is being uninstalled. We are concerned with - * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile - * belong to the same profile group. - */ - @Nullable - private String getUninstalledUserType(UserHandle myUserHandle, - UserHandle uninstalledUserHandle) { - if (!mUserManager.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) { - return null; - } - - UserManager customUserManager = mContext.createContextAsUser(uninstalledUserHandle, 0) - .getSystemService(UserManager.class); - String[] userTypes = {USER_TYPE_PROFILE_MANAGED, USER_TYPE_PROFILE_CLONE}; - for (String userType : userTypes) { - if (customUserManager.isUserOfType(userType)) { - return userType; - } - } - return null; - } - - private boolean hasClonedInstance(String packageName) { - // Check if clone user is present on the device. - UserHandle cloneUser = null; - List<UserHandle> profiles = mUserManager.getUserProfiles(); - for (UserHandle userHandle : profiles) { - if (!userHandle.equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) { - cloneUser = userHandle; - break; - } - } - // Check if another instance of given package exists in clone user profile. - try { - return cloneUser != null - && mPackageManager.getPackageUidAsUser(packageName, - PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - private boolean isCloneProfile(UserHandle userHandle) { - UserManager customUserManager = mContext.createContextAsUser(userHandle, 0) - .getSystemService(UserManager.class); - return customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE); - } - - /** - * Get number of bytes of the app data of the package. - * - * @param pkg The package that might have app data. - * @param user The user the package belongs to or {@code null} if files of all users should - * be counted. - * @return The number of bytes. - */ - private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) { - if (user != null) { - return getAppDataSizeForUser(pkg, user); - } - // We are uninstalling from all users. Get cumulative app data size for all users. - List<UserHandle> userHandles = mUserManager.getUserHandles(true); - long totalAppDataSize = 0; - int numUsers = userHandles.size(); - for (int i = 0; i < numUsers; i++) { - totalAppDataSize += getAppDataSizeForUser(pkg, userHandles.get(i)); - } - return totalAppDataSize; - } - - /** - * Get number of bytes of the app data of the package. - * - * @param pkg The package that might have app data. - * @param user The user the package belongs to - * @return The number of bytes. - */ - private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) { - StorageStatsManager storageStatsManager = - mContext.getSystemService(StorageStatsManager.class); - try { - StorageStats stats = storageStatsManager.queryStatsForPackage( - mPackageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user); - return stats.getDataBytes(); - } catch (PackageManager.NameNotFoundException | IOException | SecurityException e) { - Log.e(TAG, "Cannot determine amount of app data for " + pkg, e); - } - return 0; - } - - public void initiateUninstall(boolean keepData) { - // Get an uninstallId to track results and show a notification on non-TV devices. - try { - mUninstallId = UninstallEventReceiver.addObserver(mContext, - EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult); - } catch (EventResultPersister.OutOfIdsException e) { - Log.e(TAG, "Failed to start uninstall", e); - handleUninstallResult(PackageInstaller.STATUS_FAILURE, - PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0); - return; - } - - // TODO: Check with UX whether to show UninstallUninstalling dialog / notification? - mUninstallResult.setValue(new UninstallUninstalling(mTargetAppLabel, mIsClonedApp)); - - Bundle uninstallData = new Bundle(); - uninstallData.putInt(EXTRA_UNINSTALL_ID, mUninstallId); - uninstallData.putString(EXTRA_PACKAGE_NAME, mTargetPackageName); - uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, mUninstallFromAllUsers); - uninstallData.putCharSequence(EXTRA_APP_LABEL, mTargetAppLabel); - uninstallData.putBoolean(EXTRA_IS_CLONE_APP, mIsClonedApp); - Log.i(TAG, "Uninstalling extras = " + uninstallData); - - // Get a PendingIntent for result broadcast and issue an uninstall request - Intent broadcastIntent = new Intent(BROADCAST_ACTION); - broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mUninstallId); - broadcastIntent.setPackage(mContext.getPackageName()); - - PendingIntent pendingIntent = - PendingIntent.getBroadcast(mContext, mUninstallId, broadcastIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); - - if (!startUninstall(mTargetPackageName, mUninstalledUser, pendingIntent, - mUninstallFromAllUsers, keepData)) { - handleUninstallResult(PackageInstaller.STATUS_FAILURE, - PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0); - } - } - - private void handleUninstallResult(int status, int legacyStatus, @Nullable String message, - int serviceId) { - if (mCallback != null) { - // The caller will be informed about the result via a callback - mCallback.onUninstallComplete(mTargetPackageName, legacyStatus, message); - - // Since the caller already received the results, just finish the app at this point - mUninstallResult.setValue(null); - return; - } - - boolean returnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); - if (returnResult || mCallingActivity != null) { - Intent intent = new Intent(); - intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus); - - if (status == PackageInstaller.STATUS_SUCCESS) { - UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder() - .setResultIntent(intent) - .setActivityResultCode(Activity.RESULT_OK); - mUninstallResult.setValue(successBuilder.build()); - } else { - UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(true) - .setResultIntent(intent) - .setActivityResultCode(Activity.RESULT_FIRST_USER); - mUninstallResult.setValue(failedBuilder.build()); - } - return; - } - - // Caller did not want the result back. So, we either show a Toast, or a Notification. - if (status == PackageInstaller.STATUS_SUCCESS) { - UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder() - .setActivityResultCode(legacyStatus) - .setMessage(mIsClonedApp - ? mContext.getString(R.string.uninstall_done_clone_app, mTargetAppLabel) - : mContext.getString(R.string.uninstall_done_app, mTargetAppLabel)); - mUninstallResult.setValue(successBuilder.build()); - } else { - UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(false); - Notification.Builder uninstallFailedNotification = null; - - NotificationChannel uninstallFailureChannel = new NotificationChannel( - UNINSTALL_FAILURE_CHANNEL, - mContext.getString(R.string.uninstall_failure_notification_channel), - NotificationManager.IMPORTANCE_DEFAULT); - mNotificationManager.createNotificationChannel(uninstallFailureChannel); - - uninstallFailedNotification = new Notification.Builder(mContext, - UNINSTALL_FAILURE_CHANNEL); - - UserHandle myUserHandle = Process.myUserHandle(); - switch (legacyStatus) { - case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> { - // Find out if the package is an active admin for some non-current user. - UserHandle otherBlockingUserHandle = - findUserOfDeviceAdmin(myUserHandle, mTargetPackageName); - - if (otherBlockingUserHandle == null) { - Log.d(TAG, "Uninstall failed because " + mTargetPackageName - + " is a device admin"); - - addDeviceManagerButton(mContext, uninstallFailedNotification); - setBigText(uninstallFailedNotification, mContext.getString( - R.string.uninstall_failed_device_policy_manager)); - } else { - Log.d(TAG, "Uninstall failed because " + mTargetPackageName - + " is a device admin of user " + otherBlockingUserHandle); - - String userName = - mContext.createContextAsUser(otherBlockingUserHandle, 0) - .getSystemService(UserManager.class).getUserName(); - setBigText(uninstallFailedNotification, String.format( - mContext.getString( - R.string.uninstall_failed_device_policy_manager_of_user), - userName)); - } - } - case PackageManager.DELETE_FAILED_OWNER_BLOCKED -> { - UserHandle otherBlockingUserHandle = findBlockingUser(mTargetPackageName); - boolean isProfileOfOrSame = isProfileOfOrSame(mUserManager, myUserHandle, - otherBlockingUserHandle); - - if (isProfileOfOrSame) { - addDeviceManagerButton(mContext, uninstallFailedNotification); - } else { - addManageUsersButton(mContext, uninstallFailedNotification); - } - - String bigText = null; - if (otherBlockingUserHandle == null) { - Log.d(TAG, "Uninstall failed for " + mTargetPackageName + - " with code " + status + " no blocking user"); - } else if (otherBlockingUserHandle == UserHandle.SYSTEM) { - bigText = mContext.getString( - R.string.uninstall_blocked_device_owner); - } else { - bigText = mContext.getString(mUninstallFromAllUsers ? - R.string.uninstall_all_blocked_profile_owner - : R.string.uninstall_blocked_profile_owner); - } - if (bigText != null) { - setBigText(uninstallFailedNotification, bigText); - } - } - default -> { - Log.d(TAG, "Uninstall blocked for " + mTargetPackageName - + " with legacy code " + legacyStatus); - } - } - - uninstallFailedNotification.setContentTitle( - mContext.getString(R.string.uninstall_failed_app, mTargetAppLabel)); - uninstallFailedNotification.setOngoing(false); - uninstallFailedNotification.setSmallIcon(R.drawable.ic_error); - failedBuilder.setUninstallNotification(mUninstallId, - uninstallFailedNotification.build()); - - mUninstallResult.setValue(failedBuilder.build()); - } - } - - /** - * @param myUserHandle {@link UserHandle} of the current user. - * @param packageName Name of the package being uninstalled. - * @return the {@link UserHandle} of the user in which a package is a device admin. - */ - @Nullable - private UserHandle findUserOfDeviceAdmin(UserHandle myUserHandle, String packageName) { - for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) { - // We only catch the case when the user in question is neither the - // current user nor its profile. - if (isProfileOfOrSame(mUserManager, myUserHandle, otherUserHandle)) { - continue; - } - DevicePolicyManager dpm = mContext.createContextAsUser(otherUserHandle, 0) - .getSystemService(DevicePolicyManager.class); - if (dpm.packageHasActiveAdmins(packageName)) { - return otherUserHandle; - } - } - return null; - } - - /** - * - * @param packageName Name of the package being uninstalled. - * @return {@link UserHandle} of the user in which a package is blocked from being uninstalled. - */ - @Nullable - private UserHandle findBlockingUser(String packageName) { - for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) { - // TODO (b/307399586): Add a negation when the logic of the method - // is fixed - if (mPackageManager.canUserUninstall(packageName, otherUserHandle)) { - return otherUserHandle; - } - } - return null; - } - - /** - * Set big text for the notification. - * - * @param builder The builder of the notification - * @param text The text to set. - */ - private void setBigText(@NonNull Notification.Builder builder, - @NonNull CharSequence text) { - builder.setStyle(new Notification.BigTextStyle().bigText(text)); - } - - /** - * Add a button to the notification that links to the user management. - * - * @param context The context the notification is created in - * @param builder The builder of the notification - */ - private void addManageUsersButton(@NonNull Context context, - @NonNull Notification.Builder builder) { - builder.addAction((new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_settings_multiuser), - context.getString(R.string.manage_users), - PendingIntent.getActivity(context, 0, getUserSettingsIntent(), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build()); - } - - private Intent getUserSettingsIntent() { - Intent intent = new Intent(Settings.ACTION_USER_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - - /** - * Add a button to the notification that links to the device policy management. - * - * @param context The context the notification is created in - * @param builder The builder of the notification - */ - private void addDeviceManagerButton(@NonNull Context context, - @NonNull Notification.Builder builder) { - builder.addAction((new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_lock), - context.getString(R.string.manage_device_administrators), - PendingIntent.getActivity(context, 0, getDeviceManagerIntent(), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build()); - } - - private Intent getDeviceManagerIntent() { - Intent intent = new Intent(); - intent.setClassName("com.android.settings", - "com.android.settings.Settings$DeviceAdminSettingsActivity"); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - - /** - * Starts an uninstall for the given package. - * - * @return {@code true} if there was no exception while uninstalling. This does not represent - * the result of the uninstall. Result will be made available in - * {@link #handleUninstallResult(int, int, String, int)} - */ - private boolean startUninstall(String packageName, UserHandle targetUser, - PendingIntent pendingIntent, boolean uninstallFromAllUsers, boolean keepData) { - int flags = uninstallFromAllUsers ? PackageManager.DELETE_ALL_USERS : 0; - flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0; - try { - mContext.createContextAsUser(targetUser, 0) - .getPackageManager().getPackageInstaller().uninstall( - new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), - flags, pendingIntent.getIntentSender()); - return true; - } catch (IllegalArgumentException e) { - Log.e(TAG, "Failed to uninstall", e); - return false; - } - } - - public void cancelInstall() { - if (mCallback != null) { - mCallback.onUninstallComplete(mTargetPackageName, - PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"); - } - } - - public MutableLiveData<UninstallStage> getUninstallResult() { - return mUninstallResult; - } - - public static class CallerInfo { - - private final String mActivityName; - private final int mUid; - - public CallerInfo(String activityName, int uid) { - mActivityName = activityName; - mUid = uid; - } - - public String getActivityName() { - return mActivityName; - } - - public int getUid() { - return mUid; - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt new file mode 100644 index 000000000000..7cc95c5d7299 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt @@ -0,0 +1,739 @@ +/* + * Copyright (C) 2023 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 + * + * https://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.v2.model + +import android.Manifest +import android.app.Activity +import android.app.AppOpsManager +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.admin.DevicePolicyManager +import android.app.usage.StorageStatsManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.pm.VersionedPackage +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.android.packageinstaller.R +import com.android.packageinstaller.common.EventResultPersister +import com.android.packageinstaller.common.EventResultPersister.OutOfIdsException +import com.android.packageinstaller.common.UninstallEventReceiver +import com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid +import com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid +import com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted +import com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame + +class UninstallRepository(private val context: Context) { + + private val appOpsManager: AppOpsManager? = context.getSystemService(AppOpsManager::class.java) + private val packageManager: PackageManager = context.packageManager + private val userManager: UserManager? = context.getSystemService(UserManager::class.java) + private val notificationManager: NotificationManager? = + context.getSystemService(NotificationManager::class.java) + val uninstallResult = MutableLiveData<UninstallStage?>() + private var uninstalledUser: UserHandle? = null + private var callback: PackageManager.UninstallCompleteCallback? = null + private var targetAppInfo: ApplicationInfo? = null + private var targetActivityInfo: ActivityInfo? = null + private lateinit var intent: Intent + private lateinit var targetAppLabel: CharSequence + private var targetPackageName: String? = null + private var callingActivity: String? = null + private var uninstallFromAllUsers = false + private var isClonedApp = false + private var uninstallId = 0 + + fun performPreUninstallChecks(intent: Intent, callerInfo: CallerInfo): UninstallStage { + this.intent = intent + + val callingUid = callerInfo.uid + callingActivity = callerInfo.activityName + + if (callingUid == Process.INVALID_UID) { + Log.e(LOG_TAG, "Could not determine the launching uid.") + return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR) + // TODO: should we give any indication to the user? + } + + val callingPackage = getPackageNameForUid(context, callingUid, null) + if (callingPackage == null) { + Log.e(LOG_TAG, "Package not found for originating uid $callingUid") + return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR) + } else { + if (appOpsManager!!.noteOpNoThrow( + AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage + ) != AppOpsManager.MODE_ALLOWED + ) { + Log.e(LOG_TAG, "Install from uid $callingUid disallowed by AppOps") + return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR) + } + } + + if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P + && !isPermissionGranted( + context, Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid + ) + && !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid) + ) { + Log.e( + LOG_TAG, "Uid " + callingUid + " does not have " + + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " + + Manifest.permission.DELETE_PACKAGES + ) + return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR) + } + + // Get intent information. + // We expect an intent with URI of the form package:<packageName>#<className> + // className is optional; if specified, it is the activity the user chose to uninstall + val packageUri = intent.data + if (packageUri == null) { + Log.e(LOG_TAG, "No package URI in intent") + return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE) + } + targetPackageName = packageUri.encodedSchemeSpecificPart + if (targetPackageName == null) { + Log.e(LOG_TAG, "Invalid package name in URI: $packageUri") + return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE) + } + + uninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false) + if (uninstallFromAllUsers && !userManager!!.isAdminUser) { + Log.e(LOG_TAG, "Only admin user can request uninstall for all users") + return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) + } + + uninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle::class.java) + if (uninstalledUser == null) { + uninstalledUser = Process.myUserHandle() + } else { + val profiles = userManager!!.userProfiles + if (!profiles.contains(uninstalledUser)) { + Log.e( + LOG_TAG, "User " + Process.myUserHandle() + " can't request uninstall " + + "for user " + uninstalledUser + ) + return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) + } + } + + callback = intent.getParcelableExtra( + PackageInstaller.EXTRA_CALLBACK, PackageManager.UninstallCompleteCallback::class.java + ) + + try { + targetAppInfo = packageManager.getApplicationInfo( + targetPackageName!!, + PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER.toLong()) + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(LOG_TAG, "Unable to get packageName") + } + + if (targetAppInfo == null) { + Log.e(LOG_TAG, "Invalid packageName: $targetPackageName") + return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE) + } + + // The class name may have been specified (e.g. when deleting an app from all apps) + val className = packageUri.fragment + if (className != null) { + try { + targetActivityInfo = packageManager.getActivityInfo( + ComponentName(targetPackageName!!, className), + PackageManager.ComponentInfoFlags.of(0) + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(LOG_TAG, "Unable to get className") + // Continue as the ActivityInfo isn't critical. + } + } + + return UninstallReady() + } + + fun generateUninstallDetails(): UninstallStage { + val messageBuilder = StringBuilder() + + targetAppLabel = targetAppInfo!!.loadSafeLabel(packageManager) + + // If the Activity label differs from the App label, then make sure the user + // knows the Activity belongs to the App being uninstalled. + if (targetActivityInfo != null) { + val activityLabel = targetActivityInfo!!.loadSafeLabel(packageManager) + if (!activityLabel.contentEquals(targetAppLabel)) { + messageBuilder.append( + context.getString(R.string.uninstall_activity_text, activityLabel) + ) + messageBuilder.append(" ").append(targetAppLabel).append(".\n\n") + } + } + + val isUpdate = (targetAppInfo!!.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 + val myUserHandle = Process.myUserHandle() + val isSingleUser = isSingleUser() + + if (isUpdate) { + messageBuilder.append(context.getString( + if (isSingleUser) R.string.uninstall_update_text + else R.string.uninstall_update_text_multiuser + ) + ) + } else if (uninstallFromAllUsers && !isSingleUser) { + messageBuilder.append(context.getString(R.string.uninstall_application_text_all_users)) + } else if (uninstalledUser != myUserHandle) { + // Uninstalling user is issuing uninstall for another user + val customUserManager = context.createContextAsUser(uninstalledUser!!, 0) + .getSystemService(UserManager::class.java) + val userName = customUserManager!!.userName + + val uninstalledUserType = getUninstalledUserType(myUserHandle, uninstalledUser!!) + val messageString: String + when (uninstalledUserType) { + UserManager.USER_TYPE_PROFILE_MANAGED -> { + messageString = context.getString( + R.string.uninstall_application_text_current_user_work_profile, userName + ) + } + + UserManager.USER_TYPE_PROFILE_CLONE -> { + isClonedApp = true + messageString = context.getString( + R.string.uninstall_application_text_current_user_clone_profile + ) + } + + else -> { + messageString = context.getString( + R.string.uninstall_application_text_user, userName + ) + } + + } + messageBuilder.append(messageString) + } else if (isCloneProfile(uninstalledUser!!)) { + isClonedApp = true + messageBuilder.append(context.getString( + R.string.uninstall_application_text_current_user_clone_profile + ) + ) + } else if (myUserHandle == UserHandle.SYSTEM + && hasClonedInstance(targetAppInfo!!.packageName) + ) { + messageBuilder.append(context.getString( + R.string.uninstall_application_text_with_clone_instance, targetAppLabel + ) + ) + } else { + messageBuilder.append(context.getString(R.string.uninstall_application_text)) + } + + val message = messageBuilder.toString() + + val title = if (isClonedApp) { + context.getString(R.string.cloned_app_label, targetAppLabel) + } else { + targetAppLabel.toString() + } + + var suggestToKeepAppData = false + try { + val pkgInfo = packageManager.getPackageInfo(targetPackageName!!, 0) + suggestToKeepAppData = + pkgInfo.applicationInfo != null && pkgInfo.applicationInfo!!.hasFragileUserData() + } catch (e: PackageManager.NameNotFoundException) { + Log.e(LOG_TAG, "Cannot check hasFragileUserData for $targetPackageName", e) + } + + var appDataSize: Long = 0 + if (suggestToKeepAppData) { + appDataSize = getAppDataSize( + targetPackageName!!, + if (uninstallFromAllUsers) null else uninstalledUser + ) + } + + return UninstallUserActionRequired(title, message, appDataSize) + } + + /** + * Returns whether there is only one "full" user on this device. + * + * **Note:** On devices that use [headless system user mode] + * [android.os.UserManager.isHeadlessSystemUserMode], the system user is not "full", + * so it's not be considered in the calculation. + */ + private fun isSingleUser(): Boolean { + val userCount = userManager!!.userCount + return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2) + } + + /** + * Returns the type of the user from where an app is being uninstalled. We are concerned with + * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile + * belong to the same profile group. + */ + private fun getUninstalledUserType( + myUserHandle: UserHandle, + uninstalledUserHandle: UserHandle + ): String? { + if (!userManager!!.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) { + return null + } + val customUserManager = context.createContextAsUser(uninstalledUserHandle, 0) + .getSystemService(UserManager::class.java) + val userTypes = + arrayOf(UserManager.USER_TYPE_PROFILE_MANAGED, UserManager.USER_TYPE_PROFILE_CLONE) + + for (userType in userTypes) { + if (customUserManager!!.isUserOfType(userType)) { + return userType + } + } + return null + } + + private fun hasClonedInstance(packageName: String): Boolean { + // Check if clone user is present on the device. + var cloneUser: UserHandle? = null + val profiles = userManager!!.userProfiles + + for (userHandle in profiles) { + if (userHandle != UserHandle.SYSTEM && isCloneProfile(userHandle)) { + cloneUser = userHandle + break + } + } + // Check if another instance of given package exists in clone user profile. + return try { + cloneUser != null + && packageManager.getPackageUidAsUser( + packageName, PackageManager.PackageInfoFlags.of(0), cloneUser.identifier + ) > 0 + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + + private fun isCloneProfile(userHandle: UserHandle): Boolean { + val customUserManager = context.createContextAsUser(userHandle, 0) + .getSystemService(UserManager::class.java) + return customUserManager!!.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE) + } + + /** + * Get number of bytes of the app data of the package. + * + * @param pkg The package that might have app data. + * @param user The user the package belongs to or `null` if files of all users should + * be counted. + * @return The number of bytes. + */ + private fun getAppDataSize(pkg: String, user: UserHandle?): Long { + if (user != null) { + return getAppDataSizeForUser(pkg, user) + } + // We are uninstalling from all users. Get cumulative app data size for all users. + val userHandles = userManager!!.getUserHandles(true) + var totalAppDataSize: Long = 0 + val numUsers = userHandles.size + for (i in 0 until numUsers) { + totalAppDataSize += getAppDataSizeForUser(pkg, userHandles[i]) + } + return totalAppDataSize + } + + /** + * Get number of bytes of the app data of the package. + * + * @param pkg The package that might have app data. + * @param user The user the package belongs to + * @return The number of bytes. + */ + private fun getAppDataSizeForUser(pkg: String, user: UserHandle): Long { + val storageStatsManager = context.getSystemService(StorageStatsManager::class.java) + try { + val stats = storageStatsManager!!.queryStatsForPackage( + packageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user + ) + return stats.getDataBytes() + } catch (e: Exception) { + Log.e(LOG_TAG, "Cannot determine amount of app data for $pkg", e) + } + return 0 + } + + fun initiateUninstall(keepData: Boolean) { + // Get an uninstallId to track results and show a notification on non-TV devices. + uninstallId = try { + UninstallEventReceiver.addObserver( + context, EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult + ) + } catch (e: OutOfIdsException) { + Log.e(LOG_TAG, "Failed to start uninstall", e) + handleUninstallResult( + PackageInstaller.STATUS_FAILURE, + PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0 + ) + return + } + + // TODO: Check with UX whether to show UninstallUninstalling dialog / notification? + uninstallResult.value = UninstallUninstalling(targetAppLabel, isClonedApp) + + val uninstallData = Bundle() + uninstallData.putInt(EXTRA_UNINSTALL_ID, uninstallId) + uninstallData.putString(EXTRA_PACKAGE_NAME, targetPackageName) + uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, uninstallFromAllUsers) + uninstallData.putCharSequence(EXTRA_APP_LABEL, targetAppLabel) + uninstallData.putBoolean(EXTRA_IS_CLONE_APP, isClonedApp) + Log.i(LOG_TAG, "Uninstalling extras = $uninstallData") + + // Get a PendingIntent for result broadcast and issue an uninstall request + val broadcastIntent = Intent(BROADCAST_ACTION) + broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId) + broadcastIntent.setPackage(context.packageName) + val pendingIntent = PendingIntent.getBroadcast( + context, uninstallId, broadcastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + if (!startUninstall( + targetPackageName!!, uninstalledUser!!, pendingIntent, uninstallFromAllUsers, + keepData + ) + ) { + handleUninstallResult( + PackageInstaller.STATUS_FAILURE, + PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0 + ) + } + } + + private fun handleUninstallResult( + status: Int, + legacyStatus: Int, + message: String?, + serviceId: Int + ) { + if (callback != null) { + // The caller will be informed about the result via a callback + callback!!.onUninstallComplete(targetPackageName!!, legacyStatus, message) + + // Since the caller already received the results, just finish the app at this point + uninstallResult.value = null + return + } + val returnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) + if (returnResult || callingActivity != null) { + val intent = Intent() + intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus) + if (status == PackageInstaller.STATUS_SUCCESS) { + uninstallResult.setValue( + UninstallSuccess(resultIntent = intent, activityResultCode = Activity.RESULT_OK) + ) + } else { + uninstallResult.setValue( + UninstallFailed( + returnResult = true, + resultIntent = intent, + activityResultCode = Activity.RESULT_FIRST_USER + ) + ) + } + return + } + + // Caller did not want the result back. So, we either show a Toast, or a Notification. + if (status == PackageInstaller.STATUS_SUCCESS) { + val statusMessage = if (isClonedApp) context.getString( + R.string.uninstall_done_clone_app, targetAppLabel + ) else context.getString(R.string.uninstall_done_app, targetAppLabel) + uninstallResult.setValue( + UninstallSuccess(activityResultCode = legacyStatus, message = statusMessage) + ) + } else { + val uninstallFailureChannel = NotificationChannel( + UNINSTALL_FAILURE_CHANNEL, + context.getString(R.string.uninstall_failure_notification_channel), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager!!.createNotificationChannel(uninstallFailureChannel) + + val uninstallFailedNotification: Notification.Builder = + Notification.Builder(context, UNINSTALL_FAILURE_CHANNEL) + + val myUserHandle = Process.myUserHandle() + when (legacyStatus) { + PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> { + // Find out if the package is an active admin for some non-current user. + val otherBlockingUserHandle = + findUserOfDeviceAdmin(myUserHandle, targetPackageName!!) + if (otherBlockingUserHandle == null) { + Log.d( + LOG_TAG, "Uninstall failed because $targetPackageName" + + " is a device admin" + ) + addDeviceManagerButton(context, uninstallFailedNotification) + setBigText( + uninstallFailedNotification, context.getString( + R.string.uninstall_failed_device_policy_manager + ) + ) + } else { + Log.d( + LOG_TAG, "Uninstall failed because $targetPackageName" + + " is a device admin of user $otherBlockingUserHandle" + ) + val userName = context.createContextAsUser(otherBlockingUserHandle, 0) + .getSystemService(UserManager::class.java)!!.userName + setBigText( + uninstallFailedNotification, String.format( + context.getString( + R.string.uninstall_failed_device_policy_manager_of_user + ), userName + ) + ) + } + } + + PackageManager.DELETE_FAILED_OWNER_BLOCKED -> { + val otherBlockingUserHandle = findBlockingUser(targetPackageName!!) + val isProfileOfOrSame = isProfileOfOrSame( + userManager!!, myUserHandle, otherBlockingUserHandle + ) + if (isProfileOfOrSame) { + addDeviceManagerButton(context, uninstallFailedNotification) + } else { + addManageUsersButton(context, uninstallFailedNotification) + } + var bigText: String? = null + if (otherBlockingUserHandle == null) { + Log.d( + LOG_TAG, "Uninstall failed for $targetPackageName " + + "with code $status no blocking user" + ) + } else if (otherBlockingUserHandle === UserHandle.SYSTEM) { + bigText = context.getString(R.string.uninstall_blocked_device_owner) + } else { + bigText = context.getString( + if (uninstallFromAllUsers) R.string.uninstall_all_blocked_profile_owner + else R.string.uninstall_blocked_profile_owner + ) + } + bigText?.let { setBigText(uninstallFailedNotification, it) } + } + + else -> { + Log.d( + LOG_TAG, "Uninstall blocked for $targetPackageName" + + " with legacy code $legacyStatus" + ) + } + } + uninstallFailedNotification.setContentTitle( + context.getString(R.string.uninstall_failed_app, targetAppLabel) + ) + uninstallFailedNotification.setOngoing(false) + uninstallFailedNotification.setSmallIcon(R.drawable.ic_error) + + uninstallResult.setValue( + UninstallFailed( + returnResult = false, + uninstallNotificationId = uninstallId, + uninstallNotification = uninstallFailedNotification.build() + ) + ) + } + } + + /** + * @param myUserHandle [UserHandle] of the current user. + * @param packageName Name of the package being uninstalled. + * @return the [UserHandle] of the user in which a package is a device admin. + */ + private fun findUserOfDeviceAdmin(myUserHandle: UserHandle, packageName: String): UserHandle? { + for (otherUserHandle in userManager!!.getUserHandles(true)) { + // We only catch the case when the user in question is neither the + // current user nor its profile. + if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) { + continue + } + val dpm = context.createContextAsUser(otherUserHandle, 0) + .getSystemService(DevicePolicyManager::class.java) + if (dpm!!.packageHasActiveAdmins(packageName)) { + return otherUserHandle + } + } + return null + } + + /** + * + * @param packageName Name of the package being uninstalled. + * @return [UserHandle] of the user in which a package is blocked from being uninstalled. + */ + private fun findBlockingUser(packageName: String): UserHandle? { + for (otherUserHandle in userManager!!.getUserHandles(true)) { + // TODO (b/307399586): Add a negation when the logic of the method is fixed + if (packageManager.canUserUninstall(packageName, otherUserHandle)) { + return otherUserHandle + } + } + return null + } + + /** + * Set big text for the notification. + * + * @param builder The builder of the notification + * @param text The text to set. + */ + private fun setBigText( + builder: Notification.Builder, + text: CharSequence + ) { + builder.setStyle(Notification.BigTextStyle().bigText(text)) + } + + /** + * Add a button to the notification that links to the user management. + * + * @param context The context the notification is created in + * @param builder The builder of the notification + */ + private fun addManageUsersButton( + context: Context, + builder: Notification.Builder + ) { + builder.addAction( + Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_settings_multiuser), + context.getString(R.string.manage_users), + PendingIntent.getActivity( + context, 0, getUserSettingsIntent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + ) + } + + private fun getUserSettingsIntent(): Intent { + val intent = Intent(Settings.ACTION_USER_SETTINGS) + intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK) + return intent + } + + /** + * Add a button to the notification that links to the device policy management. + * + * @param context The context the notification is created in + * @param builder The builder of the notification + */ + private fun addDeviceManagerButton( + context: Context, + builder: Notification.Builder + ) { + builder.addAction( + Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_lock), + context.getString(R.string.manage_device_administrators), + PendingIntent.getActivity( + context, 0, getDeviceManagerIntent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + ) + } + + private fun getDeviceManagerIntent(): Intent { + val intent = Intent() + intent.setClassName( + "com.android.settings", + "com.android.settings.Settings\$DeviceAdminSettingsActivity" + ) + intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK) + return intent + } + + /** + * Starts an uninstall for the given package. + * + * @return `true` if there was no exception while uninstalling. This does not represent + * the result of the uninstall. Result will be made available in [handleUninstallResult] + */ + private fun startUninstall( + packageName: String, + targetUser: UserHandle, + pendingIntent: PendingIntent, + uninstallFromAllUsers: Boolean, + keepData: Boolean + ): Boolean { + var flags = if (uninstallFromAllUsers) PackageManager.DELETE_ALL_USERS else 0 + flags = flags or if (keepData) PackageManager.DELETE_KEEP_DATA else 0 + + return try { + context.createContextAsUser(targetUser, 0) + .packageManager.packageInstaller.uninstall( + VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), + flags, pendingIntent.intentSender + ) + true + } catch (e: IllegalArgumentException) { + Log.e(LOG_TAG, "Failed to uninstall", e) + false + } + } + + fun cancelInstall() { + if (callback != null) { + callback!!.onUninstallComplete( + targetPackageName!!, + PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user" + ) + } + } + + companion object { + private val LOG_TAG = UninstallRepository::class.java.simpleName + private const val UNINSTALL_FAILURE_CHANNEL = "uninstall_failure" + private const val BROADCAST_ACTION = "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT" + private const val EXTRA_UNINSTALL_ID = "com.android.packageinstaller.extra.UNINSTALL_ID" + private const val EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL" + private const val EXTRA_IS_CLONE_APP = "com.android.packageinstaller.extra.IS_CLONE_APP" + private const val EXTRA_PACKAGE_NAME = + "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME" + } + + class CallerInfo(val activityName: String?, val uid: Int) +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt new file mode 100644 index 000000000000..f086209fe498 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 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 + * + * https://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.v2.model + +import android.app.Activity +import android.app.Notification +import android.content.Intent +import com.android.packageinstaller.R + +sealed class UninstallStage(val stageCode: Int) { + + companion object { + const val STAGE_DEFAULT = -1 + const val STAGE_ABORTED = 0 + const val STAGE_READY = 1 + const val STAGE_USER_ACTION_REQUIRED = 2 + const val STAGE_UNINSTALLING = 3 + const val STAGE_SUCCESS = 4 + const val STAGE_FAILED = 5 + } +} + +class UninstallReady : UninstallStage(STAGE_READY) + +data class UninstallUserActionRequired( + val title: String? = null, + val message: String? = null, + val appDataSize: Long = 0 +) : UninstallStage(STAGE_USER_ACTION_REQUIRED) + +data class UninstallUninstalling(val appLabel: CharSequence, val isCloneUser: Boolean) : + UninstallStage(STAGE_UNINSTALLING) + +data class UninstallSuccess( + val resultIntent: Intent? = null, + val activityResultCode: Int = 0, + val message: String? = null, +) : UninstallStage(STAGE_SUCCESS) + +data class UninstallFailed( + val returnResult: Boolean, + /** + * If the caller wants the result back, the intent will hold the uninstall failure status code + * and legacy code. + */ + val resultIntent: Intent? = null, + val activityResultCode: Int = Activity.RESULT_CANCELED, + /** + * ID used to show [uninstallNotification] + */ + val uninstallNotificationId: Int? = null, + /** + * When the user does not request a result back, this notification will be shown indicating the + * reason for uninstall failure. + */ + val uninstallNotification: Notification? = null, +) : UninstallStage(STAGE_FAILED) { + + init { + if (uninstallNotification != null && uninstallNotificationId == null) { + throw IllegalArgumentException( + "uninstallNotification cannot be set without uninstallNotificationId" + ) + } + } +} + +data class UninstallAborted(val abortReason: Int) : UninstallStage(STAGE_ABORTED) { + + var dialogTitleResource = 0 + var dialogTextResource = 0 + val activityResultCode = Activity.RESULT_FIRST_USER + + init { + when (abortReason) { + ABORT_REASON_APP_UNAVAILABLE -> { + dialogTitleResource = R.string.app_not_found_dlg_title + dialogTextResource = R.string.app_not_found_dlg_text + } + + ABORT_REASON_USER_NOT_ALLOWED -> { + dialogTitleResource = 0 + dialogTextResource = R.string.user_is_not_allowed_dlg_text + } + + else -> { + dialogTitleResource = 0 + dialogTextResource = R.string.generic_error_dlg_text + } + } + } + + companion object { + const val ABORT_REASON_GENERIC_ERROR = 0 + const val ABORT_REASON_APP_UNAVAILABLE = 1 + const val ABORT_REASON_USER_NOT_ALLOWED = 2 + } +} + diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java deleted file mode 100644 index 520b6c573acf..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - - -import android.app.Activity; -import android.content.Intent; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class InstallAborted extends InstallStage { - - public static final int ABORT_REASON_INTERNAL_ERROR = 0; - public static final int ABORT_REASON_POLICY = 1; - public static final int ABORT_REASON_DONE = 2; - public static final int DLG_PACKAGE_ERROR = 1; - private final int mStage = InstallStage.STAGE_ABORTED; - private final int mAbortReason; - - /** - * It will hold the restriction name, when the restriction was enforced by the system, and not - * a device admin. - */ - @NonNull - private final String mMessage; - /** - * <p>If abort reason is ABORT_REASON_POLICY, then this will hold the Intent - * to display a support dialog when a feature was disabled by an admin. It will be - * {@code null} if the feature is disabled by the system. In this case, the restriction name - * will be set in {@link #mMessage} </p> - * - * <p>If the abort reason is ABORT_REASON_INTERNAL_ERROR, it <b>may</b> hold an - * intent to be sent as a result to the calling activity.</p> - */ - @Nullable - private final Intent mIntent; - private final int mErrorDialogType; - private final int mActivityResultCode; - - private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent, - int activityResultCode, int errorDialogType) { - mAbortReason = reason; - mMessage = message; - mIntent = intent; - mErrorDialogType = errorDialogType; - mActivityResultCode = activityResultCode; - } - - public int getAbortReason() { - return mAbortReason; - } - - @NonNull - public String getMessage() { - return mMessage; - } - - @Nullable - public Intent getResultIntent() { - return mIntent; - } - - public int getErrorDialogType() { - return mErrorDialogType; - } - - public int getActivityResultCode() { - return mActivityResultCode; - } - - @Override - public int getStageCode() { - return mStage; - } - - public static class Builder { - - private final int mAbortReason; - private String mMessage = ""; - private Intent mIntent = null; - private int mActivityResultCode = Activity.RESULT_CANCELED; - private int mErrorDialogType; - - public Builder(int reason) { - mAbortReason = reason; - } - - public Builder setMessage(@NonNull String message) { - mMessage = message; - return this; - } - - public Builder setResultIntent(@NonNull Intent intent) { - mIntent = intent; - return this; - } - - public Builder setErrorDialogType(int dialogType) { - mErrorDialogType = dialogType; - return this; - } - - public Builder setActivityResultCode(int resultCode) { - mActivityResultCode = resultCode; - return this; - } - - public InstallAborted build() { - return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode, - mErrorDialogType); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java deleted file mode 100644 index 67e169036551..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; - -public class InstallFailed extends InstallStage { - - private final int mStage = InstallStage.STAGE_FAILED; - @NonNull - private final AppSnippet mAppSnippet; - private final int mStatusCode; - private final int mLegacyCode; - @Nullable - private final String mMessage; - - public InstallFailed(@NonNull AppSnippet appSnippet, int statusCode, int legacyCode, - @Nullable String message) { - mAppSnippet = appSnippet; - mLegacyCode = statusCode; - mStatusCode = legacyCode; - mMessage = message; - } - - @Override - public int getStageCode() { - return mStage; - } - - @NonNull - public Drawable getAppIcon() { - return mAppSnippet.getIcon(); - } - - @NonNull - public String getAppLabel() { - return (String) mAppSnippet.getLabel(); - } - - public int getStatusCode() { - return mStatusCode; - } - - public int getLegacyCode() { - return mLegacyCode; - } - - @Nullable - public String getMessage() { - return mMessage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java deleted file mode 100644 index efd4947f712f..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; - -public class InstallInstalling extends InstallStage { - - private final int mStage = InstallStage.STAGE_INSTALLING; - @NonNull - private final AppSnippet mAppSnippet; - - public InstallInstalling(@NonNull AppSnippet appSnippet) { - mAppSnippet = appSnippet; - } - - @Override - public int getStageCode() { - return mStage; - } - - @NonNull - public Drawable getAppIcon() { - return mAppSnippet.getIcon(); - } - - @NonNull - public String getAppLabel() { - return (String) mAppSnippet.getLabel(); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java deleted file mode 100644 index 548f2c544da7..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.installstagedata; - -public class InstallReady extends InstallStage{ - - private final int mStage = InstallStage.STAGE_READY; - - @Override - public int getStageCode() { - return mStage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java deleted file mode 100644 index f91e64bdc326..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -public abstract class InstallStage { - - public static final int STAGE_DEFAULT = -1; - public static final int STAGE_ABORTED = 0; - public static final int STAGE_STAGING = 1; - public static final int STAGE_READY = 2; - public static final int STAGE_USER_ACTION_REQUIRED = 3; - public static final int STAGE_INSTALLING = 4; - public static final int STAGE_SUCCESS = 5; - public static final int STAGE_FAILED = 6; - - /** - * @return the integer value representing current install stage. - */ - public abstract int getStageCode(); -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java deleted file mode 100644 index a979cf87c350..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -public class InstallStaging extends InstallStage { - - private final int mStage = InstallStage.STAGE_STAGING; - - @Override - public int getStageCode() { - return mStage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java deleted file mode 100644 index da482564c505..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; - -public class InstallSuccess extends InstallStage { - - private final int mStage = InstallStage.STAGE_SUCCESS; - - @NonNull - private final AppSnippet mAppSnippet; - private final boolean mShouldReturnResult; - /** - * <p>If the caller is requesting a result back, this will hold the Intent with - * EXTRA_INSTALL_RESULT set to INSTALL_SUCCEEDED which is sent back to the caller.</p> - * <p>If the caller doesn't want the result back, this will hold the Intent that launches - * the newly installed / updated app.</p> - */ - @NonNull - private final Intent mResultIntent; - - public InstallSuccess(@NonNull AppSnippet appSnippet, boolean shouldReturnResult, - @NonNull Intent launcherIntent) { - mAppSnippet = appSnippet; - mShouldReturnResult = shouldReturnResult; - mResultIntent = launcherIntent; - } - - @Override - public int getStageCode() { - return mStage; - } - - @NonNull - public Drawable getAppIcon() { - return mAppSnippet.getIcon(); - } - - @NonNull - public String getAppLabel() { - return (String) mAppSnippet.getLabel(); - } - - public boolean shouldReturnResult() { - return mShouldReturnResult; - } - - @NonNull - public Intent getResultIntent() { - return mResultIntent; - } - - public static class Builder { - - private final AppSnippet mAppSnippet; - private boolean mShouldReturnResult; - private Intent mLauncherIntent; - - public Builder(@NonNull AppSnippet appSnippet) { - mAppSnippet = appSnippet; - } - - public Builder setShouldReturnResult(boolean returnResult) { - mShouldReturnResult = returnResult; - return this; - } - - public Builder setResultIntent(@NonNull Intent intent) { - mLauncherIntent = intent; - return this; - } - - public InstallSuccess build() { - return new InstallSuccess(mAppSnippet, mShouldReturnResult, mLauncherIntent); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java deleted file mode 100644 index 08a7487c69d3..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.installstagedata; - -import android.graphics.drawable.Drawable; -import androidx.annotation.Nullable; -import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; - -public class InstallUserActionRequired extends InstallStage { - - public static final int USER_ACTION_REASON_UNKNOWN_SOURCE = 0; - public static final int USER_ACTION_REASON_ANONYMOUS_SOURCE = 1; - public static final int USER_ACTION_REASON_INSTALL_CONFIRMATION = 2; - private final int mStage = InstallStage.STAGE_USER_ACTION_REQUIRED; - private final int mActionReason; - @Nullable - private final AppSnippet mAppSnippet; - private final boolean mIsAppUpdating; - @Nullable - private final String mDialogMessage; - - public InstallUserActionRequired(int actionReason, @Nullable AppSnippet appSnippet, - boolean isUpdating, @Nullable String dialogMessage) { - mActionReason = actionReason; - mAppSnippet = appSnippet; - mIsAppUpdating = isUpdating; - mDialogMessage = dialogMessage; - } - - @Override - public int getStageCode() { - return mStage; - } - - @Nullable - public Drawable getAppIcon() { - return mAppSnippet != null ? mAppSnippet.getIcon() : null; - } - - @Nullable - public String getAppLabel() { - return mAppSnippet != null ? (String) mAppSnippet.getLabel() : null; - } - - public boolean isAppUpdating() { - return mIsAppUpdating; - } - - @Nullable - public String getDialogMessage() { - return mDialogMessage; - } - - public int getActionReason() { - return mActionReason; - } - - public static class Builder { - - private final int mActionReason; - private final AppSnippet mAppSnippet; - private boolean mIsAppUpdating; - private String mDialogMessage; - - public Builder(int actionReason, @Nullable AppSnippet appSnippet) { - mActionReason = actionReason; - mAppSnippet = appSnippet; - } - - public Builder setAppUpdating(boolean isUpdating) { - mIsAppUpdating = isUpdating; - return this; - } - - public Builder setDialogMessage(@Nullable String message) { - mDialogMessage = message; - return this; - } - - public InstallUserActionRequired build() { - return new InstallUserActionRequired(mActionReason, mAppSnippet, mIsAppUpdating, - mDialogMessage); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java deleted file mode 100644 index 9aea6b18214b..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -import android.app.Activity; -import com.android.packageinstaller.R; - -public class UninstallAborted extends UninstallStage { - - public static final int ABORT_REASON_GENERIC_ERROR = 0; - public static final int ABORT_REASON_APP_UNAVAILABLE = 1; - public static final int ABORT_REASON_USER_NOT_ALLOWED = 2; - private final int mStage = UninstallStage.STAGE_ABORTED; - private final int mAbortReason; - private final int mDialogTitleResource; - private final int mDialogTextResource; - private final int mActivityResultCode = Activity.RESULT_FIRST_USER; - - public UninstallAborted(int abortReason) { - mAbortReason = abortReason; - switch (abortReason) { - case ABORT_REASON_APP_UNAVAILABLE -> { - mDialogTitleResource = R.string.app_not_found_dlg_title; - mDialogTextResource = R.string.app_not_found_dlg_text; - } - case ABORT_REASON_USER_NOT_ALLOWED -> { - mDialogTitleResource = 0; - mDialogTextResource = R.string.user_is_not_allowed_dlg_text; - } - default -> { - mDialogTitleResource = 0; - mDialogTextResource = R.string.generic_error_dlg_text; - } - } - } - - public int getAbortReason() { - return mAbortReason; - } - - public int getActivityResultCode() { - return mActivityResultCode; - } - - public int getDialogTitleResource() { - return mDialogTitleResource; - } - - public int getDialogTextResource() { - return mDialogTextResource; - } - - @Override - public int getStageCode() { - return mStage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java deleted file mode 100644 index 6ed8883570e3..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -import android.app.Activity; -import android.app.Notification; -import android.content.Intent; - -public class UninstallFailed extends UninstallStage { - - private final int mStage = UninstallStage.STAGE_FAILED; - private final boolean mReturnResult; - /** - * If the caller wants the result back, the intent will hold the uninstall failure status code - * and legacy code. - */ - private final Intent mResultIntent; - /** - * When the user does not request a result back, this notification will be shown indicating the - * reason for uninstall failure. - */ - private final Notification mUninstallNotification; - /** - * ID used to show {@link #mUninstallNotification} - */ - private final int mUninstallId; - private final int mActivityResultCode; - - public UninstallFailed(boolean returnResult, Intent resultIntent, int activityResultCode, - int uninstallId, Notification uninstallNotification) { - mReturnResult = returnResult; - mResultIntent = resultIntent; - mActivityResultCode = activityResultCode; - mUninstallId = uninstallId; - mUninstallNotification = uninstallNotification; - } - - public boolean returnResult() { - return mReturnResult; - } - - public Intent getResultIntent() { - return mResultIntent; - } - - public int getActivityResultCode() { - return mActivityResultCode; - } - - public Notification getUninstallNotification() { - return mUninstallNotification; - } - - public int getUninstallId() { - return mUninstallId; - } - - @Override - public int getStageCode() { - return mStage; - } - - public static class Builder { - - private final boolean mReturnResult; - private int mActivityResultCode = Activity.RESULT_CANCELED; - /** - * See {@link UninstallFailed#mResultIntent} - */ - private Intent mResultIntent = null; - /** - * See {@link UninstallFailed#mUninstallNotification} - */ - private Notification mUninstallNotification; - /** - * See {@link UninstallFailed#mUninstallId} - */ - private int mUninstallId; - - public Builder(boolean returnResult) { - mReturnResult = returnResult; - } - - public Builder setUninstallNotification(int uninstallId, Notification notification) { - mUninstallId = uninstallId; - mUninstallNotification = notification; - return this; - } - - public Builder setResultIntent(Intent intent) { - mResultIntent = intent; - return this; - } - - public Builder setActivityResultCode(int resultCode) { - mActivityResultCode = resultCode; - return this; - } - - public UninstallFailed build() { - return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode, - mUninstallId, mUninstallNotification); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java deleted file mode 100644 index 0108cb471b5a..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -public class UninstallReady extends UninstallStage { - - private final int mStage = UninstallStage.STAGE_READY; - - @Override - public int getStageCode() { - return mStage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java deleted file mode 100644 index 87ca4ec37349..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.model.uninstallstagedata; - -public abstract class UninstallStage { - - public static final int STAGE_DEFAULT = -1; - public static final int STAGE_ABORTED = 0; - public static final int STAGE_READY = 1; - public static final int STAGE_USER_ACTION_REQUIRED = 2; - public static final int STAGE_UNINSTALLING = 3; - public static final int STAGE_SUCCESS = 4; - public static final int STAGE_FAILED = 5; - - public abstract int getStageCode(); -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java deleted file mode 100644 index 5df6b020cef5..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -import android.content.Intent; - -public class UninstallSuccess extends UninstallStage { - - private final int mStage = UninstallStage.STAGE_SUCCESS; - private final String mMessage; - private final Intent mResultIntent; - private final int mActivityResultCode; - - public UninstallSuccess(Intent resultIntent, int activityResultCode, String message) { - mResultIntent = resultIntent; - mActivityResultCode = activityResultCode; - mMessage = message; - } - - public String getMessage() { - return mMessage; - } - - public Intent getResultIntent() { - return mResultIntent; - } - - public int getActivityResultCode() { - return mActivityResultCode; - } - - @Override - public int getStageCode() { - return mStage; - } - - public static class Builder { - - private Intent mResultIntent; - private int mActivityResultCode; - private String mMessage; - - public Builder() { - } - - public Builder setResultIntent(Intent intent) { - mResultIntent = intent; - return this; - } - - public Builder setActivityResultCode(int resultCode) { - mActivityResultCode = resultCode; - return this; - } - - public Builder setMessage(String message) { - mMessage = message; - return this; - } - - public UninstallSuccess build() { - return new UninstallSuccess(mResultIntent, mActivityResultCode, mMessage); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java deleted file mode 100644 index f5156cb676e9..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -public class UninstallUninstalling extends UninstallStage { - - private final int mStage = UninstallStage.STAGE_UNINSTALLING; - - private final CharSequence mAppLabel; - private final boolean mIsCloneUser; - - public UninstallUninstalling(CharSequence appLabel, boolean isCloneUser) { - mAppLabel = appLabel; - mIsCloneUser = isCloneUser; - } - - public CharSequence getAppLabel() { - return mAppLabel; - } - - public boolean isCloneUser() { - return mIsCloneUser; - } - - @Override - public int getStageCode() { - return mStage; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java deleted file mode 100644 index b6001493ade9..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.model.uninstallstagedata; - -public class UninstallUserActionRequired extends UninstallStage { - - private final int mStage = UninstallStage.STAGE_USER_ACTION_REQUIRED; - private final String mTitle; - private final String mMessage; - private final long mAppDataSize; - - public UninstallUserActionRequired(String title, String message, long appDataSize) { - mTitle = title; - mMessage = message; - mAppDataSize = appDataSize; - } - - public String getTitle() { - return mTitle; - } - - public String getMessage() { - return mMessage; - } - - public long getAppDataSize() { - return mAppDataSize; - } - - @Override - public int getStageCode() { - return mStage; - } - - public static class Builder { - - private String mTitle; - private String mMessage; - private long mAppDataSize = 0; - - public Builder setTitle(String title) { - mTitle = title; - return this; - } - - public Builder setMessage(String message) { - mMessage = message; - return this; - } - - public Builder setAppDataSize(long appDataSize) { - mAppDataSize = appDataSize; - return this; - } - - public UninstallUserActionRequired build() { - return new UninstallUserActionRequired(mTitle, mMessage, mAppDataSize); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt index fdb024ffc23e..c109fc673ec4 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt @@ -14,25 +14,28 @@ * limitations under the License. */ -package com.android.packageinstaller.v2.ui; +package com.android.packageinstaller.v2.ui -import android.content.Intent; +import android.content.Intent -public interface InstallActionListener { +interface InstallActionListener { + /** + * Method to handle a positive response from the user. + */ + fun onPositiveResponse(reasonCode: Int) /** - * Method to handle a positive response from the user + * Method to dispatch intent for toggling "install from unknown sources" setting for a package. */ - void onPositiveResponse(int stageCode); + fun sendUnknownAppsIntent(sourcePackageName: String) /** - * Method to dispatch intent for toggling "install from unknown sources" setting for a package + * Method to handle a negative response from the user. */ - void sendUnknownAppsIntent(String packageName); + fun onNegativeResponse(stageCode: Int) /** - * Method to handle a negative response from the user + * Launch the intent to open the newly installed / updated app. */ - void onNegativeResponse(int stageCode); - void openInstalledApp(Intent intent); + fun openInstalledApp(intent: Intent?) } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java deleted file mode 100644 index d06b4b3b1336..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.ui; - -import static android.content.Intent.CATEGORY_LAUNCHER; -import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY; -import static android.os.Process.INVALID_UID; -import static com.android.packageinstaller.v2.model.InstallRepository.EXTRA_STAGED_SESSION_ID; - -import android.app.Activity; -import android.app.AppOpsManager; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.UserManager; -import android.provider.Settings; -import android.util.Log; -import android.view.Window; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.lifecycle.ViewModelProvider; -import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.InstallRepository; -import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo; -import com.android.packageinstaller.v2.model.installstagedata.InstallAborted; -import com.android.packageinstaller.v2.model.installstagedata.InstallFailed; -import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; -import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess; -import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired; -import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment; -import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment; -import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment; -import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment; -import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment; -import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment; -import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment; -import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment; -import com.android.packageinstaller.v2.viewmodel.InstallViewModel; -import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory; -import java.util.ArrayList; -import java.util.List; - -public class InstallLaunch extends FragmentActivity implements InstallActionListener { - - public static final String EXTRA_CALLING_PKG_UID = - InstallLaunch.class.getPackageName() + ".callingPkgUid"; - public static final String EXTRA_CALLING_PKG_NAME = - InstallLaunch.class.getPackageName() + ".callingPkgName"; - private static final String TAG = InstallLaunch.class.getSimpleName(); - private static final String TAG_DIALOG = "dialog"; - private final int REQUEST_TRUST_EXTERNAL_SOURCE = 1; - private final boolean mLocalLOGV = false; - /** - * A collection of unknown sources listeners that are actively listening for app ops mode - * changes - */ - private final List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1); - private InstallViewModel mInstallViewModel; - private InstallRepository mInstallRepository; - private FragmentManager mFragmentManager; - private AppOpsManager mAppOpsManager; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.requestWindowFeature(Window.FEATURE_NO_TITLE); - - mFragmentManager = getSupportFragmentManager(); - mAppOpsManager = getSystemService(AppOpsManager.class); - - mInstallRepository = new InstallRepository(getApplicationContext()); - mInstallViewModel = new ViewModelProvider(this, - new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get( - InstallViewModel.class); - - Intent intent = getIntent(); - CallerInfo info = new CallerInfo( - intent.getStringExtra(EXTRA_CALLING_PKG_NAME), - intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID)); - mInstallViewModel.preprocessIntent(intent, info); - - mInstallViewModel.getCurrentInstallStage().observe(this, this::onInstallStageChange); - } - - /** - * Main controller of the UI. This method shows relevant dialogs based on the install stage - */ - private void onInstallStageChange(InstallStage installStage) { - switch (installStage.getStageCode()) { - case InstallStage.STAGE_STAGING -> { - InstallStagingFragment stagingDialog = new InstallStagingFragment(); - showDialogInner(stagingDialog); - mInstallViewModel.getStagingProgress().observe(this, stagingDialog::setProgress); - } - case InstallStage.STAGE_ABORTED -> { - InstallAborted aborted = (InstallAborted) installStage; - switch (aborted.getAbortReason()) { - // TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR - case InstallAborted.ABORT_REASON_DONE, - InstallAborted.ABORT_REASON_INTERNAL_ERROR -> - setResult(aborted.getActivityResultCode(), aborted.getResultIntent(), true); - case InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted); - default -> setResult(RESULT_CANCELED, null, true); - } - } - case InstallStage.STAGE_USER_ACTION_REQUIRED -> { - InstallUserActionRequired uar = (InstallUserActionRequired) installStage; - switch (uar.getActionReason()) { - case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> { - InstallConfirmationFragment actionDialog = - new InstallConfirmationFragment(uar); - showDialogInner(actionDialog); - } - case InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> { - ExternalSourcesBlockedFragment externalSourceDialog = - new ExternalSourcesBlockedFragment(uar); - showDialogInner(externalSourceDialog); - } - case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> { - AnonymousSourceFragment anonymousSourceDialog = - new AnonymousSourceFragment(); - showDialogInner(anonymousSourceDialog); - } - } - } - case InstallStage.STAGE_INSTALLING -> { - InstallInstalling installing = (InstallInstalling) installStage; - InstallInstallingFragment installingDialog = - new InstallInstallingFragment(installing); - showDialogInner(installingDialog); - } - case InstallStage.STAGE_SUCCESS -> { - InstallSuccess success = (InstallSuccess) installStage; - if (success.shouldReturnResult()) { - Intent successIntent = success.getResultIntent(); - setResult(Activity.RESULT_OK, successIntent, true); - } else { - InstallSuccessFragment successFragment = new InstallSuccessFragment(success); - showDialogInner(successFragment); - } - } - case InstallStage.STAGE_FAILED -> { - InstallFailed failed = (InstallFailed) installStage; - InstallFailedFragment failedDialog = new InstallFailedFragment(failed); - showDialogInner(failedDialog); - } - default -> { - Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode()); - showDialogInner(null); - } - } - } - - private void showPolicyRestrictionDialog(InstallAborted aborted) { - String restriction = aborted.getMessage(); - Intent adminSupportIntent = aborted.getResultIntent(); - boolean shouldFinish; - - // If the given restriction is set by an admin, display information about the - // admin enforcing the restriction for the affected user. If not enforced by the admin, - // show the system dialog. - if (adminSupportIntent != null) { - if (mLocalLOGV) { - Log.i(TAG, "Restriction set by admin, starting " + adminSupportIntent); - } - startActivity(adminSupportIntent); - // Finish the package installer app since the next dialog will not be shown by this app - shouldFinish = true; - } else { - if (mLocalLOGV) { - Log.i(TAG, "Restriction set by system: " + restriction); - } - DialogFragment blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction); - // Don't finish the package installer app since the next dialog - // will be shown by this app - shouldFinish = false; - showDialogInner(blockedByPolicyDialog); - } - setResult(RESULT_CANCELED, null, shouldFinish); - } - - /** - * Create a new dialog based on the install restriction enforced. - * - * @param restriction The restriction to create the dialog for - * @return The dialog - */ - private DialogFragment createDevicePolicyRestrictionDialog(String restriction) { - if (mLocalLOGV) { - Log.i(TAG, "createDialog(" + restriction + ")"); - } - return switch (restriction) { - case UserManager.DISALLOW_INSTALL_APPS -> - new SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text); - case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY -> - new SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text); - default -> null; - }; - } - - /** - * Replace any visible dialog by the dialog returned by InstallRepository - * - * @param newDialog The new dialog to display - */ - private void showDialogInner(@Nullable DialogFragment newDialog) { - DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag( - TAG_DIALOG); - if (currentDialog != null) { - currentDialog.dismissAllowingStateLoss(); - } - if (newDialog != null) { - newDialog.show(mFragmentManager, TAG_DIALOG); - } - } - - public void setResult(int resultCode, Intent data, boolean shouldFinish) { - super.setResult(resultCode, data); - if (shouldFinish) { - finish(); - } - } - - @Override - public void onPositiveResponse(int reasonCode) { - switch (reasonCode) { - case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> - mInstallViewModel.forcedSkipSourceCheck(); - case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> - mInstallViewModel.initiateInstall(); - } - } - - @Override - public void onNegativeResponse(int stageCode) { - if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) { - mInstallViewModel.cleanupInstall(); - } - setResult(Activity.RESULT_CANCELED, null, true); - } - - @Override - public void sendUnknownAppsIntent(String sourcePackageName) { - Intent settingsIntent = new Intent(); - settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); - final Uri packageUri = Uri.parse("package:" + sourcePackageName); - settingsIntent.setData(packageUri); - settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY); - - try { - registerAppOpChangeListener(new UnknownSourcesListener(sourcePackageName), - sourcePackageName); - startActivityForResult(settingsIntent, REQUEST_TRUST_EXTERNAL_SOURCE); - } catch (ActivityNotFoundException exc) { - Log.e(TAG, "Settings activity not found for action: " - + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); - } - } - - @Override - public void openInstalledApp(Intent intent) { - setResult(RESULT_OK, intent, true); - if (intent != null && intent.hasCategory(CATEGORY_LAUNCHER)) { - startActivity(intent); - } - } - - private void registerAppOpChangeListener(UnknownSourcesListener listener, String packageName) { - mAppOpsManager.startWatchingMode( - AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, packageName, - listener); - mActiveUnknownSourcesListeners.add(listener); - } - - private void unregisterAppOpChangeListener(UnknownSourcesListener listener) { - mActiveUnknownSourcesListeners.remove(listener); - mAppOpsManager.stopWatchingMode(listener); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_TRUST_EXTERNAL_SOURCE) { - mInstallViewModel.reattemptInstall(); - } else { - setResult(Activity.RESULT_CANCELED, null, true); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - while (!mActiveUnknownSourcesListeners.isEmpty()) { - unregisterAppOpChangeListener(mActiveUnknownSourcesListeners.get(0)); - } - } - - private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener { - - private final String mOriginatingPackage; - - public UnknownSourcesListener(String originatingPackage) { - mOriginatingPackage = originatingPackage; - } - - @Override - public void onOpChanged(String op, String packageName) { - if (!mOriginatingPackage.equals(packageName)) { - return; - } - unregisterAppOpChangeListener(this); - mActiveUnknownSourcesListeners.remove(this); - if (isDestroyed()) { - return; - } - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (!isDestroyed()) { - // Relaunch Pia to continue installation. - startActivity(getIntent() - .putExtra(EXTRA_STAGED_SESSION_ID, mInstallViewModel.getStagedSessionId())); - - // If the userId of the root of activity stack is different from current userId, - // starting Pia again lead to duplicate instances of the app in the stack. - // As such, finish the old instance. Old Pia is finished even if the userId of - // the root is the same, since there is no way to determine the difference in - // userIds. - finish(); - } - }, 500); - } - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt new file mode 100644 index 000000000000..2b610d7b06f5 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2023 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.v2.ui + +import android.app.Activity +import android.app.AppOpsManager +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.os.UserManager +import android.provider.Settings +import android.util.Log +import android.view.Window +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.ViewModelProvider +import com.android.packageinstaller.R +import com.android.packageinstaller.v2.model.InstallRepository +import com.android.packageinstaller.v2.model.InstallAborted +import com.android.packageinstaller.v2.model.InstallFailed +import com.android.packageinstaller.v2.model.InstallInstalling +import com.android.packageinstaller.v2.model.InstallStage +import com.android.packageinstaller.v2.model.InstallSuccess +import com.android.packageinstaller.v2.model.InstallUserActionRequired +import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment +import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment +import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment +import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment +import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment +import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment +import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment +import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment +import com.android.packageinstaller.v2.viewmodel.InstallViewModel +import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory + +class InstallLaunch : FragmentActivity(), InstallActionListener { + + companion object { + @JvmField val EXTRA_CALLING_PKG_UID = + InstallLaunch::class.java.packageName + ".callingPkgUid" + @JvmField val EXTRA_CALLING_PKG_NAME = + InstallLaunch::class.java.packageName + ".callingPkgName" + private val LOG_TAG = InstallLaunch::class.java.simpleName + private const val TAG_DIALOG = "dialog" + } + + private val localLOGV = false + + /** + * A collection of unknown sources listeners that are actively listening for app ops mode + * changes + */ + private val activeUnknownSourcesListeners: MutableList<UnknownSourcesListener> = ArrayList(1) + private var installViewModel: InstallViewModel? = null + private var installRepository: InstallRepository? = null + private var fragmentManager: FragmentManager? = null + private var appOpsManager: AppOpsManager? = null + private lateinit var unknownAppsIntentLauncher: ActivityResultLauncher<Intent> + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + fragmentManager = supportFragmentManager + appOpsManager = getSystemService(AppOpsManager::class.java) + installRepository = InstallRepository(applicationContext) + installViewModel = ViewModelProvider( + this, InstallViewModelFactory(this.application, installRepository!!) + )[InstallViewModel::class.java] + + val intent = intent + val info = InstallRepository.CallerInfo( + intent.getStringExtra(EXTRA_CALLING_PKG_NAME), + intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID) + ) + installViewModel!!.preprocessIntent(intent, info) + installViewModel!!.currentInstallStage.observe(this) { installStage: InstallStage -> + onInstallStageChange(installStage) + } + + // Used to launch intent for Settings, to manage "install unknown apps" permission + unknownAppsIntentLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + // Reattempt installation on coming back from Settings, after toggling + // "install unknown apps" permission + installViewModel!!.reattemptInstall() + } + } + + /** + * Main controller of the UI. This method shows relevant dialogs based on the install stage + */ + private fun onInstallStageChange(installStage: InstallStage) { + when (installStage.stageCode) { + InstallStage.STAGE_STAGING -> { + val stagingDialog = InstallStagingFragment() + showDialogInner(stagingDialog) + installViewModel!!.stagingProgress.observe(this) { progress: Int -> + stagingDialog.setProgress(progress) + } + } + + InstallStage.STAGE_ABORTED -> { + val aborted = installStage as InstallAborted + when (aborted.abortReason) { + InstallAborted.ABORT_REASON_DONE, InstallAborted.ABORT_REASON_INTERNAL_ERROR -> + setResult(aborted.activityResultCode, aborted.resultIntent, true) + + InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted) + else -> setResult(Activity.RESULT_CANCELED, null, true) + } + } + + InstallStage.STAGE_USER_ACTION_REQUIRED -> { + val uar = installStage as InstallUserActionRequired + when (uar.actionReason) { + InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> { + val actionDialog = InstallConfirmationFragment(uar) + showDialogInner(actionDialog) + } + + InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> { + val externalSourceDialog = ExternalSourcesBlockedFragment(uar) + showDialogInner(externalSourceDialog) + } + + InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> { + val anonymousSourceDialog = AnonymousSourceFragment() + showDialogInner(anonymousSourceDialog) + } + } + } + + InstallStage.STAGE_INSTALLING -> { + val installing = installStage as InstallInstalling + val installingDialog = InstallInstallingFragment(installing) + showDialogInner(installingDialog) + } + + InstallStage.STAGE_SUCCESS -> { + val success = installStage as InstallSuccess + if (success.shouldReturnResult) { + val successIntent = success.resultIntent + setResult(Activity.RESULT_OK, successIntent, true) + } else { + val successFragment = InstallSuccessFragment(success) + showDialogInner(successFragment) + } + } + + InstallStage.STAGE_FAILED -> { + val failed = installStage as InstallFailed + val failedDialog = InstallFailedFragment(failed) + showDialogInner(failedDialog) + } + + else -> { + Log.d(LOG_TAG, "Unimplemented stage: " + installStage.stageCode) + showDialogInner(null) + } + } + } + + private fun showPolicyRestrictionDialog(aborted: InstallAborted) { + val restriction = aborted.message + val adminSupportIntent = aborted.resultIntent + var shouldFinish: Boolean = false + + // If the given restriction is set by an admin, display information about the + // admin enforcing the restriction for the affected user. If not enforced by the admin, + // show the system dialog. + if (adminSupportIntent != null) { + if (localLOGV) { + Log.i(LOG_TAG, "Restriction set by admin, starting $adminSupportIntent") + } + startActivity(adminSupportIntent) + // Finish the package installer app since the next dialog will not be shown by this app + shouldFinish = true + } else { + if (localLOGV) { + Log.i(LOG_TAG, "Restriction set by system: $restriction") + } + val blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction) + // Don't finish the package installer app since the next dialog + // will be shown by this app + shouldFinish = blockedByPolicyDialog != null + showDialogInner(blockedByPolicyDialog) + } + setResult(Activity.RESULT_CANCELED, null, shouldFinish) + } + + /** + * Create a new dialog based on the install restriction enforced. + * + * @param restriction The restriction to create the dialog for + * @return The dialog + */ + private fun createDevicePolicyRestrictionDialog(restriction: String?): DialogFragment? { + if (localLOGV) { + Log.i(LOG_TAG, "createDialog($restriction)") + } + return when (restriction) { + UserManager.DISALLOW_INSTALL_APPS -> + SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text) + + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY -> + SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text) + + else -> null + } + } + + /** + * Replace any visible dialog by the dialog returned by InstallRepository + * + * @param newDialog The new dialog to display + */ + private fun showDialogInner(newDialog: DialogFragment?) { + val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment? + currentDialog?.dismissAllowingStateLoss() + newDialog?.show(fragmentManager!!, TAG_DIALOG) + } + + fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) { + super.setResult(resultCode, data) + if (shouldFinish) { + finish() + } + } + + override fun onPositiveResponse(reasonCode: Int) { + when (reasonCode) { + InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> + installViewModel!!.forcedSkipSourceCheck() + + InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> + installViewModel!!.initiateInstall() + } + } + + override fun onNegativeResponse(stageCode: Int) { + if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) { + installViewModel!!.cleanupInstall() + } + setResult(Activity.RESULT_CANCELED, null, true) + } + + override fun sendUnknownAppsIntent(sourcePackageName: String) { + val settingsIntent = Intent() + settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) + val packageUri = Uri.parse("package:$sourcePackageName") + settingsIntent.setData(packageUri) + settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + try { + registerAppOpChangeListener( + UnknownSourcesListener(sourcePackageName), sourcePackageName + ) + unknownAppsIntentLauncher.launch(settingsIntent) + } catch (exc: ActivityNotFoundException) { + Log.e( + LOG_TAG, "Settings activity not found for action: " + + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES + ) + } + } + + override fun openInstalledApp(intent: Intent?) { + setResult(Activity.RESULT_OK, intent, true) + if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { + startActivity(intent) + } + } + + private fun registerAppOpChangeListener(listener: UnknownSourcesListener, packageName: String) { + appOpsManager!!.startWatchingMode( + AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, + packageName, + listener + ) + activeUnknownSourcesListeners.add(listener) + } + + private fun unregisterAppOpChangeListener(listener: UnknownSourcesListener) { + activeUnknownSourcesListeners.remove(listener) + appOpsManager!!.stopWatchingMode(listener) + } + + override fun onDestroy() { + super.onDestroy() + while (activeUnknownSourcesListeners.isNotEmpty()) { + unregisterAppOpChangeListener(activeUnknownSourcesListeners[0]) + } + } + + private inner class UnknownSourcesListener(private val mOriginatingPackage: String) : + AppOpsManager.OnOpChangedListener { + override fun onOpChanged(op: String, packageName: String) { + if (mOriginatingPackage != packageName) { + return + } + unregisterAppOpChangeListener(this) + activeUnknownSourcesListeners.remove(this) + if (isDestroyed) { + return + } + Handler(Looper.getMainLooper()).postDelayed({ + if (!isDestroyed) { + // Relaunch Pia to continue installation. + startActivity( + intent.putExtra( + InstallRepository.EXTRA_STAGED_SESSION_ID, + installViewModel!!.stagedSessionId + ) + ) + + // If the userId of the root of activity stack is different from current userId, + // starting Pia again lead to duplicate instances of the app in the stack. + // As such, finish the old instance. Old Pia is finished even if the userId of + // the root is the same, since there is no way to determine the difference in + // userIds. + finish() + } + }, 500) + } + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt index b8a93559d782..33f5db34f754 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.packageinstaller.v2.ui; +package com.android.packageinstaller.v2.ui -public interface UninstallActionListener { - - void onPositiveResponse(boolean keepData); - - void onNegativeResponse(); +interface UninstallActionListener { + fun onPositiveResponse(keepData: Boolean) + fun onNegativeResponse() } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java deleted file mode 100644 index 7638e917c7d5..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.ui; - -import static android.os.Process.INVALID_UID; -import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - -import android.app.Activity; -import android.app.NotificationManager; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.lifecycle.ViewModelProvider; -import com.android.packageinstaller.v2.model.UninstallRepository; -import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired; -import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment; -import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment; -import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment; -import com.android.packageinstaller.v2.viewmodel.UninstallViewModel; -import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory; - -public class UninstallLaunch extends FragmentActivity implements UninstallActionListener { - - public static final String EXTRA_CALLING_PKG_UID = - UninstallLaunch.class.getPackageName() + ".callingPkgUid"; - public static final String EXTRA_CALLING_ACTIVITY_NAME = - UninstallLaunch.class.getPackageName() + ".callingActivityName"; - public static final String TAG = UninstallLaunch.class.getSimpleName(); - private static final String TAG_DIALOG = "dialog"; - - private UninstallViewModel mUninstallViewModel; - private UninstallRepository mUninstallRepository; - private FragmentManager mFragmentManager; - private NotificationManager mNotificationManager; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - - // Never restore any state, esp. never create any fragments. The data in the fragment might - // be stale, if e.g. the app was uninstalled while the activity was destroyed. - super.onCreate(null); - - mFragmentManager = getSupportFragmentManager(); - mNotificationManager = getSystemService(NotificationManager.class); - - mUninstallRepository = new UninstallRepository(getApplicationContext()); - mUninstallViewModel = new ViewModelProvider(this, - new UninstallViewModelFactory(this.getApplication(), mUninstallRepository)).get( - UninstallViewModel.class); - - Intent intent = getIntent(); - CallerInfo callerInfo = new CallerInfo( - intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME), - intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID)); - mUninstallViewModel.preprocessIntent(intent, callerInfo); - - mUninstallViewModel.getCurrentUninstallStage().observe(this, - this::onUninstallStageChange); - } - - /** - * Main controller of the UI. This method shows relevant dialogs / fragments based on the - * uninstall stage - */ - private void onUninstallStageChange(UninstallStage uninstallStage) { - if (uninstallStage.getStageCode() == UninstallStage.STAGE_ABORTED) { - UninstallAborted aborted = (UninstallAborted) uninstallStage; - if (aborted.getAbortReason() == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE || - aborted.getAbortReason() == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) { - UninstallErrorFragment errorDialog = new UninstallErrorFragment(aborted); - showDialogInner(errorDialog); - } else { - setResult(aborted.getActivityResultCode(), null, true); - } - } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_USER_ACTION_REQUIRED) { - UninstallUserActionRequired uar = (UninstallUserActionRequired) uninstallStage; - UninstallConfirmationFragment confirmationDialog = new UninstallConfirmationFragment( - uar); - showDialogInner(confirmationDialog); - } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_UNINSTALLING) { - // TODO: This shows a fragment whether or not user requests a result or not. - // Originally, if the user does not request a result, we used to show a notification. - // And a fragment if the user requests a result back. Should we consolidate and - // show a fragment always? - UninstallUninstalling uninstalling = (UninstallUninstalling) uninstallStage; - UninstallUninstallingFragment uninstallingDialog = new UninstallUninstallingFragment( - uninstalling); - showDialogInner(uninstallingDialog); - } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_FAILED) { - UninstallFailed failed = (UninstallFailed) uninstallStage; - if (!failed.returnResult()) { - mNotificationManager.notify(failed.getUninstallId(), - failed.getUninstallNotification()); - } - setResult(failed.getActivityResultCode(), failed.getResultIntent(), true); - } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_SUCCESS) { - UninstallSuccess success = (UninstallSuccess) uninstallStage; - if (success.getMessage() != null) { - Toast.makeText(this, success.getMessage(), Toast.LENGTH_LONG).show(); - } - setResult(success.getActivityResultCode(), success.getResultIntent(), true); - } else { - Log.e(TAG, "Invalid stage: " + uninstallStage.getStageCode()); - showDialogInner(null); - } - } - - /** - * Replace any visible dialog by the dialog returned by InstallRepository - * - * @param newDialog The new dialog to display - */ - private void showDialogInner(DialogFragment newDialog) { - DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag( - TAG_DIALOG); - if (currentDialog != null) { - currentDialog.dismissAllowingStateLoss(); - } - if (newDialog != null) { - newDialog.show(mFragmentManager, TAG_DIALOG); - } - } - - public void setResult(int resultCode, Intent data, boolean shouldFinish) { - super.setResult(resultCode, data); - if (shouldFinish) { - finish(); - } - } - - @Override - public void onPositiveResponse(boolean keepData) { - mUninstallViewModel.initiateUninstall(keepData); - } - - @Override - public void onNegativeResponse() { - mUninstallViewModel.cancelInstall(); - setResult(Activity.RESULT_FIRST_USER, null, true); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt new file mode 100644 index 000000000000..0050c7ebe93a --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 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 + * + * https://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.v2.ui + +import android.app.Activity +import android.app.NotificationManager +import android.content.Intent +import android.os.Bundle +import android.os.Process +import android.util.Log +import android.view.WindowManager +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.ViewModelProvider +import com.android.packageinstaller.v2.model.UninstallAborted +import com.android.packageinstaller.v2.model.UninstallFailed +import com.android.packageinstaller.v2.model.UninstallRepository +import com.android.packageinstaller.v2.model.UninstallStage +import com.android.packageinstaller.v2.model.UninstallSuccess +import com.android.packageinstaller.v2.model.UninstallUninstalling +import com.android.packageinstaller.v2.model.UninstallUserActionRequired +import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment +import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment +import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment +import com.android.packageinstaller.v2.viewmodel.UninstallViewModel +import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory + +class UninstallLaunch : FragmentActivity(), UninstallActionListener { + + companion object { + @JvmField val EXTRA_CALLING_PKG_UID = + UninstallLaunch::class.java.packageName + ".callingPkgUid" + @JvmField val EXTRA_CALLING_ACTIVITY_NAME = + UninstallLaunch::class.java.packageName + ".callingActivityName" + val LOG_TAG = UninstallLaunch::class.java.simpleName + private const val TAG_DIALOG = "dialog" + } + + private var uninstallViewModel: UninstallViewModel? = null + private var uninstallRepository: UninstallRepository? = null + private var fragmentManager: FragmentManager? = null + private var notificationManager: NotificationManager? = null + override fun onCreate(savedInstanceState: Bundle?) { + window.addSystemFlags(WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) + + // Never restore any state, esp. never create any fragments. The data in the fragment might + // be stale, if e.g. the app was uninstalled while the activity was destroyed. + super.onCreate(null) + fragmentManager = supportFragmentManager + notificationManager = getSystemService(NotificationManager::class.java) + + uninstallRepository = UninstallRepository(applicationContext) + uninstallViewModel = ViewModelProvider( + this, UninstallViewModelFactory(this.application, uninstallRepository!!) + ).get(UninstallViewModel::class.java) + + val intent = intent + val callerInfo = UninstallRepository.CallerInfo( + intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME), + intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID) + ) + uninstallViewModel!!.preprocessIntent(intent, callerInfo) + uninstallViewModel!!.currentUninstallStage.observe(this) { uninstallStage: UninstallStage -> + onUninstallStageChange(uninstallStage) + } + } + + /** + * Main controller of the UI. This method shows relevant dialogs / fragments based on the + * uninstall stage + */ + private fun onUninstallStageChange(uninstallStage: UninstallStage) { + when (uninstallStage.stageCode) { + UninstallStage.STAGE_ABORTED -> { + val aborted = uninstallStage as UninstallAborted + if (aborted.abortReason == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE || + aborted.abortReason == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED + ) { + val errorDialog = UninstallErrorFragment(aborted) + showDialogInner(errorDialog) + } else { + setResult(aborted.activityResultCode, null, true) + } + } + + UninstallStage.STAGE_USER_ACTION_REQUIRED -> { + val uar = uninstallStage as UninstallUserActionRequired + val confirmationDialog = UninstallConfirmationFragment(uar) + showDialogInner(confirmationDialog) + } + + UninstallStage.STAGE_UNINSTALLING -> { + // TODO: This shows a fragment whether or not user requests a result or not. + // Originally, if the user does not request a result, we used to show a notification. + // And a fragment if the user requests a result back. Should we consolidate and + // show a fragment always? + val uninstalling = uninstallStage as UninstallUninstalling + val uninstallingDialog = UninstallUninstallingFragment(uninstalling) + showDialogInner(uninstallingDialog) + } + + UninstallStage.STAGE_FAILED -> { + val failed = uninstallStage as UninstallFailed + if (!failed.returnResult) { + notificationManager!!.notify( + failed.uninstallNotificationId!!, failed.uninstallNotification + ) + } + setResult(failed.activityResultCode, failed.resultIntent, true) + } + + UninstallStage.STAGE_SUCCESS -> { + val success = uninstallStage as UninstallSuccess + if (success.message != null) { + Toast.makeText(this, success.message, Toast.LENGTH_LONG).show() + } + setResult(success.activityResultCode, success.resultIntent, true) + } + + else -> { + Log.e(LOG_TAG, "Invalid stage: " + uninstallStage.stageCode) + showDialogInner(null) + } + } + } + + /** + * Replace any visible dialog by the dialog returned by InstallRepository + * + * @param newDialog The new dialog to display + */ + private fun showDialogInner(newDialog: DialogFragment?) { + val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment? + currentDialog?.dismissAllowingStateLoss() + newDialog?.show(fragmentManager!!, TAG_DIALOG) + } + + fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) { + super.setResult(resultCode, data) + if (shouldFinish) { + finish() + } + } + + override fun onPositiveResponse(keepData: Boolean) { + uninstallViewModel!!.initiateUninstall(keepData) + } + + override fun onNegativeResponse() { + uninstallViewModel!!.cancelInstall() + setResult(Activity.RESULT_FIRST_USER, null, true) + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java index 6d6fcc94faf7..679f696ff59f 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java @@ -24,8 +24,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; -import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired; +import com.android.packageinstaller.v2.model.InstallStage; +import com.android.packageinstaller.v2.model.InstallUserActionRequired; import com.android.packageinstaller.v2.ui.InstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java index 4cdce52e96ba..49901de96bc4 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java @@ -25,7 +25,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired; +import com.android.packageinstaller.v2.model.InstallUserActionRequired; import com.android.packageinstaller.v2.ui.InstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java index 6398aef5d573..25363d0b5f7b 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java @@ -28,7 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired; +import com.android.packageinstaller.v2.model.InstallUserActionRequired; import com.android.packageinstaller.v2.ui.InstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java index d45cd76b2f2a..4667a7a4e48a 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java @@ -28,7 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallFailed; +import com.android.packageinstaller.v2.model.InstallFailed; import com.android.packageinstaller.v2.ui.InstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java index 9f60f96bdfac..7327b5d5b9c2 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java @@ -25,7 +25,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling; +import com.android.packageinstaller.v2.model.InstallInstalling; /** * Dialog to show when an install is in progress. diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java index ab6a93222d48..b2a65faa0a91 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java @@ -29,8 +29,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; -import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess; +import com.android.packageinstaller.v2.model.InstallStage; +import com.android.packageinstaller.v2.model.InstallSuccess; import com.android.packageinstaller.v2.ui.InstallActionListener; import java.util.List; diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java index 47fd67f0cf6b..58b8b2def6d0 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java @@ -24,7 +24,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; +import com.android.packageinstaller.v2.model.InstallStage; import com.android.packageinstaller.v2.ui.InstallActionListener; public class SimpleErrorFragment extends DialogFragment { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java index 1b0885ea684a..32ac4a61b73e 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java @@ -30,7 +30,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired; +import com.android.packageinstaller.v2.model.UninstallUserActionRequired; import com.android.packageinstaller.v2.ui.UninstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java index 305daba14f26..eb7183df07b9 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java @@ -25,7 +25,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted; +import com.android.packageinstaller.v2.model.UninstallAborted; import com.android.packageinstaller.v2.ui.UninstallActionListener; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java index 23cc421890ac..835efc607fcb 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java @@ -22,7 +22,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling; +import com.android.packageinstaller.v2.model.UninstallUninstalling; /** * Dialog to show that the app is uninstalling. diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java deleted file mode 100644 index 04a0622627b9..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.viewmodel; - -import android.app.Application; -import android.content.Intent; -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MediatorLiveData; -import androidx.lifecycle.MutableLiveData; -import com.android.packageinstaller.v2.model.InstallRepository; -import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo; -import com.android.packageinstaller.v2.model.installstagedata.InstallStage; -import com.android.packageinstaller.v2.model.installstagedata.InstallStaging; - - -public class InstallViewModel extends AndroidViewModel { - - private static final String TAG = InstallViewModel.class.getSimpleName(); - private final InstallRepository mRepository; - private final MediatorLiveData<InstallStage> mCurrentInstallStage = new MediatorLiveData<>( - new InstallStaging()); - - public InstallViewModel(@NonNull Application application, InstallRepository repository) { - super(application); - mRepository = repository; - } - - public MutableLiveData<InstallStage> getCurrentInstallStage() { - return mCurrentInstallStage; - } - - public void preprocessIntent(Intent intent, CallerInfo callerInfo) { - InstallStage stage = mRepository.performPreInstallChecks(intent, callerInfo); - if (stage.getStageCode() == InstallStage.STAGE_ABORTED) { - mCurrentInstallStage.setValue(stage); - } else { - // Since staging is an async operation, we will get the staging result later in time. - // Result of the file staging will be set in InstallRepository#mStagingResult. - // As such, mCurrentInstallStage will need to add another MutableLiveData - // as a data source - mRepository.stageForInstall(); - mCurrentInstallStage.addSource(mRepository.getStagingResult(), installStage -> { - if (installStage.getStageCode() != InstallStage.STAGE_READY) { - mCurrentInstallStage.setValue(installStage); - } else { - checkIfAllowedAndInitiateInstall(); - } - }); - } - } - - public MutableLiveData<Integer> getStagingProgress() { - return mRepository.getStagingProgress(); - } - - private void checkIfAllowedAndInitiateInstall() { - InstallStage stage = mRepository.requestUserConfirmation(); - mCurrentInstallStage.setValue(stage); - } - - public void forcedSkipSourceCheck() { - InstallStage stage = mRepository.forcedSkipSourceCheck(); - mCurrentInstallStage.setValue(stage); - } - - public void cleanupInstall() { - mRepository.cleanupInstall(); - } - - public void reattemptInstall() { - InstallStage stage = mRepository.reattemptInstall(); - mCurrentInstallStage.setValue(stage); - } - - public void initiateInstall() { - // Since installing is an async operation, we will get the install result later in time. - // Result of the installation will be set in InstallRepository#mInstallResult. - // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source - mRepository.initiateInstall(); - mCurrentInstallStage.addSource(mRepository.getInstallResult(), installStage -> { - if (installStage != null) { - mCurrentInstallStage.setValue(installStage); - } - }); - } - - public int getStagedSessionId() { - return mRepository.getStagedSessionId(); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt new file mode 100644 index 000000000000..072fb2d34928 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 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.v2.viewmodel + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.android.packageinstaller.v2.model.InstallRepository +import com.android.packageinstaller.v2.model.InstallStage +import com.android.packageinstaller.v2.model.InstallStaging + +class InstallViewModel(application: Application, val repository: InstallRepository) : + AndroidViewModel(application) { + + companion object { + private val LOG_TAG = InstallViewModel::class.java.simpleName + } + + private val _currentInstallStage = MediatorLiveData<InstallStage>(InstallStaging()) + val currentInstallStage: MutableLiveData<InstallStage> + get() = _currentInstallStage + + fun preprocessIntent(intent: Intent, callerInfo: InstallRepository.CallerInfo) { + val stage = repository.performPreInstallChecks(intent, callerInfo) + if (stage.stageCode == InstallStage.STAGE_ABORTED) { + _currentInstallStage.value = stage + } else { + // Since staging is an async operation, we will get the staging result later in time. + // Result of the file staging will be set in InstallRepository#mStagingResult. + // As such, mCurrentInstallStage will need to add another MutableLiveData + // as a data source + repository.stageForInstall() + _currentInstallStage.addSource(repository.stagingResult) { installStage: InstallStage -> + if (installStage.stageCode != InstallStage.STAGE_READY) { + _currentInstallStage.value = installStage + } else { + checkIfAllowedAndInitiateInstall() + } + } + } + } + + val stagingProgress: LiveData<Int> + get() = repository.stagingProgress + + private fun checkIfAllowedAndInitiateInstall() { + val stage = repository.requestUserConfirmation() + _currentInstallStage.value = stage + } + + fun forcedSkipSourceCheck() { + val stage = repository.forcedSkipSourceCheck() + _currentInstallStage.value = stage + } + + fun cleanupInstall() { + repository.cleanupInstall() + } + + fun reattemptInstall() { + val stage = repository.reattemptInstall() + _currentInstallStage.value = stage + } + + fun initiateInstall() { + // Since installing is an async operation, we will get the install result later in time. + // Result of the installation will be set in InstallRepository#mInstallResult. + // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source + repository.initiateInstall() + _currentInstallStage.addSource(repository.installResult) { installStage: InstallStage? -> + if (installStage != null) { + _currentInstallStage.value = installStage + } + } + } + + val stagedSessionId: Int + get() = repository.stagedSessionId +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java deleted file mode 100644 index ef459e64d7d5..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.viewmodel; - -import android.app.Application; -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import com.android.packageinstaller.v2.model.InstallRepository; - -public class InstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory { - - private final InstallRepository mRepository; - private final Application mApplication; - - public InstallViewModelFactory(Application application, InstallRepository repository) { - // Calling super class' ctor ensures that create method is called correctly and the right - // ctor of InstallViewModel is used. If we fail to do that, the default ctor: - // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel - super(application); - mApplication = application; - mRepository = repository; - } - - @NonNull - @Override - @SuppressWarnings("unchecked") - public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { - return (T) new InstallViewModel(mApplication, mRepository); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt new file mode 100644 index 000000000000..07b2f4fcf2a1 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.v2.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.packageinstaller.v2.model.InstallRepository + +class InstallViewModelFactory(val application: Application, val repository: InstallRepository) : + ViewModelProvider.AndroidViewModelFactory(application) { + + // Calling super class' ctor ensures that create method is called correctly and the right + // ctor of InstallViewModel is used. If we fail to do that, the default ctor: + // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return InstallViewModel(application, repository) as T + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java deleted file mode 100644 index 3f7bce8f85d0..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2023 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 - * - * https://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.v2.viewmodel; - -import android.app.Application; -import android.content.Intent; -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MediatorLiveData; -import androidx.lifecycle.MutableLiveData; -import com.android.packageinstaller.v2.model.UninstallRepository; -import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo; -import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage; - -public class UninstallViewModel extends AndroidViewModel { - - private static final String TAG = UninstallViewModel.class.getSimpleName(); - private final UninstallRepository mRepository; - private final MediatorLiveData<UninstallStage> mCurrentUninstallStage = - new MediatorLiveData<>(); - - public UninstallViewModel(@NonNull Application application, UninstallRepository repository) { - super(application); - mRepository = repository; - } - - public MutableLiveData<UninstallStage> getCurrentUninstallStage() { - return mCurrentUninstallStage; - } - - public void preprocessIntent(Intent intent, CallerInfo callerInfo) { - UninstallStage stage = mRepository.performPreUninstallChecks(intent, callerInfo); - if (stage.getStageCode() != UninstallStage.STAGE_ABORTED) { - stage = mRepository.generateUninstallDetails(); - } - mCurrentUninstallStage.setValue(stage); - } - - public void initiateUninstall(boolean keepData) { - mRepository.initiateUninstall(keepData); - // Since uninstall is an async operation, we will get the uninstall result later in time. - // Result of the uninstall will be set in UninstallRepository#mUninstallResult. - // As such, mCurrentUninstallStage will need to add another MutableLiveData - // as a data source - mCurrentUninstallStage.addSource(mRepository.getUninstallResult(), uninstallStage -> { - if (uninstallStage != null) { - mCurrentUninstallStage.setValue(uninstallStage); - } - }); - } - - public void cancelInstall() { - mRepository.cancelInstall(); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt new file mode 100644 index 000000000000..80886e92e33e --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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 + * + * https://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.v2.viewmodel + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import com.android.packageinstaller.v2.model.UninstallRepository +import com.android.packageinstaller.v2.model.UninstallStage + +class UninstallViewModel(application: Application, val repository: UninstallRepository) : + AndroidViewModel(application) { + + companion object { + private val LOG_TAG = UninstallViewModel::class.java.simpleName + } + + private val _currentUninstallStage = MediatorLiveData<UninstallStage>() + val currentUninstallStage: MutableLiveData<UninstallStage> + get() = _currentUninstallStage + + fun preprocessIntent(intent: Intent, callerInfo: UninstallRepository.CallerInfo) { + var stage = repository.performPreUninstallChecks(intent, callerInfo) + if (stage.stageCode != UninstallStage.STAGE_ABORTED) { + stage = repository.generateUninstallDetails() + } + _currentUninstallStage.value = stage + } + + fun initiateUninstall(keepData: Boolean) { + repository.initiateUninstall(keepData) + // Since uninstall is an async operation, we will get the uninstall result later in time. + // Result of the uninstall will be set in UninstallRepository#mUninstallResult. + // As such, _currentUninstallStage will need to add another MutableLiveData + // as a data source + _currentUninstallStage.addSource(repository.uninstallResult) { uninstallStage: UninstallStage? -> + if (uninstallStage != null) { + _currentUninstallStage.value = uninstallStage + } + } + } + + fun cancelInstall() { + repository.cancelInstall() + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java deleted file mode 100644 index cd9845e2cfad..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 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.v2.viewmodel; - -import android.app.Application; -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import com.android.packageinstaller.v2.model.UninstallRepository; - -public class UninstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory { - - private final UninstallRepository mRepository; - private final Application mApplication; - - public UninstallViewModelFactory(Application application, UninstallRepository repository) { - // Calling super class' ctor ensures that create method is called correctly and the right - // ctor of UninstallViewModel is used. If we fail to do that, the default ctor: - // UninstallViewModel(application) is used, and repository isn't initialized in - // the viewmodel - super(application); - mApplication = application; - mRepository = repository; - } - - @NonNull - @Override - @SuppressWarnings("unchecked") - public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { - return (T) new UninstallViewModel(mApplication, mRepository); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt new file mode 100644 index 000000000000..0a316e72791b --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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.v2.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.packageinstaller.v2.model.UninstallRepository + +class UninstallViewModelFactory(val application: Application, val repository: UninstallRepository) : + ViewModelProvider.AndroidViewModelFactory(application) { + + // Calling super class' ctor ensures that create method is called correctly and the right + // ctor of UninstallViewModel is used. If we fail to do that, the default ctor: + // UninstallViewModel(application) is used, and repository isn't initialized in + // the viewmodel + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return UninstallViewModel(application, repository) as T + } +} |