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

Commit c08efc96 authored by Ivan Chiang's avatar Ivan Chiang
Browse files

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

- Migrate unarchive fragments to one new UnarchiveFragment

Flag: android.content.pm.use_pia_v2
Test: atest ArchiveTest
Bug: 274120822
Bug: 402448072
Change-Id: I490f715315652f5521262e59eef1175bb5224fb1
parent 50e7bc41
Loading
Loading
Loading
Loading
+0 −10
Original line number Diff line number Diff line
@@ -53,17 +53,7 @@ object PackageUtil {
    private const val SPLIT_APK_SUFFIX = ".apk"
    const val localLogv = false

    const val ARGS_ABORT_REASON: String = "abort_reason"
    const val ARGS_APP_DATA_SIZE: String = "app_data_size"
    const val ARGS_APP_SNIPPET: String = "app_snippet"
    const val ARGS_BUTTON_TEXT: String = "button_text"
    const val ARGS_INSTALLER_LABEL: String = "installer_label"
    const val ARGS_INSTALLER_PACKAGE: String = "installer_pkg"
    const val ARGS_MESSAGE: String = "message"
    const val ARGS_PENDING_INTENT: String = "pending_intent"
    const val ARGS_REQUIRED_BYTES: String = "required_bytes"
    const val ARGS_TITLE: String = "title"
    const val ARGS_UNARCHIVAL_STATUS: String = "unarchival_status"

    /**
     * Determines if the UID belongs to the system downloads provider and returns the
+36 −14
Original line number Diff line number Diff line
@@ -34,12 +34,9 @@ import androidx.lifecycle.ViewModelProvider
import com.android.packageinstaller.R
import com.android.packageinstaller.v2.model.PackageUtil
import com.android.packageinstaller.v2.model.UnarchiveAborted
import com.android.packageinstaller.v2.model.UnarchiveError
import com.android.packageinstaller.v2.model.UnarchiveRepository
import com.android.packageinstaller.v2.model.UnarchiveStage
import com.android.packageinstaller.v2.model.UnarchiveUserActionRequired
import com.android.packageinstaller.v2.ui.fragments.UnarchiveConfirmationFragment
import com.android.packageinstaller.v2.ui.fragments.UnarchiveErrorFragment
import com.android.packageinstaller.v2.ui.fragments.UnarchiveFragment
import com.android.packageinstaller.v2.viewmodel.UnarchiveViewModel
import com.android.packageinstaller.v2.viewmodel.UnarchiveViewModelFactory

@@ -55,6 +52,7 @@ class UnarchiveLaunch : FragmentActivity(), UnarchiveActionListener {
        private val LOG_TAG = UnarchiveLaunch::class.java.simpleName

        private const val TAG_DIALOG = "dialog"
        private const val TAG_UNARCHIVE_DIALOG = "unarchive-dialog"

        private const val ACTION_UNARCHIVE_DIALOG: String =
            "com.android.intent.action.UNARCHIVE_DIALOG"
@@ -109,15 +107,11 @@ class UnarchiveLaunch : FragmentActivity(), UnarchiveActionListener {
            }

            UnarchiveStage.STAGE_USER_ACTION_REQUIRED -> {
                val uar = stage as UnarchiveUserActionRequired
                val confirmationDialog = UnarchiveConfirmationFragment.newInstance(uar)
                showDialogInner(confirmationDialog)
                showUnarchiveDialog()
            }

            UnarchiveStage.STAGE_ERROR -> {
                val error = stage as UnarchiveError
                val errorDialog = UnarchiveErrorFragment.newInstance(error)
                showDialogInner(errorDialog)
                showUnarchiveDialog()
            }
        }
    }
@@ -131,7 +125,7 @@ class UnarchiveLaunch : FragmentActivity(), UnarchiveActionListener {
        installerPkg: String?,
        pi: PendingIntent?
    ) {
        // Allow the error handling actvities to start in the background.
        // Allow the error handling activities to start in the background.
        val options = BroadcastOptions.makeBasic()
        options.setPendingIntentBackgroundActivityStartMode(
            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
@@ -176,6 +170,13 @@ class UnarchiveLaunch : FragmentActivity(), UnarchiveActionListener {
                // Do nothing. The rest of the dialogs are purely informational.
            }
        }
        finish()
    }

    private fun showUnarchiveDialog() {
        val fragment = getUnarchiveFragment() ?: UnarchiveFragment()
        fragment.updateUI()
        showDialogInner(fragment, TAG_UNARCHIVE_DIALOG)
    }

    /**
@@ -184,8 +185,29 @@ class UnarchiveLaunch : FragmentActivity(), UnarchiveActionListener {
     * @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_UNARCHIVE_DIALOG) {
            if (getUnarchiveFragment() != null) {
                return
            }
            currentTag = TAG_DIALOG
        } else {
            currentTag = TAG_UNARCHIVE_DIALOG
        }

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

    private fun getUnarchiveFragment(): UnarchiveFragment? {
        return (fragmentManager!!.findFragmentByTag(TAG_UNARCHIVE_DIALOG)
            ?: return null) as UnarchiveFragment?
    }
}
+0 −136
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 com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_INSTALLER_LABEL;

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.ImageView;
import android.widget.TextView;

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

import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.PackageUtil;
import com.android.packageinstaller.v2.model.UnarchiveUserActionRequired;
import com.android.packageinstaller.v2.ui.UiUtil;
import com.android.packageinstaller.v2.ui.UnarchiveActionListener;

public class UnarchiveConfirmationFragment extends DialogFragment {

    private static final String LOG_TAG = UnarchiveConfirmationFragment.class.getSimpleName();

    private Dialog mDialog;
    private UnarchiveUserActionRequired mDialogData;
    private UnarchiveActionListener mUnarchiveActionListener;

    UnarchiveConfirmationFragment() {
        // Required for DialogFragment
    }

    /**
     * Creates a new instance of this fragment with necessary data set as fragment arguments
     *
     * @param dialogData {@link UnarchiveUserActionRequired} object containing data to display
     *         in the dialog
     * @return an instance of the fragment
     */
    public static UnarchiveConfirmationFragment newInstance(
            @NonNull UnarchiveUserActionRequired dialogData) {
        Bundle args = new Bundle();
        args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet());
        args.putString(ARGS_INSTALLER_LABEL, dialogData.getInstallerTitle());

        UnarchiveConfirmationFragment dialog = new UnarchiveConfirmationFragment();
        dialog.setArguments(args);
        return dialog;
    }

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

    @NonNull
    @Override
    public Dialog onCreateDialog(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());

        TextView customMessage = dialogView.requireViewById(R.id.custom_message);
        customMessage.setVisibility(View.VISIBLE);
        customMessage.setText(getString(R.string.message_restore, mDialogData.getInstallerTitle()));

        mDialog = UiUtil.getAlertDialog(requireContext(), getString(R.string.title_restore),
                dialogView, R.string.button_restore, R.string.button_cancel,
                (dialog, which) -> mUnarchiveActionListener.beginUnarchive(),
                (dialog, which) -> {});
        return mDialog;
    }

    @Override
    public void onPause() {
        super.onPause();
        Button button = UiUtil.getAlertDialogPositiveButton(mDialog);
        if (button != null) {
            button.setEnabled(false);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Button button = UiUtil.getAlertDialogPositiveButton(mDialog);
        if (button != null) {
            button.setEnabled(true);
        }
    }

    @Override
    public void onDismiss(@NonNull DialogInterface dialog) {
        super.onDismiss(dialog);
        if (isAdded()) {
            requireActivity().finish();
        }
    }

    private void setDialogData(Bundle args) {
        PackageUtil.AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET,
                PackageUtil.AppSnippet.class);
        String installerTitle = args.getString(ARGS_INSTALLER_LABEL);

        mDialogData = new UnarchiveUserActionRequired(appSnippet, installerTitle);
    }
}
+274 −0
Original line number Diff line number Diff line
@@ -16,14 +16,7 @@

