chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/content/TitleBitmapFactory.java

// Copyright 2015 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.compositor.layouts.content;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.os.SystemClock;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Log;
import android.view.InflateException;

import androidx.annotation.ColorInt;
import androidx.appcompat.content.res.AppCompatResources;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.ColorPickerUtils;
import org.chromium.components.tab_groups.TabGroupColorId;
import org.chromium.ui.util.StyleUtils;

/** A factory that creates text and favicon bitmaps. */
public class TitleBitmapFactory {
    private static final String TAG = "TitleBitmapFactory";

    private static final float TITLE_WIDTH_PERCENTAGE = 1.f;
    // Canvas#drawText() seems to fail when trying to draw 4100 or more characters.
    // See https://crbug.com/524390/ for more details.
    private static final int MAX_NUM_TITLE_CHAR = 1000;

    // We were drawing up to 1000 characters, but only displaying ~30 in the tab strip. Experiment
    // with a smaller limit.
    private static final int SMALLER_MAX_NUM_TITLE_CHAR = 100;

    private final int mMaxWidth;
    private final int mViewHeight;
    private int mFaviconDimension;
    private boolean mIncognito;

    private final TextPaint mTabTextPaint;
    private final float mTabTextHeight;
    private final float mTabTextYOffset;

    private final TextPaint mGroupTextPaint;
    private final int mGroupTextHeight;
    private final float mGroupTextYOffset;

    /**
     * @param context The current Android's context.
     * @param incognito Whether the title are for incognito mode.
     */
    public TitleBitmapFactory(Context context, boolean incognito) {
        Resources res = context.getResources();
        mIncognito = incognito;

        boolean fakeBoldText = res.getBoolean(R.bool.compositor_tab_title_fake_bold_text);

        mTabTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        if (mIncognito) {
            int incognitoTabTextColor =
                    AppCompatResources.getColorStateList(
                                    context, R.color.compositor_tab_title_bar_text_incognito)
                            .getDefaultColor();
            mTabTextPaint.setColor(incognitoTabTextColor);
        }
        StyleUtils.applyTextAppearanceToTextPaint(
                context,
                mTabTextPaint,
                R.style.TextAppearance_TextMedium_Primary,
                /* applyFontFamily= */ true,
                /* applyTextSize= */ true,
                !mIncognito);
        mTabTextPaint.setFakeBoldText(fakeBoldText);
        mTabTextPaint.density = res.getDisplayMetrics().density;
        FontMetrics tabTextFontMetrics = mTabTextPaint.getFontMetrics();
        mTabTextHeight = (float) Math.ceil(tabTextFontMetrics.bottom - tabTextFontMetrics.top);
        mTabTextYOffset = -tabTextFontMetrics.top;

        mGroupTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        StyleUtils.applyTextAppearanceToTextPaint(
                context,
                mGroupTextPaint,
                R.style.TextAppearance_TextSmall,
                /* applyFontFamily */ true,
                /* applyTextSize= */ true,
                /* applyTextColor= */ false);
        mGroupTextPaint.setFakeBoldText(fakeBoldText);
        mGroupTextPaint.density = res.getDisplayMetrics().density;
        FontMetrics groupTextFontMetrics = mGroupTextPaint.getFontMetrics();
        mGroupTextHeight = (int) Math.ceil(groupTextFontMetrics.bottom - groupTextFontMetrics.top);
        mGroupTextYOffset = -groupTextFontMetrics.top;

        mFaviconDimension = res.getDimensionPixelSize(R.dimen.compositor_tab_title_favicon_size);
        mViewHeight = (int) Math.max(mFaviconDimension, mTabTextHeight);

        int width = res.getDisplayMetrics().widthPixels;
        int height = res.getDisplayMetrics().heightPixels;
        mMaxWidth = (int) (TITLE_WIDTH_PERCENTAGE * Math.max(width, height));

        // Set the favicon dimension here.
        mFaviconDimension = Math.min(mMaxWidth, mFaviconDimension);
    }

