Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 518950fc authored by Sumedh Sen's avatar Sumedh Sen
Browse files

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
parent 5b346936
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@ 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;
@@ -403,6 +405,24 @@ public class PackageUtil {
        }
    }

    /**
     * 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)},
+196 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ 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;
@@ -29,7 +30,11 @@ import static com.android.packageinstaller.v2.model.uninstallstagedata.Uninstall
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.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;
@@ -66,6 +73,7 @@ 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";

@@ -82,6 +90,7 @@ public class UninstallRepository {
    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 @@ public class UninstallRepository {
        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,8 +470,194 @@ public class UninstallRepository {
                    .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.
+38 −2
Original line number Diff line number Diff line
@@ -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 @@ public class UninstallFailed extends UninstallStage {
     * 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 @@ public class UninstallFailed extends UninstallStage {
        return mActivityResultCode;
    }

    public Notification getUninstallNotification() {
        return mUninstallNotification;
    }

    public int getUninstallId() {
        return mUninstallId;
    }

    @Override
    public int getStageCode() {
        return mStage;
@@ -61,11 +82,25 @@ public class UninstallFailed extends UninstallStage {
         * 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 class UninstallFailed extends UninstallStage {
        }

        public UninstallFailed build() {
            return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode);
            return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode,
                mUninstallId, mUninstallNotification);
        }
    }
}
+14 −2
Original line number Diff line number Diff line
@@ -21,12 +21,18 @@ 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) {
    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 @@ public class UninstallSuccess extends UninstallStage {

        private Intent mResultIntent;
        private int mActivityResultCode;
        private String mMessage;

        public Builder() {
        }
@@ -60,8 +67,13 @@ public class UninstallSuccess extends UninstallStage {
            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);
        }
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -20,9 +20,11 @@ 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;
@@ -54,6 +56,7 @@ public class UninstallLaunch extends FragmentActivity implements UninstallAction
    private UninstallViewModel mUninstallViewModel;
    private UninstallRepository mUninstallRepository;
    private FragmentManager mFragmentManager;
    private NotificationManager mNotificationManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -64,6 +67,7 @@ public class UninstallLaunch extends FragmentActivity implements UninstallAction
        super.onCreate(null);

        mFragmentManager = getSupportFragmentManager();
        mNotificationManager = getSystemService(NotificationManager.class);

        mUninstallRepository = new UninstallRepository(getApplicationContext());
        mUninstallViewModel = new ViewModelProvider(this,
@@ -110,9 +114,16 @@ public class UninstallLaunch extends FragmentActivity implements UninstallAction
            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());