package com.android.packageinstaller.v2.ui.fragments;

import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_INSTALLER_LABEL;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_INSTALLER_PACKAGE;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_PENDING_INTENT;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_REQUIRED_BYTES;
import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_UNARCHIVAL_STATUS;

import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageInstaller;
@@ -33,46 +26,33 @@ import android.text.Html;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

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

import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.UnarchiveError;
import com.android.packageinstaller.v2.model.UnarchiveStage;
import com.android.packageinstaller.v2.model.UnarchiveUserActionRequired;
import com.android.packageinstaller.v2.ui.UiUtil;
import com.android.packageinstaller.v2.ui.UnarchiveActionListener;
import com.android.packageinstaller.v2.viewmodel.UnarchiveViewModel;

public class UnarchiveErrorFragment extends DialogFragment implements
        DialogInterface.OnClickListener {

    private static final String LOG_TAG = UnarchiveErrorFragment.class.getSimpleName();
    private UnarchiveError mDialogData;
    private UnarchiveActionListener mUnarchiveActionListener;
public class UnarchiveFragment extends DialogFragment {

    public UnarchiveErrorFragment() {
        // Required for DialogFragment
    }
    private static final String LOG_TAG = UnarchiveFragment.class.getSimpleName();

    /**
     * Creates a new instance of this fragment with necessary data set as fragment arguments
     *
     * @param dialogData {@link UnarchiveError} object containing data to display
     *                   in the dialog
     * @return an instance of the fragment
     */
    public static UnarchiveErrorFragment newInstance(UnarchiveError dialogData) {
        Bundle args = new Bundle();
        args.putInt(ARGS_UNARCHIVAL_STATUS, dialogData.getUnarchivalStatus());
        args.putLong(ARGS_REQUIRED_BYTES, dialogData.getRequiredBytes());
        args.putString(ARGS_INSTALLER_LABEL, dialogData.getInstallerAppTitle());
        args.putString(ARGS_INSTALLER_PACKAGE, dialogData.getInstallerPackageName());
        args.putParcelable(ARGS_PENDING_INTENT, dialogData.getPendingIntent());
    private Dialog mDialog;
    private UnarchiveActionListener mUnarchiveActionListener;

        UnarchiveErrorFragment dialog = new UnarchiveErrorFragment();
        dialog.setArguments(args);
        return dialog;
    }
    private ImageView mAppIcon = null;
    private TextView mAppLabelTextView = null;
    private View mAppSnippet = null;
    private TextView mCustomMessageTextView = null;

    @Override
    public void onAttach(@NonNull Context context) {
@@ -80,118 +60,215 @@ public class UnarchiveErrorFragment extends DialogFragment implements
        mUnarchiveActionListener = (UnarchiveActionListener) context;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        setDialogData(requireArguments());
        Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData);
        final UnarchiveStage unarchiveStage = getCurrentUnarchiveStage();

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

        // 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);
        mCustomMessageTextView.setVisibility(View.VISIBLE);

        mDialog = UiUtil.getAlertDialog(requireContext(), getString(R.string.title_restore),
                dialogView, R.string.button_restore, R.string.button_cancel,
                (dialog, which) -> mUnarchiveActionListener.beginUnarchive(),
                (dialog, which) -> {});
        return mDialog;
    }

    @Override
    public void onPause() {
        super.onPause();
        Button button = UiUtil.getAlertDialogPositiveButton(mDialog);
        if (button != null) {
            button.setEnabled(false);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        Button button = UiUtil.getAlertDialogPositiveButton(mDialog);
        if (button != null) {
            button.setEnabled(true);
        }
    }

    @Override
    public void onDismiss(@NonNull DialogInterface dialog) {
        super.onDismiss(dialog);
        if (isAdded()) {
            requireActivity().finish();
        }
    }

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

        return getDialog(dialogView);
    private UnarchiveStage getCurrentUnarchiveStage() {
        return new ViewModelProvider(requireActivity()).get(UnarchiveViewModel.class)
                .getCurrentUnarchiveStage().getValue();
    }

    private void setDialogData(Bundle args) {
        int status = args.getInt(ARGS_UNARCHIVAL_STATUS, -1);
        PendingIntent pendingIntent = args.getParcelable(ARGS_PENDING_INTENT, PendingIntent.class);
        long requiredBytes = args.getLong(ARGS_REQUIRED_BYTES);
        String installerPkgName = args.getString(ARGS_INSTALLER_PACKAGE);
        String installerAppTitle = args.getString(ARGS_INSTALLER_LABEL);
    /**
     * Update the UI based on the current unarchive stage
     */
    public void updateUI() {
        if (!isAdded()) {
            return;
        }

        // Get the current unarchive stage
        final UnarchiveStage unarchiveStage = getCurrentUnarchiveStage();

        mDialogData = new UnarchiveError(
                status, installerPkgName, installerAppTitle, requiredBytes, pendingIntent
        );
        switch (unarchiveStage.getStageCode()) {
            case UnarchiveStage.STAGE_ERROR -> {
                updateUnarchiveErrorUI(mDialog, (UnarchiveError) unarchiveStage);
            }
            case UnarchiveStage.STAGE_USER_ACTION_REQUIRED -> {
                updateUserActionRequiredUI(mDialog, (UnarchiveUserActionRequired) unarchiveStage);
            }
        }
    }

    private Dialog getDialog(@NonNull View dialogView) {
        final int status = mDialogData.getUnarchivalStatus();
        final String installerAppTitle = mDialogData.getInstallerAppTitle();
        final long requiredBytes = mDialogData.getRequiredBytes();
    private void updateUnarchiveErrorUI(Dialog dialog, UnarchiveError unarchiveStage) {
        mAppSnippet.setVisibility(View.GONE);

        TextView customMessage = dialogView.requireViewById(R.id.custom_message);
        customMessage.setVisibility(View.VISIBLE);
        final int status = unarchiveStage.getUnarchivalStatus();
        final String installerAppTitle = unarchiveStage.getInstallerAppTitle();
        final long requiredBytes = unarchiveStage.getRequiredBytes();

        String title = null;
        CharSequence customMessage = null;
        int positiveBtnTextResId = Resources.ID_NULL;
        int negativeBtnTextResId = R.string.button_close;
        DialogInterface.OnClickListener positiveButtonListener = null;
        switch (status) {
            case PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED -> {
                title = getString(R.string.title_restore_error_user_action_needed);
                positiveBtnTextResId = R.string.button_continue;
                positiveButtonListener = this;
                negativeBtnTextResId = R.string.button_cancel;
                customMessage.setText(
                customMessage =
                        getString(R.string.message_restore_error_user_action_needed,
                            installerAppTitle));
                                installerAppTitle);
            }

            case PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE -> {
                title = getString(R.string.title_restore_error_less_storage);
                positiveBtnTextResId = R.string.button_manage_apps;
                positiveButtonListener = this;
                negativeBtnTextResId = R.string.button_cancel;

                String message = String.format(
                        getString(R.string.message_restore_error_less_storage),
                        Formatter.formatShortFileSize(requireContext(), requiredBytes));
                customMessage.setText(Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY));
                customMessage = Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY);
            }

            case PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY -> {
                title = getString(R.string.title_restore_error_offline);
                customMessage.setText(getString(R.string.message_restore_error_offline));
                customMessage = getString(R.string.message_restore_error_offline);
            }

            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED -> {
                title = String.format(getString(R.string.title_restore_error_installer_disabled),
                        installerAppTitle);
                positiveBtnTextResId = R.string.button_settings;
                positiveButtonListener = this;
                negativeBtnTextResId = R.string.button_cancel;
                customMessage.setText(String.format(
                customMessage = String.format(
                        getString(R.string.message_restore_error_installer_disabled),
                            installerAppTitle));
                        installerAppTitle);
            }

            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED -> {
                title = String.format(getString(R.string.title_restore_error_installer_absent),
                        installerAppTitle);
                customMessage.setText(String.format(
                customMessage = String.format(
                        getString(R.string.message_restore_error_installer_absent),
                            installerAppTitle));
                        installerAppTitle);
            }

            case PackageInstaller.UNARCHIVAL_GENERIC_ERROR -> {
                title = getString(R.string.title_restore_error_generic);
                customMessage.setText(getString(R.string.message_restore_error_generic));
                customMessage = getString(R.string.message_restore_error_generic);
            }

            default ->
                // This should never happen through normal API usage.
                    throw new IllegalArgumentException("Invalid unarchive status " + status);
        }
        return UiUtil.getAlertDialog(requireContext(), title, dialogView, positiveBtnTextResId,
                negativeBtnTextResId, positiveButtonListener, this);

        // Set the title and the message
        dialog.setTitle(title);
        mCustomMessageTextView.setText(customMessage);

        // Set the positive button and the listener
        Button positiveButton = UiUtil.getAlertDialogPositiveButton(dialog);
        if (positiveButton != null) {
            if (positiveBtnTextResId == Resources.ID_NULL) {
                positiveButton.setVisibility(View.GONE);
            } else {
                positiveButton.setVisibility(View.VISIBLE);
                positiveButton.setText(positiveBtnTextResId);
                positiveButton.setOnClickListener(view -> {
                    mUnarchiveActionListener.handleUnarchiveErrorAction(
                            unarchiveStage.getUnarchivalStatus(),
                            unarchiveStage.getInstallerPackageName(),
                            unarchiveStage.getPendingIntent());
                });
            }
        }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (which != Dialog.BUTTON_POSITIVE) {
            return;
        // Set the negative button and the listener
        Button negativeButton = UiUtil.getAlertDialogNegativeButton(dialog);
        if (negativeButton != null) {
            negativeButton.setText(negativeBtnTextResId);
            negativeButton.setOnClickListener(view -> {
                requireActivity().finish();
            });
        }
        mUnarchiveActionListener.handleUnarchiveErrorAction(
                mDialogData.getUnarchivalStatus(), mDialogData.getInstallerPackageName(),
                mDialogData.getPendingIntent());
    }

    private void updateUserActionRequiredUI(Dialog dialog,
            UnarchiveUserActionRequired unarchiveStage) {
        mAppSnippet.setVisibility(View.VISIBLE);

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (isAdded()) {
            getActivity().finish();
        // Set app icon and label
        mAppIcon.setImageDrawable(unarchiveStage.getAppIcon());
        mAppLabelTextView.setText(unarchiveStage.getAppLabel());

        // Set title
        dialog.setTitle(R.string.title_restore);

        mCustomMessageTextView.setText(
                getString(R.string.message_restore, unarchiveStage.getInstallerTitle()));


        // Set the positive button and the listener
        Button positiveButton = UiUtil.getAlertDialogPositiveButton(dialog);
        if (positiveButton != null) {
            positiveButton.setVisibility(View.VISIBLE);
            positiveButton.setText(R.string.button_restore);
            positiveButton.setOnClickListener(view -> {
                mUnarchiveActionListener.beginUnarchive();
            });
        }

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