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

Commit 50e7bc41 authored by Ivan Chiang's avatar Ivan Chiang
Browse files

[PM] Support better transition in PIA V2 (3/N)

Migrate uninstallation fragments to one new UninstallationFragment

Flag: android.content.pm.use_pia_v2
Test: atest CtsPackageInstallerCUJUninstallationTestCases
Bug: 274120822
Bug: 402448072
Change-Id: I2bb5380e1a71655852d8e247c3e71dca7c6f2c20
parent 2fdf5019
Loading
Loading
Loading
Loading
+35 −12
Original line number Diff line number Diff line
@@ -37,9 +37,7 @@ import com.android.packageinstaller.v2.model.UninstallFailed
import com.android.packageinstaller.v2.model.UninstallRepository
import com.android.packageinstaller.v2.model.UninstallStage
import com.android.packageinstaller.v2.model.UninstallSuccess
import com.android.packageinstaller.v2.model.UninstallUserActionRequired
import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment
import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment
import com.android.packageinstaller.v2.ui.fragments.UninstallationFragment
import com.android.packageinstaller.v2.viewmodel.UninstallViewModel
import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory

@@ -52,6 +50,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener {
            UninstallLaunch::class.java.packageName + ".callingActivityName"
        private val LOG_TAG = UninstallLaunch::class.java.simpleName
        private const val TAG_DIALOG = "dialog"
        private const val TAG_UNINSTALLATION_DIALOG = "uninstallation-dialog"
        private const val ARGS_SAVED_INTENT = "saved_intent"
    }

