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

Commit 1ae86af2 authored by Sumedh Sen's avatar Sumedh Sen
Browse files

Initiate uninstall on confirmation from the user

Once the user grants permission to uninstall, get an uninstall id, register a broadcast receiver and kick off the uninstall.

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

Change-Id: I81177783df4f2b9357b2fd90cc8284f02befd204
parent c82a365b
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static com.android.packageinstaller.v2.model.uninstallstagedata.Uninstall

import android.Manifest;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.usage.StorageStats;
import android.app.usage.StorageStatsManager;
import android.content.ComponentName;
@@ -39,18 +40,22 @@ 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.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
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.v2.model.uninstallstagedata.UninstallAborted;
import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallReady;
import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
import java.io.IOException;
import java.util.List;
@@ -58,10 +63,23 @@ import java.util.List;
public class UninstallRepository {

    private static final String TAG = UninstallRepository.class.getSimpleName();
    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 MutableLiveData<UninstallStage> mUninstallResult = new MutableLiveData<>();
    public UserHandle mUninstalledUser;
    public UninstallCompleteCallback mCallback;
    private ApplicationInfo mTargetAppInfo;
@@ -72,6 +90,7 @@ public class UninstallRepository {
    private String mCallingActivity;
    private boolean mUninstallFromAllUsers;
    private boolean mIsClonedApp;
    private int mUninstallId;

    public UninstallRepository(Context context) {
        mContext = context;
@@ -371,6 +390,77 @@ public class UninstallRepository {
        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) {
    }

    /**
     * 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 MutableLiveData<UninstallStage> getUninstallResult() {
        return mUninstallResult;
    }

    public static class CallerInfo {

        private final String mActivityName;
+43 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -32,9 +32,11 @@ 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.UninstallStage;
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;

@@ -95,6 +97,15 @@ public class UninstallLaunch extends FragmentActivity implements UninstallAction
            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 {
            Log.e(TAG, "Invalid stage: " + uninstallStage.getStageCode());
            showDialogInner(null);
@@ -126,6 +137,7 @@ public class UninstallLaunch extends FragmentActivity implements UninstallAction

    @Override
    public void onPositiveResponse(boolean keepData) {
        mUninstallViewModel.initiateUninstall(keepData);
    }

    @Override
+55 −0
Original line number Diff line number Diff line
/*
 * 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.fragments;

import android.app.AlertDialog;
import android.app.Dialog;
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;

/**
 * Dialog to show that the app is uninstalling.
 */
public class UninstallUninstallingFragment extends DialogFragment {

    UninstallUninstalling mDialogData;

    public UninstallUninstallingFragment(UninstallUninstalling dialogData) {
        mDialogData = dialogData;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
            .setCancelable(false);
        if (mDialogData.isCloneUser()) {
            builder.setTitle(requireContext().getString(R.string.uninstalling_cloned_app,
                mDialogData.getAppLabel()));
        } else {
            builder.setTitle(requireContext().getString(R.string.uninstalling_app,
                mDialogData.getAppLabel()));
        }
        Dialog dialog = builder.create();
        dialog.setCanceledOnTouchOutside(false);

        return dialog;
    }
}
+16 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ 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;
@@ -29,7 +30,8 @@ public class UninstallViewModel extends AndroidViewModel {

    private static final String TAG = UninstallViewModel.class.getSimpleName();
    private final UninstallRepository mRepository;
    private final MutableLiveData<UninstallStage> mCurrentUninstallStage = new MutableLiveData<>();
    private final MediatorLiveData<UninstallStage> mCurrentUninstallStage =
        new MediatorLiveData<>();

    public UninstallViewModel(@NonNull Application application, UninstallRepository repository) {
        super(application);
@@ -47,4 +49,17 @@ public class UninstallViewModel extends AndroidViewModel {
        }
        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);
            }
        });
    }
}