Revise the logic of computing user intervention (2/n)

In computeUserActionRequirement(), it now honors update ownership
enforcement. If the app being installed has the update owner, the
user intervention will be required once the update isn't performed
by the update owner. In this Cl, we also revise the PackageInstaler
app to reflect the result to users.

Bug: 244413073
Test: atest CtsPackageInstallTestCases:UpdateOwnershipEnforcementTest
Change-Id: Ie620013a4adf861956226d34637dbd3a0dd9a3ea
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 137751b..ceed59f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3592,6 +3592,9 @@
     field public static final int LOCATION_DATA_APP = 0; // 0x0
     field public static final int LOCATION_MEDIA_DATA = 2; // 0x2
     field public static final int LOCATION_MEDIA_OBB = 1; // 0x1
+    field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0
+    field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1
+    field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2
   }
 
   public static class PackageInstaller.InstallInfo {
@@ -3621,6 +3624,7 @@
     method public boolean getInstallAsFullApp(boolean);
     method public boolean getInstallAsInstantApp(boolean);
     method public boolean getInstallAsVirtualPreload();
+    method public int getPendingUserActionReason();
     method public boolean getRequestDowngrade();
     method public int getRollbackDataPolicy();
     method @NonNull public java.util.Set<java.lang.String> getWhitelistedRestrictedPermissions();
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 8f29563..812b5b3 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -544,6 +544,46 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface PackageSourceType{}
 
+    /**
+     * Indicate the user intervention is required when the installer attempts to commit the session.
+     * This is the default case.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0;
+
+    /**
+     * Indicate the user intervention is required because the update ownership enforcement is
+     * enabled, and the update owner will change.
+     *
+     * @see PackageInstaller.SessionParams#setRequestUpdateOwnership
+     * @see InstallSourceInfo#getUpdateOwnerPackageName
+     * @hide
+     */
+    @SystemApi
+    public static final int REASON_OWNERSHIP_CHANGED = 1;
+
+    /**
+     * Indicate the user intervention is required because the update ownership enforcement is
+     * enabled, and remind the update owner will retain.
+     *
+     * @see PackageInstaller.SessionParams#setRequestUpdateOwnership
+     * @see InstallSourceInfo#getUpdateOwnerPackageName
+     * @hide
+     */
+    @SystemApi
+    public static final int REASON_REMIND_OWNERSHIP = 2;
+
+    /** @hide */
+    @IntDef(prefix = { "REASON_" }, value = {
+            REASON_CONFIRM_PACKAGE_CHANGE,
+            REASON_OWNERSHIP_CHANGED,
+            REASON_REMIND_OWNERSHIP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UserActionReason {}
+
     /** Default set of checksums - includes all available checksums.
      * @see Session#requestChecksums  */
     private static final int DEFAULT_CHECKSUMS =
@@ -2686,9 +2726,18 @@
          *              Android S ({@link android.os.Build.VERSION_CODES#S API 31})</li>
          *          </ul>
          *     </li>
-         *     <li>The installer is the {@link InstallSourceInfo#getInstallingPackageName()
-         *     installer of record} of an existing version of the app (in other words, this install
-         *     session is an app update) or the installer is updating itself.</li>
+         *     <li>The installer is:
+         *         <ul>
+         *             <li>The {@link InstallSourceInfo#getUpdateOwnerPackageName() update owner}
+         *             of an existing version of the app (in other words, this install session is
+         *             an app update) if the update ownership enforcement is enabled.</li>
+         *             <li>The {@link InstallSourceInfo#getInstallingPackageName() installer of
+         *             record} of an existing version of the app (in other words, this install
+         *             session is an app update) if the update ownership enforcement isn't
+         *             enabled.</li>
+         *             <li>Updating itself.</li>
+         *         </ul>
+         *     </li>>
          *     <li>The installer declares the
          *     {@link android.Manifest.permission#UPDATE_PACKAGES_WITHOUT_USER_ACTION
          *     UPDATE_PACKAGES_WITHOUT_USER_ACTION} permission.</li>
@@ -3025,6 +3074,9 @@
         /** @hide */
         public boolean keepApplicationEnabledSetting;
 
+        /** @hide */
+        public int pendingUserActionReason;
+
         /** {@hide} */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         public SessionInfo() {
@@ -3079,6 +3131,7 @@
             installerUid = source.readInt();
             packageSource = source.readInt();
             keepApplicationEnabledSetting = source.readBoolean();
+            pendingUserActionReason = source.readInt();
         }
 
         /**
@@ -3633,6 +3686,15 @@
             return (installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0;
         }
 
+        /**
+         * Return the reason for requiring the user action.
+         * @hide
+         */
+        @SystemApi
+        public @UserActionReason int getPendingUserActionReason() {
+            return pendingUserActionReason;
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -3683,6 +3745,7 @@
             dest.writeInt(installerUid);
             dest.writeInt(packageSource);
             dest.writeBoolean(keepApplicationEnabledSetting);
+            dest.writeInt(pendingUserActionReason);
         }
 
         public static final Parcelable.Creator<SessionInfo>
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index b713c14..cb2baa9 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -37,6 +37,11 @@
     <string name="install_confirm_question">Do you want to install this app?</string>
     <!-- Message for updating an existing app [CHAR LIMIT=NONE] -->
     <string name="install_confirm_question_update">Do you want to update this app?</string>
+    <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. -->
+    <!-- Message for updating an existing app when updating owner changed [CHAR LIMIT=NONE] -->
+    <string name="install_confirm_question_update_owner_changed">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nBy updating, you\'ll get future updates from <xliff:g id="new_update_owner">%2$s</xliff:g> instead.</string>
+    <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
+    <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>.</string>
     <!-- [CHAR LIMIT=100] -->
     <string name="install_failed">App not installed.</string>
     <!-- Reason displayed when installation fails because the package was blocked
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 3138158..49c9188 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -33,10 +33,12 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
@@ -47,9 +49,11 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
@@ -88,6 +92,7 @@
     private int mOriginatingUid = Process.INVALID_UID;
     private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid
     private int mActivityResultCode = Activity.RESULT_CANCELED;
+    private int mPendingUserActionReason = -1;
 
     private final boolean mLocalLOGV = false;
     PackageManager mPm;
@@ -132,10 +137,27 @@
     private boolean mEnableOk = false;
 
     private void startInstallConfirm() {
-        View viewToEnable;
+        TextView viewToEnable;
 
         if (mAppInfo != null) {
             viewToEnable = requireViewById(R.id.install_confirm_question_update);
+
+            final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel();
+            final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage);
+            if (!TextUtils.isEmpty(existingUpdateOwnerLabel)) {
+                if (mPendingUserActionReason == PackageInstaller.REASON_OWNERSHIP_CHANGED) {
+                    viewToEnable.setText(
+                            getString(R.string.install_confirm_question_update_owner_changed,
+                                    existingUpdateOwnerLabel, requestedUpdateOwnerLabel));
+                } else if (mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP
+                        || mPendingUserActionReason
+                        == PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE) {
+                    viewToEnable.setText(
+                            getString(R.string.install_confirm_question_update_owner_reminder,
+                                    existingUpdateOwnerLabel, requestedUpdateOwnerLabel));
+                }
+            }
+
             mOk.setText(R.string.update);
         } else {
             // This is a new application with no permissions.
@@ -149,6 +171,27 @@
         mOk.setFilterTouchesWhenObscured(true);
     }
 
+    private CharSequence getExistingUpdateOwnerLabel() {
+        try {
+            final String packageName = mPkgInfo.packageName;
+            final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName);
+            final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName();
+            return getApplicationLabel(existingUpdateOwner);
+        } catch (NameNotFoundException e) {
+            return null;
+        }
+    }
+
+    private CharSequence getApplicationLabel(String packageName) {
+        try {
+            final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName,
+                    ApplicationInfoFlags.of(0));
+            return mPm.getApplicationLabel(appInfo);
+        } catch (NameNotFoundException e) {
+            return null;
+        }
+    }
+
     /**
      * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}.
      *
@@ -344,6 +387,7 @@
             packageSource = Uri.fromFile(new File(resolvedBaseCodePath));
             mOriginatingURI = null;
             mReferrerURI = null;
+            mPendingUserActionReason = info.getPendingUserActionReason();
         } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) {
             final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
                     -1 /* defaultValue */);
@@ -358,11 +402,13 @@
             packageSource = info;
             mOriginatingURI = null;
             mReferrerURI = null;
+            mPendingUserActionReason = info.getPendingUserActionReason();
         } else {
             mSessionId = -1;
             packageSource = intent.getData();
             mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
             mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
+            mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE;
         }
 
         // if there's nothing to do, quietly slip into the ether
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 4bbf09d..8ea7788 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -89,6 +89,7 @@
 import android.content.pm.PackageInstaller.PreapprovalDetails;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageInstaller.UserActionReason;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.pm.PackageManagerInternal;
@@ -447,6 +448,9 @@
     @GuardedBy("mLock")
     private boolean mHasDeviceAdminReceiver;
 
+    @GuardedBy("mLock")
+    private int mUserActionRequirement;
+
     static class FileEntry {
         private final int mIndex;
         private final InstallationFile mFile;
@@ -843,10 +847,17 @@
     private static final int USER_ACTION_NOT_NEEDED = 0;
     private static final int USER_ACTION_REQUIRED = 1;
     private static final int USER_ACTION_PENDING_APK_PARSING = 2;
+    private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED = 3;
+    private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED = 4;
 
-    @IntDef({USER_ACTION_NOT_NEEDED, USER_ACTION_REQUIRED, USER_ACTION_PENDING_APK_PARSING})
-    @interface
-    UserActionRequirement {}
+    @IntDef({
+            USER_ACTION_NOT_NEEDED,
+            USER_ACTION_REQUIRED,
+            USER_ACTION_PENDING_APK_PARSING,
+            USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED,
+            USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED
+    })
+    @interface UserActionRequirement {}
 
     /**
      * Checks if the permissions still need to be confirmed.
@@ -900,8 +911,13 @@
         final String existingInstallerPackageName = existingInstallSourceInfo != null
                 ? existingInstallSourceInfo.getInstallingPackageName()
                 : null;
+        final String existingUpdateOwnerPackageName = existingInstallSourceInfo != null
+                ? existingInstallSourceInfo.getUpdateOwnerPackageName()
+                : null;
         final boolean isInstallerOfRecord = isUpdate
                 && Objects.equals(existingInstallerPackageName, getInstallerPackageName());
+        final boolean isUpdateOwner = Objects.equals(existingUpdateOwnerPackageName,
+                getInstallerPackageName());
         final boolean isSelfUpdate = targetPackageUid == mInstallerUid;
         final boolean isPermissionGranted = isInstallPermissionGranted
                 || (isUpdatePermissionGranted && isUpdate)
@@ -909,16 +925,35 @@
                 || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver);
         final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID);
         final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID);
+        final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID);
+        final boolean isUpdateOwnershipEnforcementEnabled =
+                mPm.isUpdateOwnershipEnforcementAvailable()
+                        && existingUpdateOwnerPackageName != null;
 
-        // Device owners and affiliated profile owners  are allowed to silently install packages, so
+        // Device owners and affiliated profile owners are allowed to silently install packages, so
         // the permission check is waived if the installer is the device owner.
-        final boolean noUserActionNecessary = isPermissionGranted || isInstallerRoot
-                || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwner();
+        final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem
+                || isInstallerDeviceOwnerOrAffiliatedProfileOwner();
 
         if (noUserActionNecessary) {
             return USER_ACTION_NOT_NEEDED;
         }
 
+        if (isUpdateOwnershipEnforcementEnabled
+                && !isApexSession()
+                && !isUpdateOwner
+                && !isInstallerShell) {
+            final boolean isRequestUpdateOwner =
+                    (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0;
+
+            return isRequestUpdateOwner ? USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED
+                    : USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED;
+        }
+
+        if (isPermissionGranted) {
+            return USER_ACTION_NOT_NEEDED;
+        }
+
         if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid,
                 userId)) {
             // show the installer to account for device policy or unknown sources use cases
@@ -927,13 +962,20 @@
 
         if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED
                 && isUpdateWithoutUserActionPermissionGranted
-                && (isInstallerOfRecord || isSelfUpdate)) {
+                && ((isUpdateOwnershipEnforcementEnabled ? isUpdateOwner
+                : isInstallerOfRecord) || isSelfUpdate)) {
             return USER_ACTION_PENDING_APK_PARSING;
         }
 
         return USER_ACTION_REQUIRED;
     }
 
+    private void updateUserActionRequirement(int requirement) {
+        synchronized (mLock) {
+            mUserActionRequirement = requirement;
+        }
+    }
+
     @SuppressWarnings("GuardedBy" /*mPm.mInstaller is {@code final} field*/)
     public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
             Context context, PackageManagerService pm,
@@ -1122,6 +1164,7 @@
             info.installerUid = mInstallerUid;
             info.packageSource = params.packageSource;
             info.keepApplicationEnabledSetting = params.keepApplicationEnabledSetting;
+            info.pendingUserActionReason = userActionRequirementToReason(mUserActionRequirement);
         }
         return info;
     }
@@ -2222,7 +2265,10 @@
         @UserActionRequirement int userActionRequirement = USER_ACTION_NOT_NEEDED;
         // TODO(b/159331446): Move this to makeSessionActiveForInstall and update javadoc
         userActionRequirement = session.computeUserActionRequirement();
-        if (userActionRequirement == USER_ACTION_REQUIRED) {
+        session.updateUserActionRequirement(userActionRequirement);
+        if (userActionRequirement == USER_ACTION_REQUIRED
+                || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED
+                || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED) {
             session.sendPendingUserActionIntent(target);
             return true;
         }
@@ -2255,6 +2301,18 @@
         return false;
     }
 
+    private static @UserActionReason int userActionRequirementToReason(
+            @UserActionRequirement int requirement) {
+        switch (requirement) {
+            case USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED:
+                return PackageInstaller.REASON_OWNERSHIP_CHANGED;
+            case USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED:
+                return PackageInstaller.REASON_REMIND_OWNERSHIP;
+            default:
+                return PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE;
+        }
+    }
+
     /**
      * Find out any session needs user action.
      *