@@ -112,8 +111,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener {
                    UninstallAborted.ABORT_REASON_APP_UNAVAILABLE,
                    UninstallAborted.ABORT_REASON_UNKNOWN,
                    UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED -> {
                        val errorDialog = UninstallErrorFragment.newInstance(aborted)
                        showDialogInner(errorDialog)
                        showUninstallationDialog()
                    }

                    else -> {
@@ -123,9 +121,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener {
            }

            UninstallStage.STAGE_USER_ACTION_REQUIRED -> {
                val uar = uninstallStage as UninstallUserActionRequired
                val confirmationDialog = UninstallConfirmationFragment.newInstance(uar)
                showDialogInner(confirmationDialog)
                showUninstallationDialog()
            }

            UninstallStage.STAGE_FAILED -> {
@@ -153,15 +149,42 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener {
        }
    }

    private fun showUninstallationDialog() {
        val fragment = getUninstallationFragment() ?: UninstallationFragment()
        fragment.updateUI()
        showDialogInner(fragment, TAG_UNINSTALLATION_DIALOG)
    }

    /**
     * Replace any visible dialog by the dialog returned by InstallRepository
     * Replace any visible dialog by the dialog returned by UninstallRepository with the tag
     * TAG_DIALOG.
     *
     * @param newDialog The new dialog to display
     */
    private fun showDialogInner(newDialog: DialogFragment?) {
        val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment?
        currentDialog?.dismissAllowingStateLoss()
        newDialog?.show(fragmentManager!!, TAG_DIALOG)
        showDialogInner(newDialog, TAG_DIALOG)
    }
    private fun showDialogInner(newDialog: DialogFragment?, tag: String) {
        var currentTag: String? = null
        if (tag == TAG_UNINSTALLATION_DIALOG) {
            if (getUninstallationFragment() != null) {
                return
            }
            currentTag = TAG_DIALOG
        } else {
            currentTag = TAG_UNINSTALLATION_DIALOG
        }

        val currentDialog = fragmentManager!!.findFragmentByTag(currentTag)
        if (currentDialog is DialogFragment) {
            currentDialog.dismissAllowingStateLoss()
        }
        newDialog?.show(fragmentManager!!, tag)
    }

    private fun getUninstallationFragment(): UninstallationFragment? {
        return (fragmentManager!!.findFragmentByTag(TAG_UNINSTALLATION_DIALOG)
            ?: return null) as UninstallationFragment?
    }

    fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
+0 −144
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 static android.text.format.Formatter.formatFileSize;

import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_DATA_SIZE;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_BUTTON_TEXT;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_MESSAGE;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_TITLE;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;

import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
import com.android.packageinstaller.v2.model.UninstallUserActionRequired;
import com.android.packageinstaller.v2.ui.UiUtil;
import com.android.packageinstaller.v2.ui.UninstallActionListener;

/**
 * Dialog to show while requesting user confirmation for uninstalling an app.
 */
public class UninstallConfirmationFragment extends DialogFragment {

    private static final String LOG_TAG = UninstallConfirmationFragment.class.getSimpleName();
    private UninstallUserActionRequired mDialogData;
    private UninstallActionListener mUninstallActionListener;
    private CheckBox mKeepData;

    public UninstallConfirmationFragment() {
        // Required for DialogFragment
    }

    /**
     * Create a new instance of this fragment with necessary data set as fragment arguments
     *
     * @param dialogData {@link UninstallUserActionRequired} object containing data to
     *                   display in the dialog
     * @return an instance of the fragment
     */
    public static UninstallConfirmationFragment newInstance(
            @NonNull UninstallUserActionRequired dialogData) {
        Bundle args = new Bundle();
        args.putString(ARGS_TITLE, dialogData.getTitle());
        args.putString(ARGS_MESSAGE, dialogData.getMessage());
        args.putString(ARGS_BUTTON_TEXT, dialogData.getButtonText());
        args.putLong(ARGS_APP_DATA_SIZE, dialogData.getAppDataSize());
        args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet());

        UninstallConfirmationFragment fragment = new UninstallConfirmationFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        mUninstallActionListener = (UninstallActionListener) context;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        setDialogData(requireArguments());

        Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData);

        // There is no root view here. Ok to pass null view root
        @SuppressWarnings("InflateParams")
        View dialogView = getLayoutInflater().inflate(R.layout.uninstall_fragment_layout, null);
        dialogView.requireViewById(R.id.app_snippet).setVisibility(View.VISIBLE);
        ((ImageView) dialogView.requireViewById(R.id.app_icon))
            .setImageDrawable(mDialogData.getAppIcon());
        ((TextView) dialogView.requireViewById(R.id.app_label)).setText(mDialogData.getAppLabel());

        if (mDialogData.getMessage() != null) {
            TextView customMessage = dialogView.requireViewById(R.id.custom_message);
            customMessage.setText(mDialogData.getMessage());
            customMessage.setVisibility(View.VISIBLE);
        }

        long appDataSize = mDialogData.getAppDataSize();
        if (appDataSize != 0) {
            View keepDataLayout = dialogView.requireViewById(R.id.keep_data_layout);
            keepDataLayout.setVisibility(View.VISIBLE);

            TextView keepDataBytes = keepDataLayout.requireViewById(R.id.keep_data_bytes);
            keepDataBytes.setText(formatFileSize(getContext(), appDataSize));
            keepDataLayout.setOnClickListener(v -> mKeepData.toggle());
            mKeepData = keepDataLayout.requireViewById(R.id.keep_data_checkbox);
        }

        return UiUtil.getAlertDialog(requireContext(), mDialogData.getTitle(), dialogView,
                mDialogData.getButtonText(), getString(R.string.button_cancel),
                (dialogInt, which) -> mUninstallActionListener.onPositiveResponse(
                        mKeepData != null && mKeepData.isChecked()),
                (dialogInt, which) ->
                        mUninstallActionListener.onNegativeResponse());
    }

    @Override
    public void onCancel(@NonNull DialogInterface dialog) {
        super.onCancel(dialog);
        mUninstallActionListener.onNegativeResponse();
    }

    private void setDialogData(Bundle args) {
        long appDataSize = args.getLong(ARGS_APP_DATA_SIZE);
        String buttonText = args.getString(ARGS_BUTTON_TEXT);
        String message = args.getString(ARGS_MESSAGE);
        AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class);
        String title = args.getString(ARGS_TITLE);

        mDialogData = new UninstallUserActionRequired(title, message, buttonText, appDataSize,
            appSnippet);
    }
}
+0 −105
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 static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ABORT_REASON;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;

import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.UninstallAborted;
import com.android.packageinstaller.v2.ui.UiUtil;
import com.android.packageinstaller.v2.ui.UninstallActionListener;

/**
 * Dialog to show when an app cannot be uninstalled
 */
