chromium/components/browser_ui/accessibility/android/java/src/org/chromium/components/browser_ui/accessibility/PageZoomPreference.java

// Copyright 2022 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.browser_ui.accessibility;

import static org.chromium.content_public.browser.HostZoomMap.AVAILABLE_ZOOM_FACTORS;

import android.content.Context;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;

import org.chromium.components.browser_ui.accessibility.AccessibilitySettingsDelegate.IntegerPreferenceDelegate;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.ui.widget.ChromeImageButton;

/** Custom preference for the page zoom section of Accessibility Settings. */
public class PageZoomPreference extends Preference implements SeekBar.OnSeekBarChangeListener {
    private int mInitialValue;
    private SeekBar mSeekBar;
    private ChromeImageButton mDecreaseButton;
    private ChromeImageButton mIncreaseButton;
    private TextView mCurrentValueText;

    private SeekBar mTextSizeContrastSeekBar;
    private ChromeImageButton mTextSizeContrastDecreaseButton;
    private ChromeImageButton mTextSizeContrastIncreaseButton;
    private TextView mTextSizeContrastCurrentLevelText;
    private IntegerPreferenceDelegate mTextSizeContrastDelegate;
    private static final int TEXT_SIZE_CONTRAST_BUTTON_INCREMENT = 10;

    // Values taken from dimens of text_size_* in //ui/android/java/res/values/dimens.xml
    private static final float DEFAULT_LARGE_TEXT_SIZE_SP = 16.0f;
    private static final float DEFAULT_MEDIUM_TEXT_SIZE_SP = 14.0f;
    private static final float DEFAULT_SMALL_TEXT_SIZE_SP = 12.0f;

    private float mDefaultPreviewImageSize;
    private ImageView mPreviewImage;
    private LinearLayout.LayoutParams mPreviewImageParams;

    private TextView mPreviewLargeText;
    private TextView mPreviewMediumText;
    private TextView mPreviewSmallText;

    private float mCurrentMultiplier;
    private int mTextSizeContrastFactor;

