summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/PackageInstaller/Android.bp15
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java912
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt867
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt134
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java462
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt440
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java126
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt110
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java716
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt739
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt112
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java127
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java69
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java47
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java27
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java34
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java27
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java95
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java99
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java71
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java119
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java27
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java30
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java79
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java43
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java74
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt (renamed from packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java)23
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java354
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt348
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt (renamed from packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java)10
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java167
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt169
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java4
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java4
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java105
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt96
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java45
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt33
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java69
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt62
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java46
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt34
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
+ }
+}