public class UninstallErrorFragment extends DialogFragment {

    private static final String LOG_TAG = UninstallErrorFragment.class.getSimpleName();
    private UninstallAborted mDialogData;
    private UninstallActionListener mUninstallActionListener;

    public UninstallErrorFragment() {
        // Required for DialogFragment
    }

    /**
     * Create a new instance of this fragment with necessary data set as fragment arguments
     *
     * @param dialogData {@link UninstallAborted} object containing data to display in the
     *                   dialog
     * @return an instance of the fragment
     */
    public static UninstallErrorFragment newInstance(UninstallAborted dialogData) {
        Bundle args = new Bundle();
        args.putInt(ARGS_ABORT_REASON, dialogData.getAbortReason());

        UninstallErrorFragment fragment = new UninstallErrorFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        mUninstallActionListener = (UninstallActionListener) context;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        setDialogData(requireArguments());

        Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData);

        // There is no root view here. Ok to pass null view root
        @SuppressWarnings("InflateParams")
        View dialogView = getLayoutInflater().inflate(R.layout.uninstall_fragment_layout, null);
        final TextView customMessage = dialogView.requireViewById(R.id.custom_message);
        customMessage.setVisibility(View.VISIBLE);
        customMessage.setText(mDialogData.getDialogTextResource());

        return UiUtil.getAlertDialog(requireContext(),
                getString(mDialogData.getDialogTitleResource()),
                dialogView, /* positiveBtnText= */ null, getString(R.string.button_close),
                /* positiveBtnListener= */ null,
                (dialogInt, which) -> mUninstallActionListener.onNegativeResponse(),
                /* themeResId= */ 0);
    }

    @Override
    public void onCancel(@NonNull DialogInterface dialog) {
        super.onCancel(dialog);
        mUninstallActionListener.onNegativeResponse();
    }

    private void setDialogData(Bundle args) {
        int abortReason = args.getInt(ARGS_ABORT_REASON);
        mDialogData = new UninstallAborted(abortReason);
    }
}
+205 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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
 *
 *      http://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 static android.text.format.Formatter.formatFileSize;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;

import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.UninstallAborted;
import com.android.packageinstaller.v2.model.UninstallStage;
import com.android.packageinstaller.v2.model.UninstallUserActionRequired;
import com.android.packageinstaller.v2.ui.UiUtil;
import com.android.packageinstaller.v2.ui.UninstallActionListener;
import com.android.packageinstaller.v2.viewmodel.UninstallViewModel;

/**
 * Dialog to show while requesting uninstalling an app.
 */
public class UninstallationFragment extends DialogFragment {

    private static final String LOG_TAG = UninstallationFragment.class.getSimpleName();
    private UninstallActionListener mUninstallActionListener;
    private Dialog mDialog;

