Handle uninstall result - caller does not want the result back.

When the caller does not request the uninstall result, we show a toast in cast of a successful uninstall and a notification in case of a failed uninstall.

Bug: 182205982
Test: builds successfully
Test: No CTS Tests. Flag to use new app is turned off by default

Change-Id: I10342c9f9ab83c418afd74479b9f04e7837e6545
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
index e188838..fe05237 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -30,6 +30,8 @@
 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;
@@ -404,6 +406,24 @@
     }
 
     /**
+     * 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)},
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
index 6b3f293..628d111 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
@@ -22,6 +22,7 @@
 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;
@@ -29,7 +30,11 @@
 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;
@@ -42,12 +47,14 @@
 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;
@@ -66,6 +73,7 @@
 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";
 
@@ -82,6 +90,7 @@
     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;
@@ -100,6 +109,7 @@
         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) {
@@ -460,7 +470,193 @@
                     .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;
     }
 
     /**
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
index dd91809..6ed8883 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
@@ -17,6 +17,7 @@
 package com.android.packageinstaller.v2.model.uninstallstagedata;
 
 import android.app.Activity;
+import android.app.Notification;
 import android.content.Intent;
 
 public class UninstallFailed extends UninstallStage {
@@ -28,12 +29,24 @@
      * 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) {
+    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() {
@@ -48,6 +61,14 @@
         return mActivityResultCode;
     }
 
+    public Notification getUninstallNotification() {
+        return mUninstallNotification;
+    }
+
+    public int getUninstallId() {
+        return mUninstallId;
+    }
+
     @Override
     public int getStageCode() {
         return mStage;
@@ -61,11 +82,25 @@
          * 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;
@@ -77,7 +112,8 @@
         }
 
         public UninstallFailed build() {
-            return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode);
+            return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode,
+                mUninstallId, mUninstallNotification);
         }
     }
 }
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
index 6d29306..5df6b02 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
@@ -21,12 +21,18 @@
 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) {
+    public UninstallSuccess(Intent resultIntent, int activityResultCode, String message) {
         mResultIntent = resultIntent;
         mActivityResultCode = activityResultCode;
+        mMessage = message;
+    }
+
+    public String getMessage() {
+        return mMessage;
     }
 
     public Intent getResultIntent() {
@@ -46,6 +52,7 @@
 
         private Intent mResultIntent;
         private int mActivityResultCode;
+        private String mMessage;
 
         public Builder() {
         }
@@ -60,8 +67,13 @@
             return this;
         }
 
+        public Builder setMessage(String message) {
+            mMessage = message;
+            return this;
+        }
+
         public UninstallSuccess build() {
-            return new UninstallSuccess(mResultIntent, mActivityResultCode);
+            return new UninstallSuccess(mResultIntent, mActivityResultCode, mMessage);
         }
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
index 2215980..0886d77 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
@@ -20,9 +20,11 @@
 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;
@@ -54,6 +56,7 @@
     private UninstallViewModel mUninstallViewModel;
     private UninstallRepository mUninstallRepository;
     private FragmentManager mFragmentManager;
+    private NotificationManager mNotificationManager;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -64,6 +67,7 @@
         super.onCreate(null);
 
         mFragmentManager = getSupportFragmentManager();
+        mNotificationManager = getSystemService(NotificationManager.class);
 
         mUninstallRepository = new UninstallRepository(getApplicationContext());
         mUninstallViewModel = new ViewModelProvider(this,
@@ -110,9 +114,16 @@
             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());