diff options
| author | 2023-10-31 16:17:57 -0700 | |
|---|---|---|
| committer | 2023-11-22 16:57:12 -0800 | |
| commit | 1ae86af2b67620c1657d6ab43ea0bd036339fd81 (patch) | |
| tree | aa71b2750acb4b1fcfcbbd5c2efb4fc9ed861c46 | |
| parent | c82a365bd6ff4b0664fe6912c3e2faa8b19bb6ff (diff) | |
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
5 files changed, 216 insertions, 1 deletions
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 71fe8c8ca7e8..2f371a96ab12 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 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; 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 000000000000..f5156cb676e9 --- /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 e22a59ebe4da..0c2eb502c395 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; 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 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 000000000000..23cc421890ac --- /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 1697832a13d1..690f7793b4fb 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.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); + } + }); + } } |