chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/RoundedIconGenerator.java

// Copyright 2014 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.widget;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.RectF;
import android.text.TextPaint;
import android.text.TextUtils;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.url.GURL;
import org.chromium.url.URI;

import java.util.Locale;

/**
 * Generator for transparent icons containing a rounded rectangle with a given background color,
 * having a centered character drawn on top of it.
 */
public class RoundedIconGenerator {
    private static final String TAG = "RoundedIconGenerator";

    private final int mIconWidthPx;
    private final int mIconHeightPx;
    private final int mCornerRadiusPx;

    private final RectF mBackgroundRect;

    private final Paint mBackgroundPaint;
    private final TextPaint mTextPaint;

    private final float mTextBaselineY;

    /**
     * Constructs the generator and initializes the common members based on the display density.
     *
     * @param res The resources used to convert sizes to px.
     * @param iconWidthDp The width of the generated icon in dp.
     * @param iconHeightDp The height of the generated icon in dp.
     * @param cornerRadiusDp The radius of the corners in the icon in dp.
     * @param backgroundColor Color with which the rounded rectangle should be drawn.
     * @param textSizeDp Size at which the text should be drawn in dp.
     */
    public RoundedIconGenerator(
            Resources res,
            int iconWidthDp,
            int iconHeightDp,
            int cornerRadiusDp,
            int backgroundColor,
            int textSizeDp) {
        this(
                (int) (res.getDisplayMetrics().density * iconWidthDp),
                (int) (res.getDisplayMetrics().density * iconHeightDp),
                (int) (res.getDisplayMetrics().density * cornerRadiusDp),
                backgroundColor,
                res.getDisplayMetrics().density * textSizeDp);
    }

    /**
     * Constructs the generator and initializes the common members ignoring display density.
     *
     * @param iconWidthPx The width of the generated icon in pixels.
     * @param iconHeightPx The height of the generated icon in pixels.
     * @param cornerRadiusPx The radius of the corners in the icon in pixels.
     * @param backgroundColor Color at which the rounded rectangle should be drawn.
     * @param textSizePx Size at which the text should be drawn in pixels.
     */
    public RoundedIconGenerator(
            int iconWidthPx,
            int iconHeightPx,
            int cornerRadiusPx,
            int backgroundColor,
            float textSizePx) {
        mIconWidthPx = iconWidthPx;
        mIconHeightPx = iconHeightPx;
        mCornerRadiusPx = cornerRadiusPx;

        mBackgroundRect = new RectF(0, 0, mIconWidthPx, mIconHeightPx);

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(backgroundColor);

        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setFakeBoldText(true);
        mTextPaint.setTextSize(textSizePx);
        mTextPaint.setTextAlign(Paint.Align.CENTER);

        FontMetrics textFontMetrics = mTextPaint.getFontMetrics();
        // Font's top is above the baseline, so top is negative.
        final float fontMaxHeight = textFontMetrics.bottom - textFontMetrics.top;

        // Y value that centers the tallest bounding box vertically.
        final float textTop = Math.max((mIconHeightPx - fontMaxHeight) / 2f, 0f);
        mTextBaselineY = textTop - textFontMetrics.top;
    }

    /** Sets the background color to use when generating icons. */
    public void setBackgroundColor(@ColorInt int color) {
        mBackgroundPaint.setColor(color);
    }

    /**
     * Generates an icon based on |text| (using the first character).
     *
     * @param text The text to render the first character of on the icon.
     * @return The generated icon.
     */
    public Bitmap generateIconForText(String text) {
        Bitmap icon = Bitmap.createBitmap(mIconWidthPx, mIconHeightPx, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(icon);

        canvas.drawRoundRect(mBackgroundRect, mCornerRadiusPx, mCornerRadiusPx, mBackgroundPaint);

        int length = Math.min(1, text.length());
        String displayText = text.substring(0, length).toUpperCase(Locale.getDefault());
        // Using Align.CENTER, so X is in the middle of the icon.
        canvas.drawText(displayText, mIconWidthPx / 2f, mTextBaselineY, mTextPaint);

        return icon;
    }

    /** {@link #generateIconForUrl(GURL, boolean)} */
    @Nullable
    @Deprecated // TODO(crbug.com/40549331): Use GURL-variant or #generateIconForText
    public Bitmap generateIconForUrl(String url, boolean includePrivateRegistries) {
        if (TextUtils.isEmpty(url)) return null;

        String text = getIconTextForUrl(url, includePrivateRegistries);
        if (TextUtils.isEmpty(text)) return null;

        return generateIconForText(text);
    }

    /**
     * Returns a Bitmap representing the icon to be used for |url|.
     *
     * @param url URL for which the icon should be generated.
     * @param includePrivateRegistries Should private registries be considered as TLDs?
     * @return The generated icon, or NULL if |url| is empty or the domain cannot be resolved.
     */
    @Nullable
    public Bitmap generateIconForUrl(GURL url, boolean includePrivateRegistries) {
        if (url == null) return null;
        return generateIconForUrl(url.getSpec(), includePrivateRegistries);
    }

    /** {@link #generateIconForUrl(GURL)} */
    @Nullable
    @Deprecated // TODO(crbug.com/40549331): Use GURL-variant or #generateIconForText
    public Bitmap generateIconForUrl(String url) {
        return generateIconForUrl(url, false);
    }

    /**
     * Returns a Bitmap representing the icon to be used for |url|. Private registries such
     * as "appspot.com" will not be considered as effective TLDs.
     *
     * @TODO(beverloo) Update all call-sites of rounded icons to be explicit about whether
     * private registries should be considered, matching the getDomainAndRegistry requirements.
     * See https://crbug.com/458104.
     *
     * @param url URL for which the icon should be generated.
     * @return The generated icon, or NULL if |url| is empty or the domain cannot be resolved.
     */
    @Nullable
    public Bitmap generateIconForUrl(GURL url) {
        return generateIconForUrl(url.getSpec(), false);
    }

    /**
     * Returns the text which should be used for generating a rounded icon based on |url|.
     *
     * @param url URL to consider when getting the icon's text.
     * @param includePrivateRegistries Should private registries be considered as TLDs?
     * @return The text to use on the rounded icon, or NULL if |url| is empty or the domain cannot
     *         be resolved.
     */
    @Nullable
    @VisibleForTesting
    public static String getIconTextForUrl(String url, boolean includePrivateRegistries) {
        String domain = UrlUtilities.getDomainAndRegistry(url, includePrivateRegistries);
        if (!TextUtils.isEmpty(domain)) return domain;

        // Special-case chrome:// and chrome-native:// URLs.
        if (url.startsWith(UrlConstants.CHROME_URL_PREFIX)
                || url.startsWith(UrlConstants.CHROME_NATIVE_URL_PREFIX)) {
            return UrlConstants.CHROME_SCHEME;
        }

        // Use the host component of |url| when it can be parsed as a URI.
        try {
            URI uri = new URI(url);
            if (!TextUtils.isEmpty(uri.getHost())) {
                return uri.getHost();
            }
        } catch (Exception e) {
            Log.w(TAG, "Unable to parse the URL for generating an icon: " + url);
        }

        return url;
    }
}