chromium/chrome/android/java/src/org/chromium/chrome/browser/ntp/IncognitoDescriptionView.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.chrome.browser.ntp;

import static org.chromium.ui.base.ViewUtils.dpToPx;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.text.SpannableString;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.SwitchCompat;

import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.document.ChromeAsyncTabLauncher;
import org.chromium.components.content_settings.CookieControlsEnforcement;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
import org.chromium.ui.widget.ChromeBulletSpan;

/** The view to describle incognito mode. */
public class IncognitoDescriptionView extends LinearLayout {
    private int mWidthDp;
    private int mHeightDp;

    private LinearLayout mContainer;
    private TextView mHeader;
    private TextView mSubtitle;
    private LinearLayout mBulletpointsContainer;
    private TextView mLearnMore;
    private TextView[] mParagraphs;
    private ViewGroup mCookieControlsCard;
    private SwitchCompat mCookieControlsToggle;
    private ImageView mCookieControlsManagedIcon;
    private TextView mCookieControlsTitle;
    private TextView mCookieControlsSubtitle;

    private static final int BULLETPOINTS_HORIZONTAL_SPACING_DP = 40;
    private static final int BULLETPOINTS_MARGIN_BOTTOM_DP = 12;
    private static final int CONTENT_WIDTH_DP = 600;
    private static final int WIDE_LAYOUT_THRESHOLD_DP = 720;
    private static final int COOKIES_CONTROL_MARGIN_TOP_DP = 12;

    static final String TRACKING_PROTECTION_URL =
            "https://support.google.com/chrome/?p=pause_protections";

    /** Default constructor needed to inflate via XML. */
    public IncognitoDescriptionView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setLearnMoreOnclickListener(OnClickListener listener) {
        mLearnMore.setOnClickListener(listener);
    }

    public void setCookieControlsToggleOnCheckedChangeListener(OnCheckedChangeListener listener) {
        if (!findCookieControlElements()) return;
        mCookieControlsToggle.setOnCheckedChangeListener(listener);
    }

    public void setCookieControlsToggle(boolean enabled) {
        if (!findCookieControlElements()) return;
        mCookieControlsToggle.setChecked(enabled);
    }