    public PageZoomPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        setLayoutResource(R.layout.page_zoom_preference);
    }

    @Override
    public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);

        // Re-use the main control layout, but remove extra padding and background.
        LinearLayout container = (LinearLayout) holder.findViewById(R.id.page_zoom_view_container);
        int top = container.getPaddingTop();
        int bot = container.getPaddingBottom();
        container.setBackground(null);
        container.setPadding(0, top, 0, bot);

        mPreviewLargeText = (TextView) holder.findViewById(R.id.page_zoom_preview_large_text);
        mPreviewMediumText = (TextView) holder.findViewById(R.id.page_zoom_preview_medium_text);
        mPreviewSmallText = (TextView) holder.findViewById(R.id.page_zoom_preview_small_text);

        var resources = getContext().getResources();
        mDefaultPreviewImageSize =
                resources.getDimensionPixelSize(R.dimen.page_zoom_preview_image_size);
        mPreviewImage = (ImageView) holder.findViewById(R.id.page_zoom_preview_image);
        mPreviewImageParams =
                new LinearLayout.LayoutParams(mPreviewImage.getWidth(), mPreviewImage.getHeight());

        // Set up Page Zoom slider.
        mCurrentValueText = (TextView) holder.findViewById(R.id.page_zoom_current_value_text);
        mCurrentValueText.setText(resources.getString(R.string.page_zoom_level, 100));

        mDecreaseButton =
                (ChromeImageButton) holder.findViewById(R.id.page_zoom_decrease_zoom_button);
        mDecreaseButton.setOnClickListener(v -> onHandleDecreaseClicked());

        mIncreaseButton =
                (ChromeImageButton) holder.findViewById(R.id.page_zoom_increase_zoom_button);
        mIncreaseButton.setOnClickListener(v -> onHandleIncreaseClicked());

        mSeekBar = (SeekBar) holder.findViewById(R.id.page_zoom_slider);
        mSeekBar.setOnSeekBarChangeListener(this);
        mSeekBar.setMax(PageZoomUtils.PAGE_ZOOM_MAXIMUM_SEEKBAR_VALUE);
        mSeekBar.setProgress(mInitialValue);
        mCurrentMultiplier = mInitialValue;
        updateViewsOnProgressChanged(mInitialValue, mSeekBar);

        // Set up text size contrast slider.
        if (ContentFeatureMap.isEnabled(ContentFeatureList.SMART_ZOOM)) {
            holder.findViewById(R.id.text_size_contrast_title).setVisibility(View.VISIBLE);
            holder.findViewById(R.id.text_size_contrast_summary).setVisibility(View.VISIBLE);
            holder.findViewById(R.id.text_size_contrast_layout_container)
                    .setVisibility(View.VISIBLE);

            mTextSizeContrastCurrentLevelText =
                    (TextView) holder.findViewById(R.id.text_size_contrast_current_value_text);
            mTextSizeContrastCurrentLevelText.setText(
                    resources.getString(R.string.text_size_contrast_level, 0));
            mTextSizeContrastCurrentLevelText.setVisibility(View.VISIBLE);

            mTextSizeContrastDecreaseButton =
                    (ChromeImageButton)
                            holder.findViewById(R.id.text_size_contrast_decrease_zoom_button);
            mTextSizeContrastDecreaseButton.setOnClickListener(
                    v -> onHandleContrastDecreaseClicked());
            mTextSizeContrastDecreaseButton.setVisibility(View.VISIBLE);

            mTextSizeContrastIncreaseButton =
                    (ChromeImageButton)
                            holder.findViewById(R.id.text_size_contrast_increase_zoom_button);
            mTextSizeContrastIncreaseButton.setOnClickListener(
                    v -> onHandleContrastIncreaseClicked());
            mTextSizeContrastIncreaseButton.setVisibility(View.VISIBLE);

            mTextSizeContrastSeekBar =
                    (SeekBar) holder.findViewById(R.id.text_size_contrast_slider);
            mTextSizeContrastSeekBar.setOnSeekBarChangeListener(this);
            mTextSizeContrastSeekBar.setMax(PageZoomUtils.TEXT_SIZE_CONTRAST_MAX_LEVEL);
            mTextSizeContrastFactor = mTextSizeContrastDelegate.getValue();
            mTextSizeContrastSeekBar.setProgress(mTextSizeContrastFactor);
            mTextSizeContrastSeekBar.setVisibility(View.VISIBLE);
            updateViewsOnProgressChanged(mTextSizeContrastFactor, mTextSizeContrastSeekBar);
        }
    }

    /**
     * Initial values to set the progress of seekbars.
     * @param zoomLevel int - existing user pref value for zoom level (or default).
     */
    public void setInitialValue(int zoomLevel) {
        mInitialValue = zoomLevel;
    }

    /**
     * Set a delegate for the text size contrast slider functionality.
     * @param delegate IntegerPreferenceDelegate - embedder's instance of a preference delegate.
     */
    public void setTextSizeContrastDelegate(IntegerPreferenceDelegate delegate) {
        mTextSizeContrastDelegate = delegate;
    }

    private void updateViewsOnProgressChanged(int progress, SeekBar seekBar) {
        updateZoomPercentageText(progress, seekBar);
        updatePreviewWidget(progress, seekBar);
        updateButtonStates(progress, seekBar);
    }

    private void updateZoomPercentageText(int progress, SeekBar seekBar) {
        if (seekBar.getId() == R.id.page_zoom_slider) {
            int zoomLevel =
                    (int) Math.round(100 * PageZoomUtils.convertSeekBarValueToZoomLevel(progress));
            mCurrentValueText.setText(
                    getContext().getResources().getString(R.string.page_zoom_level, zoomLevel));
        } else if (seekBar.getId() == R.id.text_size_contrast_slider) {
            mTextSizeContrastCurrentLevelText.setText(
                    getContext()
                            .getResources()
                            .getString(R.string.text_size_contrast_level, progress));
        }
    }

    private void updatePreviewWidget(int progress, SeekBar seekBar) {
        if (seekBar.getId() == R.id.page_zoom_slider) {
            mCurrentMultiplier = (float) PageZoomUtils.convertSeekBarValueToZoomLevel(progress);
        } else if (seekBar.getId() == R.id.text_size_contrast_slider) {
            mTextSizeContrastFactor = progress;
        }

        mPreviewLargeText.setTextSize(
                TypedValue.COMPLEX_UNIT_SP, calculateAdjustedTextSize(DEFAULT_LARGE_TEXT_SIZE_SP));
        mPreviewMediumText.setTextSize(
                TypedValue.COMPLEX_UNIT_SP, calculateAdjustedTextSize(DEFAULT_MEDIUM_TEXT_SIZE_SP));
        mPreviewSmallText.setTextSize(
                TypedValue.COMPLEX_UNIT_SP, calculateAdjustedTextSize(DEFAULT_SMALL_TEXT_SIZE_SP));

        // TODO(crbug.com/40919531): Edit preview images when smart image sizing is added.
        mPreviewImageParams.width = (int) (mDefaultPreviewImageSize * mCurrentMultiplier);
        mPreviewImageParams.height = (int) (mDefaultPreviewImageSize * mCurrentMultiplier);
        mPreviewImage.setLayoutParams(mPreviewImageParams);
    }

    private float calculateAdjustedTextSize(float startingTextSize) {
        // TODO(crbug.com/40919531): Add the following non-linear formula
        if (mTextSizeContrastFactor > 0
                && ContentFeatureMap.isEnabled(ContentFeatureList.SMART_ZOOM)) {}
        return startingTextSize * mCurrentMultiplier;
    }

    private void updateButtonStates(int progress, SeekBar seekBar) {
        if (seekBar.getId() == R.id.page_zoom_slider) {
            double newZoomFactor = PageZoomUtils.convertSeekBarValueToZoomFactor(progress);

            // If the new zoom factor is greater than the minimum zoom factor, enable decrease
            // button.
            mDecreaseButton.setEnabled(newZoomFactor > AVAILABLE_ZOOM_FACTORS[0]);

            // If the new zoom factor is less than the maximum zoom factor, enable increase button.
            mIncreaseButton.setEnabled(
                    newZoomFactor < AVAILABLE_ZOOM_FACTORS[AVAILABLE_ZOOM_FACTORS.length - 1]);
        } else if (seekBar.getId() == R.id.text_size_contrast_slider) {
            mTextSizeContrastDecreaseButton.setEnabled(progress > 0);
            mTextSizeContrastIncreaseButton.setEnabled(
                    progress < PageZoomUtils.TEXT_SIZE_CONTRAST_MAX_LEVEL);
        }
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        // Update the zoom percentage text, preview widget and enabled state of increase/decrease
        // buttons as the slider is updated.
        updateViewsOnProgressChanged(progress, seekBar);
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {}

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        if (seekBar.getId() == R.id.page_zoom_slider) {
            // When a user stops changing the slider value, record the new value in prefs.
            callChangeListener(seekBar.getProgress());
        } else if (seekBar.getId() == R.id.text_size_contrast_slider) {
            saveTextSizeContrastValueToPreferences();
        }
    }

    private void onHandleDecreaseClicked() {
        // When decreasing zoom, "snap" to the greatest preset value that is less than the current.
        double currentZoomFactor =
                PageZoomUtils.convertSeekBarValueToZoomFactor(mSeekBar.getProgress());
        int index = PageZoomUtils.getNextIndex(true, currentZoomFactor);

        if (index >= 0) {
            int seekBarValue =
                    PageZoomUtils.convertZoomFactorToSeekBarValue(AVAILABLE_ZOOM_FACTORS[index]);
            mSeekBar.setProgress(seekBarValue);
            callChangeListener(seekBarValue);
        }
    }

    private void onHandleIncreaseClicked() {
        // When increasing zoom, "snap" to the smallest preset value that is more than the current.
        double currentZoomFactor =
                PageZoomUtils.convertSeekBarValueToZoomFactor(mSeekBar.getProgress());
        int index = PageZoomUtils.getNextIndex(false, currentZoomFactor);

        if (index <= AVAILABLE_ZOOM_FACTORS.length - 1) {
            int seekBarValue =
                    PageZoomUtils.convertZoomFactorToSeekBarValue(AVAILABLE_ZOOM_FACTORS[index]);
            mSeekBar.setProgress(seekBarValue);
            callChangeListener(seekBarValue);
        }
    }

    private void onHandleContrastDecreaseClicked() {
        // Decrease the contrast slider by defined increment.
        mTextSizeContrastSeekBar.setProgress(
                mTextSizeContrastSeekBar.getProgress() - TEXT_SIZE_CONTRAST_BUTTON_INCREMENT);
        saveTextSizeContrastValueToPreferences();
    }

    private void onHandleContrastIncreaseClicked() {
        // Increase the contrast slider by defined increment.
        mTextSizeContrastSeekBar.setProgress(
                mTextSizeContrastSeekBar.getProgress() + TEXT_SIZE_CONTRAST_BUTTON_INCREMENT);
        saveTextSizeContrastValueToPreferences();
    }

    private void saveTextSizeContrastValueToPreferences() {
        mTextSizeContrastDelegate.setValue(mTextSizeContrastSeekBar.getProgress());
    }

    // Testing methods.

    public SeekBar getZoomSliderForTesting() {
        return mSeekBar;
    }

    public void setZoomValueForTesting(int progress) {
        mSeekBar.setProgress(progress);
        updateViewsOnProgressChanged(progress, mSeekBar);
    }

    public SeekBar getTextSizeContrastSliderForTesting() {
        return mTextSizeContrastSeekBar;
    }

    public void setTextContrastValueForTesting(int contrast) {
        mTextSizeContrastSeekBar.setProgress(contrast);
        updateViewsOnProgressChanged(contrast, mTextSizeContrastSeekBar);
    }
}