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
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 71fe8c8..2f371a9 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
@@ -28,6 +28,7 @@
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.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 @@
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 @@
private String mCallingActivity;
private boolean mUninstallFromAllUsers;
private boolean mIsClonedApp;
+ private int mUninstallId;
public UninstallRepository(Context context) {
mContext = context;
@@ -371,6 +390,77 @@
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;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
new file mode 100644
index 0000000..f5156cb
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+ }
+}
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 e22a59e..0c2eb50 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
@@ -32,9 +32,11 @@
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 @@
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 @@
@Override
public void onPositiveResponse(boolean keepData) {
+ mUninstallViewModel.initiateUninstall(keepData);
}
@Override
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
new file mode 100644
index 0000000..23cc421
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
index 1697832..690f779 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
@@ -20,6 +20,7 @@
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 @@
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 @@
}
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);
+ }
+ });
+ }
}