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

Commit 21f3cf1b authored by Sumedh Sen's avatar Sumedh Sen
Browse files

Implement new UI for Uninstall dialogs

Bug: 274120822
Test: Follow Pia Manual Test Plan
Flag: android.content.pm.use_pia_v2
Change-Id: I9d76ba0f4e338a460cae10e9a571f69513e24c96
parent 82f7c887
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -121,7 +121,8 @@
            android:configChanges="orientation|keyboardHidden|screenSize"
            android:excludeFromRecents="true"
            android:noHistory="true"
            android:exported="false">
            android:exported="false"
            android:theme="@style/Theme.PackageInstaller">
        </activity>

        <receiver android:name=".common.UninstallEventReceiver"
+35 −1
Original line number Diff line number Diff line
@@ -45,7 +45,19 @@
    <string name="title_install_failed_less_storage">Not enough storage</string>
    <string name="title_install_failed_not_installed">App not installed</string>

    <!-- TODO: These strings are placeholders, until UX finalizes strings for Uninstall flow. -->
    <string name="title_uninstall">Uninstall this app?</string>
    <string name="title_uninstall_clone">Uninstall this app clone?</string>
    <string name="title_uninstall_updates_system_app">Uninstall updates?</string>
    <string name="title_uninstall_all_users">Uninstall this app for all users?</string>
    <string name="title_uninstall_other_user">Uninstall this app for another user?</string>
    <string name="title_uninstall_app_not_found">App not found</string>
    <string name="title_uninstall_user_not_allowed">Can\'t uninstall this app</string>
    <string name="title_uninstall_failed">App not uninstalled</string>

    <string name="title_archive">Archive this app?</string>
    <string name="title_archive_other_user">Archive this app for another user?</string>
    <!-- End of placeholder strings -->
    <!-- Dialog Titles end -->

    <!-- Dialog Messages -->
@@ -64,8 +76,27 @@
    <string name="message_no_install_apps_restriction">Installing apps has been restricted</string>
    <string name="message_no_install_unknown_apps_restriction">Installing unknown apps has been restricted</string>

    <!-- TODO: These strings are placeholders, until UX finalizes strings for Uninstall flow. -->
    <string name="message_uninstall_keep_data">Keep <xliff:g id="size" example="1.5MB">%1$s</xliff:g> of app data.</string>
    <string name="message_uninstall_with_clone_instance">A clone of this app exists and will also be deleted</string>
    <string name="message_delete_clone_app">This app is a clone of <xliff:g id="app_label" example="Example App">%1$s</xliff:g></string>
    <string name="message_uninstall_activity"><xliff:g id="activity_name">%1$s</xliff:g> is part of the following app: <xliff:g id="app_name">%2$s</xliff:g></string>
    <string name="message_uninstall_updates_system_app">This app will be replaced with the factory version and all app data will be removed</string>
    <string name="message_uninstall_updates_system_app_all_users">This app will be replaced with the factory version and all app data will be removed. This will affect all users on the device, including those with work profiles.</string>
    <string name="message_uninstall_all_users">This app and its data will be removed for all users on this device</string>
    <string name="message_uninstall_other_user">This app will be uninstalled for user named \'<xliff:g id="user_name">%1$s</xliff:g>\'</string>
    <string name="message_uninstall_work_profile">This app will be uninstalled from your work profile</string>
    <string name="message_uninstall_private_space">This app will be uninstalled from your private space</string>
    <string name="message_uninstall_app_not_found">This app wasn\'t found in the list of installed apps, or has been already uninstalled</string>
    <string name="message_uninstall_user_not_allowed">The current user is not allowed to perform this uninstallation</string>
    <string name="message_uninstall_failed">This app could not be uninstalled</string>

    <string name="message_archive">Your app data will be saved</string>
    <string name="message_archive_all_users">This app will be archived for all users. App data will be saved</string>
    <string name="message_archive_other_user">This app will be archived for user named \'<xliff:g id="user_name">%1$s</xliff:g>\'. App data will be saved</string>
    <string name="message_archive_work_profile">This app will be archived from your work profile. App data will be saved</string>
    <string name="message_archive_private_space">This app will be archived from your private space. App data will be saved</string>
    <!-- End of placeholder strings -->
    <!-- Dialog Messages end -->

    <!-- Dialog Buttons -->
