chromium/components/webapps/browser/android/java/src/org/chromium/components/webapps/AddToHomescreenDialogView.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webapps;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RatingBar;
import android.widget.TextView;

import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * Displays the "Add to Homescreen" dialog, which contains a (possibly editable) title, icon, and
 * possibly an origin.
 *
 * <p>When the constructor is called, the dialog is shown immediately. A spinner is displayed if any
 * data is not yet fetched, and accepting the dialog is disabled until all data is available and in
 * its place on the screen.
 */
public class AddToHomescreenDialogView
        implements View.OnClickListener, ModalDialogProperties.Controller {
    private PropertyModel mDialogModel;
    private ModalDialogManager mModalDialogManager;
    @VisibleForTesting protected AddToHomescreenViewDelegate mDelegate;

    private View mParentView;

    /**
     * {@link #mShortcutTitleInput} and the {@link #mAppLayout} are mutually exclusive, depending on
     * whether the home screen item is a bookmark shortcut or a web/native app.
     */
    private EditText mShortcutTitleInput;

    private LinearLayout mAppLayout;
    private TextView mAppNameView;
    private EditText mHomebrewAppNameInput;
    private TextView mAppOriginView;
    private RatingBar mAppRatingBar;
    private ImageView mPlayLogoView;

    private View mProgressBarView;
    private ImageView mIconView;

    private @AppType int mAppType;
    private boolean mCanSubmit;

    private final String mInstallTitleText;
    private final String mInstallButtonText;
    private final String mAddTitleText;
    private final String mAddButtonText;

    @VisibleForTesting
    public AddToHomescreenDialogView(
            Context context,
            ModalDialogManager modalDialogManager,
            AddToHomescreenViewDelegate delegate) {
        assert delegate != null;

        mModalDialogManager = modalDialogManager;
        mDelegate = delegate;
        mParentView = LayoutInflater.from(context).inflate(R.layout.add_to_homescreen_dialog, null);

        mProgressBarView = mParentView.findViewById(R.id.spinny);
        mIconView = (ImageView) mParentView.findViewById(R.id.icon);

        mShortcutTitleInput = mParentView.findViewById(R.id.shortcut_name);
        mShortcutTitleInput.setOnEditorActionListener(
                (TextView v, int actionId, KeyEvent event) -> {
                    if (actionId == EditorInfo.IME_ACTION_DONE) {
                        if (!mDialogModel.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)) {
                            onClick(mDialogModel, ModalDialogProperties.ButtonType.POSITIVE);
                        }
                        return true;
                    }
                    return false;
                });

        mAppLayout = (LinearLayout) mParentView.findViewById(R.id.app_info);
        mAppNameView = (TextView) mAppLayout.findViewById(R.id.app_name);
        mHomebrewAppNameInput = mAppLayout.findViewById(R.id.homebrew_name);
        mAppOriginView = (TextView) mAppLayout.findViewById(R.id.origin);
        mAppRatingBar = (RatingBar) mAppLayout.findViewById(R.id.control_rating);
        mPlayLogoView = (ImageView) mParentView.findViewById(R.id.play_logo);

        mAppNameView.setOnClickListener(this);
        mIconView.setOnClickListener(this);

        mParentView.addOnLayoutChangeListener(
                new View.OnLayoutChangeListener() {
                    @Override
                    public void onLayoutChange(
                            View v,
                            int left,
                            int top,
                            int right,
                            int bottom,
                            int oldLeft,
                            int oldTop,
                            int oldRight,
                            int oldBottom) {
                        if (mProgressBarView.getMeasuredHeight()
                                        == mShortcutTitleInput.getMeasuredHeight()
                                && mShortcutTitleInput.getBackground() != null) {
                            // Force the text field to align better with the icon by accounting for
                            // the padding introduced by the background drawable.
                            mShortcutTitleInput.getLayoutParams().height =
                                    mProgressBarView.getMeasuredHeight()
                                            + mShortcutTitleInput.getPaddingBottom();
                            String caller =
                                    "AddToHomescreenDialogView.<init>."
                                            + "OnLayoutChangeListener.onLayoutChange";
                            ViewUtils.requestLayout(v, caller);
                            v.removeOnLayoutChangeListener(this);
                        }
                    }
                });

        // The "Add" button should be disabled if the dialog's text field is empty.
        mShortcutTitleInput.addTextChangedListener(
                new TextWatcher() {
                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {}

                    @Override
                    public void beforeTextChanged(
                            CharSequence s, int start, int count, int after) {}

                    @Override
                    public void afterTextChanged(Editable editableText) {
                        updateInstallButton();
                    }
                });

        mHomebrewAppNameInput.addTextChangedListener(
                new TextWatcher() {
                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {}

                    @Override
                    public void beforeTextChanged(
                            CharSequence s, int start, int count, int after) {}

                    @Override
                    public void afterTextChanged(Editable editableText) {
                        updateInstallButton();
                    }
                });

        Resources resources = context.getResources();
        mInstallTitleText = resources.getString(R.string.menu_install_webapp);
        mInstallButtonText = resources.getString(R.string.app_banner_install);
        mAddTitleText = resources.getString(R.string.pwa_uni_install_option_shortcut);
        mAddButtonText = resources.getString(R.string.add);

        mDialogModel =
                new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                        .with(ModalDialogProperties.CONTROLLER, this)
                        .with(ModalDialogProperties.TITLE, mAddTitleText)
                        .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, mAddButtonText)
                        .with(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, true)
                        .with(
                                ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
                                resources,
                                R.string.cancel)
                        .with(ModalDialogProperties.CUSTOM_VIEW, mParentView)
                        .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                        .with(
                                ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS,
                                UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS)
                        .build();
        mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.TAB);
    }

    // @VisibleForTests implies that a method should only be accessed from tests or within private
    // scope, but this is needed by AddToHomescreenViewBinder.
    protected void setTitle(String title) {
        mAppNameView.setText(title);
        mShortcutTitleInput.setText(title);
        mShortcutTitleInput.setSelection(mShortcutTitleInput.getText().length());
        mHomebrewAppNameInput.setText(title);
        mHomebrewAppNameInput.setSelection(mHomebrewAppNameInput.getText().length());
        mIconView.setContentDescription(title);
    }

    void setUrl(String url) {
        mAppOriginView.setText(url);
    }

    void setIcon(Bitmap icon, boolean isAdaptive) {
        if (isAdaptive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            setAdaptiveIcon(icon);
        } else {
            assert !isAdaptive : "Adaptive icons should not be provided pre-Android O.";
            mIconView.setImageBitmap(icon);
        }
        mProgressBarView.setVisibility(View.GONE);
        mIconView.setVisibility(View.VISIBLE);
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void setAdaptiveIcon(Bitmap icon) {
        mIconView.setImageIcon(Icon.createWithAdaptiveBitmap(icon));
    }

    void setType(@AppType int type) {
        mAppType = type;
        resetAllVisibility();
        switch (mAppType) {
            case AppType.NATIVE:
                mAppLayout.setVisibility(View.VISIBLE);
                mAppNameView.setVisibility(View.VISIBLE);
                mAppRatingBar.setVisibility(View.VISIBLE);
                mPlayLogoView.setVisibility(View.VISIBLE);
                break;
            case AppType.SHORTCUT:
                mShortcutTitleInput.setVisibility(View.VISIBLE);
                break;
            case AppType.WEBAPK:
                mAppLayout.setVisibility(View.VISIBLE);
                mAppNameView.setVisibility(View.VISIBLE);
                mAppOriginView.setVisibility(View.VISIBLE);
                break;
            case AppType.WEBAPK_DIY:
                mAppLayout.setVisibility(View.VISIBLE);
                mHomebrewAppNameInput.setVisibility(View.VISIBLE);
                mAppOriginView.setVisibility(View.VISIBLE);
                mAppOriginView.setVisibility(View.VISIBLE);
                break;
        }

        if (mAppType == AppType.SHORTCUT) {
            mDialogModel.set(ModalDialogProperties.TITLE, mAddTitleText);
            mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT, mAddButtonText);
        } else {
            mDialogModel.set(ModalDialogProperties.TITLE, mInstallTitleText);
            mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT, mInstallButtonText);
        }
    }

    void setNativeInstallButtonText(String installButtonText) {
        mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT, installButtonText);
        mDialogModel.set(
                ModalDialogProperties.POSITIVE_BUTTON_CONTENT_DESCRIPTION,
                ContextUtils.getApplicationContext()
                        .getString(
                                R.string.app_banner_view_native_app_install_accessibility,
                                installButtonText));
    }

    void setNativeAppRating(float rating) {
        mAppRatingBar.setRating(rating);
        mPlayLogoView.setImageResource(R.drawable.google_play);
    }

    // @VisibleForTests implies that a method should only be accessed from tests or within private
    // scope, but this is needed by AddToHomescreenViewBinder.
    protected void setCanSubmit(boolean canSubmit) {
        mCanSubmit = canSubmit;
        updateInstallButton();
    }

    // Update button state for shortcut or diy app.
    private void updateInstallButton() {
        TextView appNameView = getAppNameView();
        boolean missingTitle =
                appNameView.getVisibility() == View.VISIBLE
                        && TextUtils.isEmpty(appNameView.getText());
        mDialogModel.set(
                ModalDialogProperties.POSITIVE_BUTTON_DISABLED, !mCanSubmit || missingTitle);
    }

    private void resetAllVisibility() {
        mShortcutTitleInput.setVisibility(View.GONE);
        mAppLayout.setVisibility(View.GONE);
        mHomebrewAppNameInput.setVisibility(View.GONE);
        mAppNameView.setVisibility(View.GONE);
        mAppOriginView.setVisibility(View.GONE);
        mAppRatingBar.setVisibility(View.GONE);
        mPlayLogoView.setVisibility(View.GONE);
    }

    /**
     * From {@link View.OnClickListener}. Called when the views that have this class registered as
     * their {@link View.OnClickListener} are clicked.
     *
     * @param v The view that was clicked.
     */
    @Override
    public void onClick(View v) {
        if ((v == mAppNameView || v == mIconView)) {
            if (mDelegate.onAppDetailsRequested()) {
                mModalDialogManager.dismissDialog(
                        mDialogModel, DialogDismissalCause.ACTION_ON_CONTENT);
            }
        }
    }

    /**
     * From {@link ModalDialogProperties.Controller}. Called when a dialog button is clicked.
     *
     * @param model The dialog model that is associated with this click event.
     * @param buttonType The type of the button.
     */
    @Override
    public void onClick(PropertyModel model, int buttonType) {
        int dismissalCause = DialogDismissalCause.NEGATIVE_BUTTON_CLICKED;
        if (buttonType == ModalDialogProperties.ButtonType.POSITIVE) {
            String title = getAppNameView().getText().toString();
            mDelegate.onAddToHomescreen(title, mAppType);
            dismissalCause = DialogDismissalCause.POSITIVE_BUTTON_CLICKED;
        }
        mModalDialogManager.dismissDialog(mDialogModel, dismissalCause);
    }

    @Override
    public void onDismiss(PropertyModel model, int dismissalCause) {
        if (dismissalCause == DialogDismissalCause.POSITIVE_BUTTON_CLICKED) return;

        mDelegate.onViewDismissed();
    }

    @VisibleForTesting
    TextView getAppNameView() {
        switch (mAppType) {
            case AppType.SHORTCUT:
                return mShortcutTitleInput;
            case AppType.WEBAPK_DIY:
                return mHomebrewAppNameInput;
            case AppType.WEBAPK:
            case AppType.NATIVE:
                return mAppNameView;
            default:
                assert false;
                return null;
        }
    }

    View getParentViewForTest() {
        return mParentView;
    }
}