    /**
     * Generates the favicon bitmap.
     *
     * @param favicon   The favicon of the tab.
     * @return          The Bitmap with the favicon.
     */
    public Bitmap getFaviconBitmap(Bitmap favicon) {
        assert favicon != null;
        try {
            Bitmap b =
                    Bitmap.createBitmap(
                            mFaviconDimension, mFaviconDimension, Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(b);
            if (favicon.getWidth() > mFaviconDimension || favicon.getHeight() > mFaviconDimension) {
                float scale =
                        (float) mFaviconDimension
                                / Math.max(favicon.getWidth(), favicon.getHeight());
                c.scale(scale, scale);
            } else {
                c.translate(
                        Math.round((mFaviconDimension - favicon.getWidth()) / 2.0f),
                        Math.round((mFaviconDimension - favicon.getHeight()) / 2.0f));
            }
            c.drawBitmap(favicon, 0, 0, null);
            return b;
        } catch (OutOfMemoryError ex) {
            Log.e(TAG, "OutOfMemoryError while building favicon texture.");
        } catch (InflateException ex) {
            Log.w(TAG, "InflateException while building favicon texture.");
        }

        return null;
    }

    /**
     * Generates the tab title bitmap.
     *
     * @param title The title of the group.
     * @return The Bitmap with the title.
     */
    public Bitmap getTabTitleBitmap(String title) {
        return getTitleBitmap(mTabTextPaint, mTabTextHeight, mTabTextYOffset, title);
    }

    /**
     * Generates the group title bitmap.
     *
     * @param filter To fetch tab information from.
     * @param context The current Android's context.
     * @param rootId The root ID of the group.
     * @param title The title of the group.
     * @return The Bitmap with the title.
     */
    public Bitmap getGroupTitleBitmap(
            TabGroupModelFilter filter, Context context, int rootId, String title) {
        @TabGroupColorId int colorId = filter.getTabGroupColor(rootId);
        @ColorInt
        int color =
                ColorPickerUtils.getTabGroupColorPickerItemTextColor(context, colorId, mIncognito);
        mGroupTextPaint.setColor(color);
        return getTitleBitmap(mGroupTextPaint, mGroupTextHeight, mGroupTextYOffset, title);
    }

    /**
     * Generates a title bitmap.
     *
     * @param textPaint The TextPaint used to create the title bitmap.
     * @param height The height of the title.
     * @param yOffset The y-offset of the title.
     * @param title The title of the tab.
     * @return The Bitmap with the title.
     */
    public Bitmap getTitleBitmap(TextPaint textPaint, float height, float yOffset, String title) {
        try {
            final long startTime = SystemClock.elapsedRealtime();
            boolean drawText = !TextUtils.isEmpty(title);
            int textWidth = drawText ? getTitleWidth(title, textPaint) : 0;

            // Minimum 1 width bitmap to avoid createBitmap function's IllegalArgumentException,
            // when textWidth == 0.
            Bitmap b =
                    Bitmap.createBitmap(
                            Math.max(Math.min(mMaxWidth, textWidth), 1),
                            mViewHeight,
                            Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(b);
            if (drawText) {
                final int maxCharsToDraw =
                        ChromeFeatureList.sSmallerTabStripTitleLimit.isEnabled()
                                ? SMALLER_MAX_NUM_TITLE_CHAR
                                : MAX_NUM_TITLE_CHAR;
                RecordHistogram.recordCount100Histogram(
                        "Android.TabStrip.TitleBitmapFactory.getTitleBitmap.Length",
                        title.length());
                c.drawText(
                        title,
                        0,
                        Math.min(maxCharsToDraw, title.length()),
                        0,
                        Math.round((mViewHeight - height) / 2.0f + yOffset),
                        textPaint);
            }

            RecordHistogram.recordTimesHistogram(
                    "Android.TabStrip.TitleBitmapFactory.getTitleBitmap.Duration",
                    SystemClock.elapsedRealtime() - startTime);

            return b;
        } catch (OutOfMemoryError ex) {
            Log.e(TAG, "OutOfMemoryError while building title texture.");
        } catch (InflateException ex) {
            Log.w(TAG, "InflateException while building title texture.");
        }

        return null;
    }

    /**
     * @param titleString The title of the tab group.
     * @return The width in px of the title.
     */
    public int getGroupTitleWidth(String titleString) {
        return getTitleWidth(titleString, mGroupTextPaint);
    }

    private int getTitleWidth(String titleString, TextPaint textPaint) {
        return (int) Math.ceil(Layout.getDesiredWidth(titleString, textPaint));
    }
}