    private ImageView mAppIcon = null;
    private TextView mAppLabelTextView = null;
    private View mAppSnippet = null;
    private CheckBox mKeepDataCheckbox = null;
    private View mKeepDataLayout = null;
    private TextView mCustomMessageTextView = null;
    private TextView mKeepDataBytesTextView = null;

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        mUninstallActionListener = (UninstallActionListener) context;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        final UninstallStage uninstallStage = getCurrentUninstallStage();
        Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + uninstallStage);

        // There is no root view here. Ok to pass null view root
        @SuppressWarnings("InflateParams")
        View dialogView = getLayoutInflater().inflate(R.layout.uninstall_fragment_layout, null);
        mAppSnippet = dialogView.requireViewById(R.id.app_snippet);
        mAppIcon = dialogView.requireViewById(R.id.app_icon);
        mAppLabelTextView = dialogView.requireViewById(R.id.app_label);
        mCustomMessageTextView = dialogView.requireViewById(R.id.custom_message);
        mKeepDataLayout = dialogView.requireViewById(R.id.keep_data_layout);
        mKeepDataBytesTextView = mKeepDataLayout.requireViewById(R.id.keep_data_bytes);
        mKeepDataCheckbox = mKeepDataLayout.requireViewById(R.id.keep_data_checkbox);

        mDialog = UiUtil.getAlertDialog(requireContext(), getString(R.string.title_uninstall),
                dialogView, R.string.button_uninstall, R.string.button_cancel,
                /* positiveBtnListener= */ null, (dialogInt, which) ->
                        mUninstallActionListener.onNegativeResponse());

        return mDialog;
    }

    @Override
    public void onCancel(@NonNull DialogInterface dialog) {
        super.onCancel(dialog);
        mUninstallActionListener.onNegativeResponse();
    }

    @Override
    public void onStart() {
        super.onStart();
        updateUI();
    }

    private UninstallStage getCurrentUninstallStage() {
        return new ViewModelProvider(requireActivity()).get(UninstallViewModel.class)
                .getCurrentUninstallStage().getValue();
    }

    /**
     * Update the UI based on the current uninstall stage
     */
    public void updateUI() {
        if (!isAdded()) {
            return;
        }

        // Get the current uninstall stage
        final UninstallStage uninstallStage = getCurrentUninstallStage();

        switch (uninstallStage.getStageCode()) {
            case UninstallStage.STAGE_ABORTED -> {
                updateUninstallAbortedUI(mDialog, (UninstallAborted) uninstallStage);
            }
            case UninstallStage.STAGE_USER_ACTION_REQUIRED -> {
                updateUserActionRequiredUI(mDialog, (UninstallUserActionRequired) uninstallStage);
            }
        }
    }

    private void updateUninstallAbortedUI(Dialog dialog, UninstallAborted uninstallStage) {
        mAppSnippet.setVisibility(View.GONE);
        mKeepDataLayout.setVisibility(View.GONE);
        mCustomMessageTextView.setVisibility(View.VISIBLE);

        // Set the title and the message
        dialog.setTitle(uninstallStage.getDialogTitleResource());
        mCustomMessageTextView.setText(uninstallStage.getDialogTextResource());

        // Hide the positive button
        Button positiveButton = UiUtil.getAlertDialogPositiveButton(dialog);
        if (positiveButton != null) {
            positiveButton.setVisibility(View.GONE);
        }

        // Set the negative button and the listener
        Button negativeButton = UiUtil.getAlertDialogNegativeButton(dialog);
        if (negativeButton != null) {
            negativeButton.setText(R.string.button_close);
            negativeButton.setOnClickListener(view -> {
                mUninstallActionListener.onNegativeResponse();
            });
        }
    }

    private void updateUserActionRequiredUI(Dialog dialog,
            UninstallUserActionRequired uninstallStage) {
        mAppSnippet.setVisibility(View.VISIBLE);

        // Set app icon and label
        mAppIcon.setImageDrawable(uninstallStage.getAppIcon());
        mAppLabelTextView.setText(uninstallStage.getAppLabel());

        // Set title
        dialog.setTitle(uninstallStage.getTitle());

        // Set custom message
        if (uninstallStage.getMessage() != null) {
            mCustomMessageTextView.setVisibility(View.VISIBLE);
            mCustomMessageTextView.setText(uninstallStage.getMessage());
        } else {
            mCustomMessageTextView.setVisibility(View.GONE);
        }

        // Set keep data information
        long appDataSize = uninstallStage.getAppDataSize();
        if (appDataSize != 0) {
            mKeepDataLayout.setVisibility(View.VISIBLE);

            mKeepDataBytesTextView.setText(formatFileSize(getContext(), appDataSize));
            mKeepDataLayout.setOnClickListener(v -> mKeepDataCheckbox.toggle());
        } else {
            mKeepDataLayout.setVisibility(View.GONE);
        }

        // Set the positive button and the listener
        Button positiveButton = UiUtil.getAlertDialogPositiveButton(dialog);
        if (positiveButton != null) {
            positiveButton.setVisibility(View.VISIBLE);
            positiveButton.setText(uninstallStage.getButtonText());
            positiveButton.setOnClickListener(view -> {
                mUninstallActionListener.onPositiveResponse(mKeepDataCheckbox.isChecked());
            });
        }

        // Set the negative button and the listener
        Button negativeButton = UiUtil.getAlertDialogNegativeButton(dialog);
        if (negativeButton != null) {
            negativeButton.setText(R.string.button_cancel);
            negativeButton.setOnClickListener(view -> {
                mUninstallActionListener.onNegativeResponse();
            });
        }
    }
}