    public void setCookieControlsIconOnclickListener(OnClickListener listener) {
        if (!findCookieControlElements()) return;
        mCookieControlsManagedIcon.setOnClickListener(listener);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mWidthDp = getContext().getResources().getConfiguration().screenWidthDp;
        mHeightDp = getContext().getResources().getConfiguration().screenHeightDp;

        populateBulletpoints(R.id.new_tab_incognito_features, R.string.new_tab_otr_not_saved);
        populateBulletpoints(R.id.new_tab_incognito_warning, R.string.new_tab_otr_visible);

        mContainer = findViewById(R.id.new_tab_incognito_container);
        mHeader = findViewById(R.id.new_tab_incognito_title);
        mSubtitle = findViewById(R.id.new_tab_incognito_subtitle);
        mLearnMore = findViewById(R.id.learn_more);
        mParagraphs =
                new TextView[] {
                    mSubtitle,
                    findViewById(R.id.new_tab_incognito_features),
                    findViewById(R.id.new_tab_incognito_warning)
                };
        mBulletpointsContainer = findViewById(R.id.new_tab_incognito_bulletpoints_container);

        adjustView();
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // View#onConfigurationChanged() doesn't get called when resizing this view in
        // multi-window mode, so #onMeasure() is used instead.
        Configuration config = getContext().getResources().getConfiguration();
        if (mWidthDp != config.screenWidthDp || mHeightDp != config.screenHeightDp) {
            mWidthDp = config.screenWidthDp;
            mHeightDp = config.screenHeightDp;
            adjustView();
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public void formatTrackingProtectionText(Context context, View layout) {
        TextView view = layout.findViewById(R.id.tracking_protection_description_two);
        if (view == null) {
            adjustCookieControlsCard();
            return;
        }

        String text =
                context.getResources()
                        .getString(R.string.new_tab_otr_third_party_blocked_cookie_part_two);
        ClickableSpan span =
                new ClickableSpan() {
                    @Override
                    public void onClick(View view) {
                        new ChromeAsyncTabLauncher(/* incognito= */ true)
                                .launchUrl(TRACKING_PROTECTION_URL, TabLaunchType.FROM_CHROME_UI);
                    }

                    @Override
                    public void updateDrawState(TextPaint textPaint) {
                        super.updateDrawState(textPaint);
                        textPaint.setColor(
                                context.getColor(R.color.default_text_color_secondary_light_list));
                    }
                };
        view.setText(
                SpanApplier.applySpans(text, new SpanApplier.SpanInfo("<link>", "</link>", span)));
        view.setMovementMethod(LinkMovementMethod.getInstance());
        adjustCookieControlsCard();
    }

    private void adjustView() {
        adjustIcon();
        adjustLayout();
        adjustLearnMore();
        adjustCookieControlsCard();
    }

    /**
     * @param element Resource ID of the element to be populated with the bulletpoints.
     * @param content String ID to serve as the text of |element|. Must contain an <em></em> span,
     *     which will be emphasized, and three
     *     <li>items, which will be converted to bulletpoints. Populates |element| with |content|.
     */
    private void populateBulletpoints(@IdRes int element, @StringRes int content) {
        TextView view = findViewById(element);
        SpannableString spannedText = getSpannedBulletText(getContext(), content);
        view.setText(spannedText);
    }

    @NonNull
    static SpannableString getSpannedBulletText(Context context, @StringRes int content) {
        String text = context.getResources().getString(content);
        // Some translations don't have a line break between list entries.
        text = text.replaceAll("([^\n ]) *(<li>|</?ul>)", "$1\n$2");

        // TODO(msramek): Unfortunately, our strings are missing the closing "</li>" tag, which
        // is not a problem when they're used in the Desktop WebUI (omitting the tag is valid in
        // HTML5), but it is a problem for SpanApplier. Update the strings and remove this regex.
        // Note that modifying the strings is a non-trivial operation as they went through a special
        // translation process.
        text = text.replaceAll("<li>([^<]+)\n", "<li>$1</li>\n");

        // Format the bulletpoints:
        //   - Disambiguate the <li></li> spans for SpanApplier.
        //   - Remove leading whitespace (caused by formatting in the .grdp file)
        //   - Remove the trailing newline after the last bulletpoint.
        text = text.replaceFirst(" *<li>([^<]*)</li>", "<li1>$1</li1>");
        text = text.replaceFirst(" *<li>([^<]*)</li>", "<li2>$1</li2>");
        text = text.replaceFirst(" *<li>([^<]*)</li>\n", "<li3>$1</li3>");

        String error =
                "Format error. Locale: "
                        + context.getResources().getConfiguration().getLocales()
                        + " \nstring: "
                        + context.getResources().getString(content);
        assert text.contains("<li1>") : error;
        assert text.contains("<li2>") : error;
        assert text.contains("<li3>") : error;

        // Remove the <ul></ul> tags which serve no purpose here, including the whitespace around
        // them.
        text = text.replaceAll(" *</?ul>\\n?", "");

        SpannableString spannedText =
                SpanApplier.applySpans(
                        text,
                        new SpanInfo(
                                "<em>",
                                "</em>",
                                new ForegroundColorSpan(
                                        context.getColor(R.color.incognito_emphasis))),
                        new SpanInfo("<li1>", "</li1>", new ChromeBulletSpan(context)),
                        new SpanInfo("<li2>", "</li2>", new ChromeBulletSpan(context)),
                        new SpanInfo("<li3>", "</li3>", new ChromeBulletSpan(context)));
        return spannedText;
    }

    /** Adjusts the paddings, margins, and the orientation of bulletpoints. */
    private void adjustLayout() {
        int paddingHorizontalDp;
        int paddingVerticalDp;

        boolean bulletpointsArrangedHorizontally;

        if (mWidthDp <= WIDE_LAYOUT_THRESHOLD_DP) {
            // Small padding.
            paddingHorizontalDp = mWidthDp <= 240 ? 24 : 32;
            paddingVerticalDp = 32;

            // Align left.
            mContainer.setGravity(Gravity.START);

            // Decide the bulletpoints orientation.
            bulletpointsArrangedHorizontally = false;

            // The subtitle is sized automatically, but not wider than CONTENT_WIDTH_DP.
            mSubtitle.setLayoutParams(
                    new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.WRAP_CONTENT,
                            LinearLayout.LayoutParams.WRAP_CONTENT));
            mSubtitle.setMaxWidth(dpToPx(getContext(), CONTENT_WIDTH_DP));

            // The bulletpoints container takes the same width as subtitle. Since the width can
            // not be directly measured at this stage, we must calculate it manually.
            mBulletpointsContainer.getLayoutParams().width =
                    dpToPx(
                            getContext(),
                            Math.min(CONTENT_WIDTH_DP, mWidthDp - 2 * paddingHorizontalDp));
        } else {
            // Large padding.
            paddingHorizontalDp = 0; // Should not be necessary on a screen this large.
            paddingVerticalDp = mHeightDp <= 320 ? 16 : 72;

            // Align to the center.
            mContainer.setGravity(Gravity.CENTER_HORIZONTAL);

            // Decide the bulletpoints orientation.
            bulletpointsArrangedHorizontally = true;

            int contentWidthPx = dpToPx(getContext(), CONTENT_WIDTH_DP);
            mSubtitle.setLayoutParams(
                    new LinearLayout.LayoutParams(
                            contentWidthPx, LinearLayout.LayoutParams.WRAP_CONTENT));
            mBulletpointsContainer.getLayoutParams().width = contentWidthPx;
        }

        // Apply the bulletpoints orientation.
        if (bulletpointsArrangedHorizontally) {
            mBulletpointsContainer.setOrientation(LinearLayout.HORIZONTAL);
        } else {
            mBulletpointsContainer.setOrientation(LinearLayout.VERTICAL);
        }

        // Set up paddings and margins.
        mContainer.setPadding(
                dpToPx(getContext(), paddingHorizontalDp),
                dpToPx(getContext(), paddingVerticalDp),
                dpToPx(getContext(), paddingHorizontalDp),
                dpToPx(getContext(), paddingVerticalDp));

        // Total space between adjacent paragraphs (Including margins, paddings, etc.)
        int totalSpaceBetweenViews =
                getContext()
                        .getResources()
                        .getDimensionPixelSize(R.dimen.incognito_ntp_total_space_between_views);

        for (TextView paragraph : mParagraphs) {
            // If bulletpoints are arranged horizontally, there should be space between them.
            int rightMarginPx =
                    (bulletpointsArrangedHorizontally
                                    && paragraph == mBulletpointsContainer.getChildAt(0))
                            ? dpToPx(getContext(), BULLETPOINTS_HORIZONTAL_SPACING_DP)
                            : 0;

            ((LinearLayout.LayoutParams) paragraph.getLayoutParams())
                    .setMargins(0, totalSpaceBetweenViews, rightMarginPx, 0);
            paragraph.setLayoutParams(paragraph.getLayoutParams()); // Apply the new layout.
        }

        // The learn more text view has height of min_touch_target_size. Typically the actual text
        // is not that tall, and already has some space. We want to have a
        // totalSpaceBetweenViews tall gap between the learn more text and the adjacent
        // elements. So add the difference as an additional margin.
        int innerSpacing =
                (int)
                        ((getContext()
                                                .getResources()
                                                .getDimensionPixelSize(
                                                        R.dimen.min_touch_target_size)
                                        - mLearnMore.getTextSize())
                                / 2);
        int learnMoreSpacingTop =
                totalSpaceBetweenViews
                        - innerSpacing
                        - dpToPx(getContext(), BULLETPOINTS_MARGIN_BOTTOM_DP);
        int learnMoreSpacingBottom =
                dpToPx(getContext(), COOKIES_CONTROL_MARGIN_TOP_DP) - innerSpacing;

        LinearLayout.LayoutParams params = (LayoutParams) mLearnMore.getLayoutParams();
        params.setMargins(0, learnMoreSpacingTop, 0, learnMoreSpacingBottom);
        ViewUtils.requestLayout(mLearnMore, "IncognitoDescriptionView.adjustLayout");

        ((LinearLayout.LayoutParams) mHeader.getLayoutParams())
                .setMargins(0, totalSpaceBetweenViews, 0, 0);
        mHeader.setLayoutParams(mHeader.getLayoutParams()); // Apply the new layout.
    }

    /** Adjust the Incognito icon. */
    private void adjustIcon() {
        // The icon resource is 120dp x 120dp (i.e. 120px x 120px at MDPI). This method always
        // resizes the icon view to 120dp x 120dp or smaller, therefore image quality is not lost.

        int sizeDp;
        if (mWidthDp <= WIDE_LAYOUT_THRESHOLD_DP) {
            sizeDp = (mWidthDp <= 240 || mHeightDp <= 480) ? 48 : 72;
        } else {
            sizeDp = mHeightDp <= 480 ? 72 : 120;
        }

        ImageView icon = findViewById(R.id.new_tab_incognito_icon);
        icon.getLayoutParams().width = dpToPx(getContext(), sizeDp);
        icon.getLayoutParams().height = dpToPx(getContext(), sizeDp);
    }

    /** Adjust the "Learn More" link. */
    private void adjustLearnMore() {
        final String subtitleText =
                getContext()
                        .getResources()
                        .getString(R.string.new_tab_otr_subtitle_with_reading_list);
        boolean learnMoreInSubtitle = mWidthDp > WIDE_LAYOUT_THRESHOLD_DP;

        mLearnMore.setVisibility(learnMoreInSubtitle ? View.GONE : View.VISIBLE);

        if (!learnMoreInSubtitle) {
            // Revert to the original text.
            mSubtitle.setText(subtitleText);
            mSubtitle.setMovementMethod(null);
            return;
        }

        // Concatenate the original text with a clickable "Learn more" link.
        StringBuilder concatenatedText = new StringBuilder();
        concatenatedText.append(subtitleText);
        concatenatedText.append(" ");
        concatenatedText.append(getContext().getResources().getString(R.string.learn_more));
        SpannableString textWithLearnMoreLink = new SpannableString(concatenatedText.toString());

        NoUnderlineClickableSpan span =
                new NoUnderlineClickableSpan(
                        getContext(),
                        R.color.baseline_primary_80,
                        (view) -> mLearnMore.callOnClick());
        textWithLearnMoreLink.setSpan(
                span, subtitleText.length() + 1, textWithLearnMoreLink.length(), /* flags= */ 0);
        mSubtitle.setText(textWithLearnMoreLink);
        mSubtitle.setMovementMethod(LinkMovementMethod.getInstance());
    }

    /** Adjust the Cookie Controls Card. */
    private void adjustCookieControlsCard() {
        mCookieControlsCard = findViewById(R.id.cookie_controls_card);
        if (mCookieControlsCard == null) {
            mCookieControlsCard = findViewById(R.id.tracking_protection_card);
        }
        // Still null - not inflated yet.
        if (mCookieControlsCard == null) return;
        if (mWidthDp <= WIDE_LAYOUT_THRESHOLD_DP) {
            // Portrait
            mCookieControlsCard.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT;
        } else {
            // Landscape
            mCookieControlsCard.getLayoutParams().width = dpToPx(getContext(), CONTENT_WIDTH_DP);
        }
    }

    public void setCookieControlsEnforcement(@CookieControlsEnforcement int enforcement) {
        // No cookie controls toggle on the page.
        if (!findCookieControlElements()) return;

        boolean enforced = enforcement != CookieControlsEnforcement.NO_ENFORCEMENT;
        mCookieControlsToggle.setEnabled(!enforced);
        mCookieControlsManagedIcon.setVisibility(enforced ? View.VISIBLE : View.GONE);
        mCookieControlsTitle.setEnabled(!enforced);
        mCookieControlsSubtitle.setEnabled(!enforced);

        Resources resources = getContext().getResources();
        StringBuilder subtitleText = new StringBuilder();
        subtitleText.append(resources.getString(R.string.new_tab_otr_third_party_cookie_sublabel));
        if (!enforced) {
            mCookieControlsSubtitle.setText(subtitleText.toString());
            return;
        }

        int iconRes;
        String addition;
        switch (enforcement) {
            case CookieControlsEnforcement.ENFORCED_BY_POLICY:
                iconRes = R.drawable.ic_business_small;
                addition = resources.getString(R.string.managed_by_your_organization);
                break;
            case CookieControlsEnforcement.ENFORCED_BY_COOKIE_SETTING:
                iconRes = R.drawable.settings_cog;
                addition =
                        resources.getString(
                                R.string.new_tab_otr_cookie_controls_controlled_tooltip_text);
                break;
            default:
                return;
        }
        mCookieControlsManagedIcon.setImageResource(iconRes);
        subtitleText.append("\n");
        subtitleText.append(addition);
        mCookieControlsSubtitle.setText(subtitleText.toString());
    }

    /** Finds the 3PC controls and returns true if they exist. */
    private boolean findCookieControlElements() {
        mCookieControlsToggle = findViewById(R.id.cookie_controls_card_toggle);
        if (mCookieControlsToggle == null) return false;
        mCookieControlsManagedIcon = findViewById(R.id.cookie_controls_card_managed_icon);
        mCookieControlsTitle = findViewById(R.id.cookie_controls_card_title);
        mCookieControlsSubtitle = findViewById(R.id.cookie_controls_card_subtitle);
        return true;
    }
}