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());