chromium/content/public/android/java/src/org/chromium/content/browser/HostZoomMapImpl.java

// Copyright 2021 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.content.browser;

import static org.chromium.content_public.browser.HostZoomMap.TEXT_SIZE_MULTIPLIER_RATIO;
import static org.chromium.content_public.browser.HostZoomMap.getSystemFontScale;
import static org.chromium.content_public.browser.HostZoomMap.setSystemFontScale;

import org.jni_zero.CalledByNative;
import org.jni_zero.CalledByNativeForTesting;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.MathUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.HostZoomMap;
import org.chromium.content_public.browser.SiteZoomInfo;
import org.chromium.content_public.browser.WebContents;

import java.util.HashMap;

/** Implementations of {@link HostZoomMap} */
@JNINamespace("content")
public class HostZoomMapImpl {
    // Private constructor to prevent unwanted construction.
    private HostZoomMapImpl() {}

    /**
     * Set a new zoom level for the given web contents.
     * @param webContents   WebContents to update
     * @param newZoomLevel  double - new zoom level
     */
    public static void setZoomLevel(
            WebContents webContents, double newZoomLevel, double adjustedZoomLevel) {
        HostZoomMapImplJni.get().setZoomLevel(webContents, newZoomLevel, adjustedZoomLevel);
    }

    /**
     * Set a new zoom level for the given host.
     * @param host   host url to update.
     * @param level  double - new zoom level.
     * @param browserContextHandle BrowserContextHandle to update host zoom for.
     */
    public static void setZoomLevelForHost(
            BrowserContextHandle browserContextHandle, String host, double level) {
        HostZoomMapImplJni.get().setZoomLevelForHost(browserContextHandle, host, level);
    }

    /**
     * Gets zoom levels for all hosts.
     * @param browserContextHandle BrowserContextHandle to get host zooms for.
     */
    public static HashMap<String, Double> getAllHostZoomLevels(
            BrowserContextHandle browserContextHandle) {
        SiteZoomInfo[] siteZoomInfoList =
                HostZoomMapImplJni.get().getAllHostZoomLevels(browserContextHandle);
        HashMap<String, Double> hostToZoomLevel = new HashMap<>();
        for (int i = 0; i < siteZoomInfoList.length; i++) {
            hostToZoomLevel.put(siteZoomInfoList[i].host, siteZoomInfoList[i].zoomLevel);
        }
        return hostToZoomLevel;
    }

    /**
     * Get the current zoom level for a given web contents.
     * @param webContents   WebContents to get zoom level for
     * @return double       current zoom level
     */
    public static double getZoomLevel(WebContents webContents) {
        return HostZoomMapImplJni.get().getZoomLevel(webContents);
    }

    /**
     * Set the default zoom level for a given browser context handle (e.g. Profile).
     * @param context       BrowserContextHandle to update default for.
     * @param newDefaultZoomLevel   double, new default value.
     */
    public static void setDefaultZoomLevel(
            BrowserContextHandle context, double newDefaultZoomLevel) {
        HostZoomMapImplJni.get().setDefaultZoomLevel(context, newDefaultZoomLevel);
    }

    /**
     * Get the default zoom level for a given browser context handle (e.g. Profile).
     * @param context       BrowserContextHandle to get default for.
     * @return double       default zoom level.
     */
    public static double getDefaultZoomLevel(BrowserContextHandle context) {
        return HostZoomMapImplJni.get().getDefaultZoomLevel(context);
    }

    @CalledByNative
    public static double getAdjustedZoomLevel(double zoomLevel) {
        float systemFontScale = getSystemFontScale();
        // The OS |fontScale| will not be factored in zoom estimation if Page Zoom is disabled; a
        // systemFontScale = 1 will be used in this case.
        if (!ContentFeatureMap.isEnabled(ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM)
                || !shouldAdjustForOSLevel()) {
            systemFontScale = 1;
        }
        return adjustZoomLevel(zoomLevel, systemFontScale);
    }

    @CalledByNative
    public static SiteZoomInfo buildSiteZoomInfo(
            @JniType("std::string") String host, double zoomLevel) {
        return new SiteZoomInfo(host, zoomLevel);
    }

    /**
     * Returns true when the field trial param to adjust zoom for OS-level font setting is true,
     * false otherwise.
     *
     * <p>Note: The former text-autoscaling feature takes the OS-level font setting into account, so
     * this will be included in any text-size-adjust changes. We default to false here so that we do
     * not double-apply the setting.
     *
     * @return bool True if zoom should be adjusted.
     */
    public static boolean shouldAdjustForOSLevel() {
        return ContentFeatureMap.getInstance()
                .getFieldTrialParamByFeatureAsBoolean(
                        ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM,
                        ContentFeatureList.ACCESSIBILITY_PAGE_ZOOM_PARAM,
                        false);
    }

    /**
     * Adjust a given zoom level to account for the OS-level |fontScale| configuration on Android.
     *
     * @param zoomLevel The zoom level to adjust.
     * @param systemFontScale User selected font scale value.
     * @return double The adjusted zoom level.
     */
    public static double adjustZoomLevel(double zoomLevel, float systemFontScale) {
        // No calculation to do if the user has set OS-level |fontScale| to 1 (default).
        if (MathUtils.areFloatsEqual(systemFontScale, 1f) || !shouldAdjustForOSLevel()) {
            return zoomLevel;
        }

        // Convert the zoom factor to a level, e.g. factor = 0.0 should translate to 1.0 (100%).
        // Multiply the level by the OS-level |fontScale|. For example, if the user has chosen a
        // Chrome-level zoom of 150%, and a OS-level setting of XL (130%), then we want to continue
        // to display 150% to the user but actually render 1.5 * 1.3 = 1.95 (195%) zoom. We must
        // apply this at the zoom level (not factor) to compensate for logarithmic scale.
        double adjustedLevel = systemFontScale * Math.pow(TEXT_SIZE_MULTIPLIER_RATIO, zoomLevel);

        // We do not pass levels to the backend, but factors. So convert back and round.
        double adjustedFactor = Math.log10(adjustedLevel) / Math.log10(TEXT_SIZE_MULTIPLIER_RATIO);
        return MathUtils.roundTwoDecimalPlaces(adjustedFactor);
    }

    @CalledByNativeForTesting
    public static void setSystemFontScaleForTesting(float scale) {
        var oldValue = getSystemFontScale();
        setSystemFontScale(scale);
        ResettersForTesting.register(() -> setSystemFontScale(oldValue));
    }

    @NativeMethods
    public interface Natives {
        void setZoomLevel(WebContents webContents, double newZoomLevel, double adjustedZoomLevel);

        double getZoomLevel(WebContents webContents);

        void setDefaultZoomLevel(BrowserContextHandle context, double newDefaultZoomLevel);

        double getDefaultZoomLevel(BrowserContextHandle context);

        @JniType("std::vector")
        SiteZoomInfo[] getAllHostZoomLevels(BrowserContextHandle context);

        void setZoomLevelForHost(BrowserContextHandle context, String host, double level);
    }
}