chromium/components/browser_ui/accessibility/android/java/src/org/chromium/components/browser_ui/accessibility/PageZoomMediator.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.components.browser_ui.accessibility.PageZoomUtils.PAGE_ZOOM_MAXIMUM_SEEKBAR_VALUE;
import static org.chromium.components.browser_ui.accessibility.PageZoomUtils.convertZoomFactorToSeekBarValue;
import static org.chromium.content_public.browser.HostZoomMap.AVAILABLE_ZOOM_FACTORS;
import static org.chromium.content_public.browser.HostZoomMap.setSystemFontScale;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.content_public.browser.HostZoomMap;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.modelutil.PropertyModel;

/**
 * Internal Mediator for the page zoom feature. Created by the |PageZoomCoordinator|, and should
 * not be accessed outside the component.
 */
public class PageZoomMediator {
    private final PropertyModel mModel;
    private WebContents mWebContents;
    private double mLatestZoomValue;
    private double mDefaultZoomFactor;

    public PageZoomMediator(PropertyModel model) {
        mModel = model;

        mModel.set(PageZoomProperties.DECREASE_ZOOM_CALLBACK, this::handleDecreaseClicked);
        mModel.set(PageZoomProperties.INCREASE_ZOOM_CALLBACK, this::handleIncreaseClicked);
        mModel.set(PageZoomProperties.RESET_ZOOM_CALLBACK, this::handleResetClicked);
        mModel.set(PageZoomProperties.SEEKBAR_CHANGE_CALLBACK, this::handleSeekBarValueChanged);
        mModel.set(PageZoomProperties.MAXIMUM_SEEK_VALUE, PAGE_ZOOM_MAXIMUM_SEEKBAR_VALUE);

        // Update the stored system font scale based on OS-level configuration. |this| will be
        // re-constructed after configuration changes, so this will be up-to-date for this session.
        setSystemFontScale(
                ContextUtils.getApplicationContext().getResources().getConfiguration().fontScale);
    }

    /**
     * Set the web contents that should be controlled by this instance.
     * @param webContents   The WebContents this instance should control.
     */
    protected void setWebContents(WebContents webContents) {
        mWebContents = webContents;
        initialize();
    }

    /**
     * Returns the latest updated user selected zoom value during this session.
     * @return double representing latest updated zoom value. Returns placeholder 0.0 if user did
     *         not select a zoom value during this session.
     */
    protected double latestZoomValue() {
        return mLatestZoomValue;
    }

    /** Logs UKM for the user changing the zoom level on the page from the slider. */
    protected void logZoomLevelUKM(double value) {
        PageZoomMetrics.logZoomLevelUKM(mWebContents, value);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void handleDecreaseClicked(Void unused) {
        // When decreasing zoom, "snap" to the greatest preset value that is less than the current.
        double currentZoomFactor = getZoomLevel(mWebContents);
        int index = PageZoomUtils.getNextIndex(true, currentZoomFactor);

        if (index >= 0) {
            handleIndexChanged(index);
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void handleIncreaseClicked(Void unused) {
        // When increasing zoom, "snap" to the smallest preset value that is more than the current.
        double currentZoomFactor = getZoomLevel(mWebContents);
        int index = PageZoomUtils.getNextIndex(false, currentZoomFactor);

        if (index <= AVAILABLE_ZOOM_FACTORS.length - 1) {
            handleIndexChanged(index);
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void handleResetClicked(Void unused) {
        // Reset as if the user moved the seekbar to the default zoom value
        handleSeekBarValueChanged(
                PageZoomUtils.convertZoomFactorToSeekBarValue(mDefaultZoomFactor));
    }

    void handleSeekBarValueChanged(int newValue) {
        if (PageZoomUtils.shouldSnapSeekBarValueToDefaultZoom(newValue, mDefaultZoomFactor)) {
            newValue = PageZoomUtils.convertZoomFactorToSeekBarValue(mDefaultZoomFactor);
        }

        setZoomLevel(mWebContents, PageZoomUtils.convertSeekBarValueToZoomFactor(newValue));
        mModel.set(PageZoomProperties.CURRENT_SEEK_VALUE, newValue);
        updateButtonStates(PageZoomUtils.convertSeekBarValueToZoomFactor(newValue));
        mLatestZoomValue = PageZoomUtils.convertSeekBarValueToZoomLevel(newValue);
    }

    private void initialize() {
        // We must first fetch the current zoom factor for the given web contents.
        double currentZoomFactor = getZoomLevel(mWebContents);

        // The seekbar should start at the seek value that corresponds to this zoom factor.
        mModel.set(
                PageZoomProperties.CURRENT_SEEK_VALUE,
                convertZoomFactorToSeekBarValue(currentZoomFactor));

        mDefaultZoomFactor = mModel.get(PageZoomProperties.DEFAULT_ZOOM_FACTOR);

        updateButtonStates(currentZoomFactor);

        // Reset latest zoom value when initializing
        mLatestZoomValue = 0.0;
    }

    private void handleIndexChanged(int index) {
        double zoomFactor = AVAILABLE_ZOOM_FACTORS[index];
        int seekBarValue = PageZoomUtils.convertZoomFactorToSeekBarValue(zoomFactor);
        mModel.set(PageZoomProperties.CURRENT_SEEK_VALUE, seekBarValue);
        setZoomLevel(mWebContents, zoomFactor);
        updateButtonStates(zoomFactor);
        mLatestZoomValue = PageZoomUtils.convertSeekBarValueToZoomLevel(seekBarValue);
    }

    private void updateButtonStates(double newZoomFactor) {
        // If the new zoom factor is greater than the minimum zoom factor, enable decrease button.
        mModel.set(
                PageZoomProperties.DECREASE_ZOOM_ENABLED,
                newZoomFactor > AVAILABLE_ZOOM_FACTORS[0]);

        // If the new zoom factor is less than the maximum zoom factor, enable increase button.
        mModel.set(
                PageZoomProperties.INCREASE_ZOOM_ENABLED,
                newZoomFactor < AVAILABLE_ZOOM_FACTORS[AVAILABLE_ZOOM_FACTORS.length - 1]);
    }

    // Pass-through methods to HostZoomMap, which has static methods to call through JNI.
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void setZoomLevel(@NonNull WebContents webContents, double newZoomLevel) {
        HostZoomMap.setZoomLevel(webContents, newZoomLevel);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    double getZoomLevel(@NonNull WebContents webContents) {
        return HostZoomMap.getZoomLevel(webContents);
    }
}