@@ -80,9 +111,12 @@
    <string name="button_uninstall">Uninstall</string>
    <string name="button_reinstall">Reinstall</string>
    <string name="button_manage_apps">Manage apps</string>
    <string name="button_archive">Archive</string>
    <string name="button_uninstall_updates_system_app">Uninstall updates</string>
    <!-- Dialog Buttons end -->

    <!-- Miscellaneous -->
    <string name="string_cloned_app_label"><xliff:g id="package_label">%1$s</xliff:g> Clone</string>
    <!-- Miscellaneous end -->

    <!-- Package Installer App V2 Strings section end -->
+8 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.content.pm.Flags.usePiaV2;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;

import static com.android.packageinstaller.PackageUtil.getMaxTargetSdkVersionForUid;
import static com.android.packageinstaller.PackageUtil.getReasonForDebug;

import android.Manifest;
import android.app.Activity;
@@ -46,6 +47,7 @@ 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;
@@ -91,8 +93,12 @@ public class UninstallerActivity extends Activity {
        // be stale, if e.g. the app was uninstalled while the activity was destroyed.
        super.onCreate(null);

        if (usePiaV2() && !isTv()) {
            Log.i(TAG, "Using Pia V2");
        boolean testOverrideForPiaV2 = Settings.System.getInt(getContentResolver(),
                "use_pia_v2", 0) == 1;
        boolean usePiaV2aConfig = usePiaV2();

        if ((usePiaV2aConfig || testOverrideForPiaV2) && !isTv()) {
            Log.d(TAG, getReasonForDebug(usePiaV2aConfig, testOverrideForPiaV2));

            boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
            Intent piaV2 = new Intent(getIntent());
+1 −2
Original line number Diff line number Diff line
@@ -63,12 +63,11 @@ object PackageUtil {
    const val ARGS_APP_DATA_SIZE: String = "app_data_size"
    const val ARGS_APP_LABEL: String = "app_label"
    const val ARGS_APP_SNIPPET: String = "app_snippet"
    const val ARGS_BUTTON_TEXT: String = "button_text"
    const val ARGS_ERROR_DIALOG_TYPE: String = "error_dialog_type"
    const val ARGS_EXISTING_OWNER: String = "existing_owner"
    const val ARGS_INSTALLER_LABEL: String = "installer_label"
    const val ARGS_INSTALLER_PACKAGE: String = "installer_pkg"
    const val ARGS_IS_ARCHIVE: String = "is_archive"
    const val ARGS_IS_CLONE_USER: String = "clone_user"
    const val ARGS_LEGACY_CODE: String = "legacy_code"
    const val ARGS_MESSAGE: String = "message"
    const val ARGS_INSTALL_TYPE: String = "new_app_state"
+106 −83
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ import com.android.packageinstaller.R
import com.android.packageinstaller.common.EventResultPersister
import com.android.packageinstaller.common.EventResultPersister.OutOfIdsException
import com.android.packageinstaller.common.UninstallEventReceiver
import com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet
import com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid
import com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid
import com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted
@@ -211,6 +212,8 @@ class UninstallRepository(private val context: Context) {

    fun generateUninstallDetails(): UninstallStage {
        val messageBuilder = StringBuilder()
        var dialogTitle = context.getString(R.string.title_uninstall)
        var positiveButtonText = context.getString(R.string.button_uninstall)

        targetAppLabel = targetAppInfo!!.loadSafeLabel(packageManager)

@@ -220,33 +223,45 @@ class UninstallRepository(private val context: Context) {
            val activityLabel = targetActivityInfo!!.loadSafeLabel(packageManager)
            if (!activityLabel.contentEquals(targetAppLabel)) {
                messageBuilder.append(
                    context.getString(R.string.uninstall_activity_text, activityLabel)
                    context.getString(
                        R.string.message_uninstall_activity,
                        activityLabel, targetAppLabel)
                )
                messageBuilder.append(" ").append(targetAppLabel).append(".\n\n")
                dialogTitle = context.getString(R.string.title_uninstall)
            }
        }

        val isUpdate = (targetAppInfo!!.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
        val isUpdatedSystemApp =
            (targetAppInfo!!.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
        val isArchive =
            PmFlags.archiving() && ((deleteFlags and PackageManager.DELETE_ARCHIVE) != 0)
        val myUserHandle = Process.myUserHandle()
        val isSingleUser = isSingleUser()
        val isSingleUserOnDevice = isSingleUserOnDevice()

        if (isArchive) {
            positiveButtonText = context.getString(R.string.button_archive)
        }

        if (isUpdate) {
        if (isUpdatedSystemApp) {
            dialogTitle = context.getString(R.string.title_uninstall_updates_system_app)
            positiveButtonText = context.getString(R.string.button_uninstall_updates_system_app)
            messageBuilder.append(
                context.getString(
                    if (isSingleUser) {
                        R.string.uninstall_update_text
                    if (isSingleUserOnDevice) {
                        R.string.message_uninstall_updates_system_app
                    } else {
                        R.string.uninstall_update_text_multiuser
                        R.string.message_uninstall_updates_system_app_all_users
                    }
                )
            )
        } else if (uninstallFromAllUsers && !isSingleUser) {
            val messageString = if (isArchive) {
                context.getString(R.string.archive_application_text_all_users)
        } else if (uninstallFromAllUsers && !isSingleUserOnDevice) {
            var messageString: String
            if (isArchive) {
                messageString = context.getString(R.string.message_archive_all_users)
                dialogTitle = context.getString(R.string.title_archive)
            } else {
                context.getString(R.string.uninstall_application_text_all_users)
                messageString = context.getString(R.string.message_uninstall_all_users)
                dialogTitle = context.getString(R.string.title_uninstall_all_users)
            }
            messageBuilder.append(messageString)
        } else if (uninstalledUser != myUserHandle) {
@@ -255,90 +270,102 @@ class UninstallRepository(private val context: Context) {
                .getSystemService(UserManager::class.java)
            val userName = customUserManager!!.userName

            var messageString = if (isArchive) {
                context.getString(R.string.archive_application_text_user, userName)
            var messageString: String
            if (isArchive) {
                messageString = context.getString(R.string.message_archive_other_user, userName)
                dialogTitle = context.getString(R.string.title_archive_other_user)
            } else {
                context.getString(R.string.uninstall_application_text_user, userName)
                messageString = context.getString(R.string.message_uninstall_other_user, userName)
                dialogTitle = context.getString(R.string.title_uninstall_other_user)
            }

            if (userManager!!.isSameProfileGroup(myUserHandle, uninstalledUser!!)) {
                if (customUserManager.isManagedProfile) {
                    messageString = if (isArchive) {
                        context.getString(
                            R.string.archive_application_text_current_user_work_profile, userName
                        )
                    if (isArchive) {
                        messageString = context.getString(R.string.message_archive_work_profile)
                        dialogTitle = context.getString(R.string.title_archive)
                    } else {
                        context.getString(
                            R.string.uninstall_application_text_current_user_work_profile, userName
                        )
                        messageString = context.getString(R.string.message_uninstall_work_profile)
                        dialogTitle = context.getString(R.string.title_uninstall)
                    }
                } else if (customUserManager.isCloneProfile) {
                    isClonedApp = true
                    messageString = context.getString(
                            R.string.uninstall_application_text_current_user_clone_profile
                            R.string.message_delete_clone_app, targetAppLabel
                    )
                    dialogTitle = context.getString(R.string.title_uninstall_clone)
                } else if (Flags.allowPrivateProfile()
                        && MultiuserFlags.enablePrivateSpaceFeatures()
                        && customUserManager.isPrivateProfile
                ) {
                    // TODO(b/324244123): Get these Strings from a User Property API.
                    messageString = if (isArchive) {
                        context.getString(
                            R.string.archive_application_text_current_user_private_profile, userName
                        )
                    if (isArchive) {
                        messageString = context.getString(R.string.message_archive_private_space)
                        dialogTitle = context.getString(R.string.title_archive)
                    } else {
                        context.getString(
                            R.string.uninstall_application_text_current_user_private_profile
                        )
                        messageString = context.getString(R.string.message_uninstall_private_space)
                        dialogTitle = context.getString(R.string.title_uninstall)
                    }
                }
            }
            messageBuilder.append(messageString)
        } else if (isCloneProfile(uninstalledUser!!)) {
            isClonedApp = true
            dialogTitle = context.getString(R.string.title_uninstall_clone)
            messageBuilder.append(
                context.getString(
                    R.string.uninstall_application_text_current_user_clone_profile
                    R.string.message_delete_clone_app, targetAppLabel
                )
            )
        } else if (isPrivateSpace(uninstalledUser!!)) {
            var messageString: String
            if (isArchive) {
                messageString = context.getString(R.string.message_archive_private_space)
                dialogTitle = context.getString(R.string.title_archive)
            } else {
                messageString = context.getString(R.string.message_uninstall_private_space)
                dialogTitle = context.getString(R.string.title_uninstall)
            }
            messageBuilder.append(messageString)
        } else if (myUserHandle == UserHandle.SYSTEM &&
            hasClonedInstance(targetAppInfo!!.packageName)
        ) {
            messageBuilder.append(
                context.getString(
                    R.string.uninstall_application_text_with_clone_instance,
                    targetAppLabel
                )
            )
            dialogTitle = context.getString(R.string.title_uninstall)
            messageBuilder.append(context.getString(R.string.message_uninstall_with_clone_instance))
        } else if (isArchive) {
            messageBuilder.append(context.getString(R.string.archive_application_text))
        } else {
            messageBuilder.append(context.getString(R.string.uninstall_application_text))
            dialogTitle = context.getString(R.string.title_archive)
            messageBuilder.append(context.getString(R.string.message_archive))
        }

        val message = messageBuilder.toString()

        val title = if (isClonedApp) {
            context.getString(R.string.cloned_app_label, targetAppLabel)
        } else if (isArchive) {
            context.getString(R.string.archiving_app_label, targetAppLabel)
        val message = if (messageBuilder.isNotEmpty()) {
            messageBuilder.toString()
        } else {
            targetAppLabel.toString()
            null
        }

        var suggestToKeepAppData = false
        try {
            val pkgInfo = packageManager.getPackageInfo(
        val pkgInfo = try {
            packageManager.getPackageInfo(
                targetPackageName!!, PackageInfoFlags.of(PackageManager.MATCH_ARCHIVED_PACKAGES)
            )
            suggestToKeepAppData =
                pkgInfo.applicationInfo != null
                    && pkgInfo.applicationInfo!!.hasFragileUserData()
                    && !isArchive
        } catch (e: PackageManager.NameNotFoundException) {
            Log.e(LOG_TAG, "Cannot check hasFragileUserData for $targetPackageName", e)
            Log.e(LOG_TAG, "Cannot get packageInfo for $targetPackageName", e)
            null
        }

        // Create a context from the user from where we need to uninstall the app to help get
        // correctly badged icon (e.g badging for work profile, private space)
        val userContext = context.createContextAsUser(uninstalledUser!!, 0)
        val appSnippet: PackageUtil.AppSnippet? = pkgInfo?.let { getAppSnippet(userContext, it) }
        appSnippet?.label = if (isClonedApp) {
            context.getString(R.string.string_cloned_app_label, targetAppLabel)
        } else {
            targetAppLabel.toString()
        }

        var suggestToKeepAppData = pkgInfo?.applicationInfo != null
                && (pkgInfo.applicationInfo?.hasFragileUserData() == true)
                && !isArchive

        var appDataSize: Long = 0
        if (suggestToKeepAppData) {
            appDataSize = getAppDataSize(
@@ -347,7 +374,7 @@ class UninstallRepository(private val context: Context) {
            )
        }

        return UninstallUserActionRequired(title, message, appDataSize, isArchive)
        return UninstallUserActionRequired(dialogTitle, message, positiveButtonText, appDataSize, appSnippet)
    }

    /**
@@ -357,7 +384,7 @@ class UninstallRepository(private val context: Context) {
     * [android.os.UserManager.isHeadlessSystemUserMode], the system user is not "full",
     * so it's not be considered in the calculation.
     */
    private fun isSingleUser(): Boolean {
    private fun isSingleUserOnDevice(): Boolean {
        val userCount = userManager!!.userCount
        return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2)
    }
@@ -390,6 +417,12 @@ class UninstallRepository(private val context: Context) {
        return customUserManager!!.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)
    }

    private fun isPrivateSpace(userHandle: UserHandle): Boolean {
        val customUserManager = context.createContextAsUser(userHandle, 0)
            .getSystemService(UserManager::class.java)
        return customUserManager!!.isUserOfType(UserManager.USER_TYPE_PROFILE_PRIVATE)
    }

    /**
     * Get number of bytes of the app data of the package.
     *
@@ -449,9 +482,6 @@ class UninstallRepository(private val context: Context) {
            return
        }

        // TODO: Check with UX whether to show UninstallUninstalling dialog / notification?
        uninstallResult.value = UninstallUninstalling(targetAppLabel, isClonedApp)

        val uninstallData = Bundle()
        uninstallData.putInt(EXTRA_UNINSTALL_ID, uninstallId)
        uninstallData.putString(EXTRA_PACKAGE_NAME, targetPackageName)
@@ -472,21 +502,13 @@ class UninstallRepository(private val context: Context) {
            broadcastIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
        )
        if (!startUninstall(
        startUninstall(
            targetPackageName!!,
            uninstalledUser!!,
            pendingIntent,
            uninstallFromAllUsers,
            keepData
        )
        ) {
            handleUninstallResult(
                PackageInstaller.STATUS_FAILURE,
                PackageManager.DELETE_FAILED_INTERNAL_ERROR,
                null,
                0
            )
        }
    }

    private fun handleUninstallResult(
@@ -761,9 +783,6 @@ class UninstallRepository(private val context: Context) {

    /**
     * Starts an uninstall for the given package.
     *
     * @return `true` if there was no exception while uninstalling. This does not represent
     * the result of the uninstall. Result will be made available in [handleUninstallResult]
     */
    private fun startUninstall(
        packageName: String,
@@ -771,22 +790,26 @@ class UninstallRepository(private val context: Context) {
        pendingIntent: PendingIntent,
        uninstallFromAllUsers: Boolean,
        keepData: Boolean
    ): Boolean {
    ) {
        var flags = if (uninstallFromAllUsers) PackageManager.DELETE_ALL_USERS else 0
        flags = flags or if (keepData) PackageManager.DELETE_KEEP_DATA else 0
        flags = flags or deleteFlags

        return try {
        try {
            context.createContextAsUser(targetUser, 0)
                .packageManager.packageInstaller.uninstall(
                    VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
                    flags,
                    pendingIntent.intentSender
                )
            true
        } catch (e: IllegalArgumentException) {
            Log.e(LOG_TAG, "Failed to uninstall", e)
            false
            handleUninstallResult(
                PackageInstaller.STATUS_FAILURE,
                PackageManager.DELETE_FAILED_INTERNAL_ERROR,
                null,
                0
            )
        }
    }

Loading