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);
+            }
+        });
+    }
 }