chromium/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabListFaviconProvider.java

// Copyright 2024 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.tab_ui;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;

import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;

import org.chromium.base.Callback;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.ui.base.ViewUtils;
import org.chromium.url.GURL;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/** Provider for processed favicons in Tab list. */
public class TabListFaviconProvider {
    /**
     * Wrapper class that holds a favicon drawable and whether recolor is allowed. Subclasses should
     * make a best effort to implement an {@link Object#equals(Object)} that will allow efficient
     * comparisons of favicon objects.
     */
    public abstract static class TabFavicon {
        private final @NonNull Drawable mDefaultDrawable;
        private final @NonNull Drawable mSelectedDrawable;
        private final boolean mIsRecolorAllowed;

        protected TabFavicon(
                @NonNull Drawable defaultDrawable,
                @NonNull Drawable selectedDrawable,
                boolean allowRecolor) {
            mDefaultDrawable = defaultDrawable;
            mSelectedDrawable = selectedDrawable;
            mIsRecolorAllowed = allowRecolor;
        }

        /** Return the {@link Drawable} when this favicon is not selected */
        public Drawable getDefaultDrawable() {
            return mDefaultDrawable;
        }

        /** Return the {@link Drawable} when this favicon is selected. */
        public Drawable getSelectedDrawable() {
            return mSelectedDrawable;
        }

        /** Return whether this {@link TabFavicon} has a different drawable when selected. */
        public boolean hasSelectedState() {
            return mDefaultDrawable != mSelectedDrawable;
        }

        /** Return whether the drawables this {@link TabFavicon} contains can be recolored. */
        public boolean isRecolorAllowed() {
            return mIsRecolorAllowed;
        }

        @Override
        public abstract boolean equals(Object other);
    }

    /** A favicon that is sourced from and equality checked on a single URL. */
    @VisibleForTesting
    public static class UrlTabFavicon extends TabFavicon {
        private final @NonNull GURL mGurl;

        private UrlTabFavicon(
                @NonNull Drawable defaultDrawable,
                @NonNull Drawable selectedDrawable,
                boolean allowRecolor,
                @NonNull GURL gurl) {
            super(defaultDrawable, selectedDrawable, allowRecolor);
            mGurl = gurl;
        }

        @VisibleForTesting
        public UrlTabFavicon(@NonNull Drawable drawable, @NonNull GURL gurl) {
            this(drawable, drawable, false, gurl);
        }

        @Override
        public int hashCode() {
            return mGurl.hashCode();
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof UrlTabFavicon)) {
                return false;
            }
            return Objects.equals(mGurl, ((UrlTabFavicon) other).mGurl);
        }
    }

    /** Tracks the GURLS that were used for the composed favicon for the equality check.  */
    @VisibleForTesting
    public static class ComposedTabFavicon extends TabFavicon {
        private final @NonNull GURL[] mGurls;

        @VisibleForTesting
        public ComposedTabFavicon(@NonNull Drawable drawable, @NonNull GURL[] gurls) {
            super(drawable, drawable, false);
            mGurls = gurls;
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(mGurls);
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof ComposedTabFavicon)) {
                return false;
            }
            return Arrays.equals(mGurls, ((ComposedTabFavicon) other).mGurls);
        }
    }

    @IntDef({
        StaticTabFaviconType.UNKNOWN,
        StaticTabFaviconType.ROUNDED_GLOBE,
        StaticTabFaviconType.ROUNDED_CHROME,
        StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT,
        StaticTabFaviconType.ROUNDED_GLOBE_INCOGNITO,
        StaticTabFaviconType.ROUNDED_CHROME_INCOGNITO,
        StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT_INCOGNITO,
        StaticTabFaviconType.ROUNDED_GLOBE_FOR_STRIP,
        StaticTabFaviconType.ROUNDED_CHROME_FOR_STRIP,
    })
    @Retention(RetentionPolicy.SOURCE)
    @VisibleForTesting
    public @interface StaticTabFaviconType {
        int UNKNOWN = 0;
        int ROUNDED_GLOBE = 1;
        int ROUNDED_CHROME = 2;
        int ROUNDED_COMPOSED_DEFAULT = 3;
        int ROUNDED_GLOBE_INCOGNITO = 4;
        int ROUNDED_CHROME_INCOGNITO = 5;
        int ROUNDED_COMPOSED_DEFAULT_INCOGNITO = 6;
        int ROUNDED_GLOBE_FOR_STRIP = 7;
        int ROUNDED_CHROME_FOR_STRIP = 8;
    }

    /** A favicon that is one of a fixed number of static icons. */
    @VisibleForTesting
    public static class ResourceTabFavicon extends TabFavicon {
        private final @StaticTabFaviconType int mType;

        private ResourceTabFavicon(
                @NonNull Drawable defaultDrawable,
                @NonNull Drawable selectedDrawable,
                boolean allowRecolor,
                @StaticTabFaviconType int type) {
            super(defaultDrawable, selectedDrawable, allowRecolor);
            mType = type;
        }

        @VisibleForTesting
        public ResourceTabFavicon(@NonNull Drawable defaultDrawable, @StaticTabFaviconType int type) {
            this(defaultDrawable, defaultDrawable, false, type);
        }

        @Override
        public int hashCode() {
            return Integer.hashCode(mType);
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof ResourceTabFavicon)) {
                return false;
            }
            return this.mType == ((ResourceTabFavicon) other).mType;
        }
    }

    private interface TabFaviconResolver {
        /** Attempts to create a {@link TabFavicon} from {@link Context}. */
        @Nullable
        TabFavicon resolve(Context context);
    }

    /** Lazily resolves a static {@link TabFavicon}. */
    private static class LazyTabFaviconResolver {
        // Null after resolution succeeds.
        private @Nullable TabFaviconResolver mResolver;
        // Null until resolution succeeds.
        private @Nullable TabFavicon mTabFavicon;

        LazyTabFaviconResolver(TabFaviconResolver resolver) {
            assert resolver != null;
            mResolver = resolver;
        }

        TabFavicon get(Context context) {
            if (mTabFavicon == null) {
                mTabFavicon = mResolver.resolve(context);
                if (mTabFavicon != null) {
                    mResolver = null;
                }
            }
            return mTabFavicon;
        }
    }

    private static LazyTabFaviconResolver sRoundedGlobeFavicon;

    /** This icon may fail to load. See crbug.com/324996488. */
    private static LazyTabFaviconResolver sRoundedChromeFavicon;

    private static LazyTabFaviconResolver sRoundedComposedDefaultFavicon;

    private static LazyTabFaviconResolver sRoundedGlobeFaviconIncognito;

    /** This icon may fail to load. See crbug.com/324996488. */
    private static LazyTabFaviconResolver sRoundedChromeFaviconIncognito;

    private static LazyTabFaviconResolver sRoundedComposedDefaultFaviconIncognito;

    private static LazyTabFaviconResolver sRoundedGlobeFaviconForStrip;
    private static LazyTabFaviconResolver sRoundedChromeFaviconForStrip;

    private final @ColorInt int mSelectedIconColor;
    private final @ColorInt int mIncognitoSelectedIconColor;

    private final int mStripFaviconSize;
    private final int mDefaultFaviconSize;
    private final int mFaviconSize;
    private final int mFaviconInset;
    private final int mFaviconCornerRadius;

    private final Context mContext;
    private final boolean mIsTabStrip;
    private boolean mIsInitialized;

    private Profile mProfile;
    private FaviconHelper mFaviconHelper;

    /**
     * Construct the provider that provides favicons for tab list.
     * @param context    The context to use for accessing {@link android.content.res.Resources}
     * @param isTabStrip Indicator for whether this class provides favicons for tab strip or not.
     * @param faviconCornerRadiusId The resource Id for the favicon corner radius.
     *
     */
    public TabListFaviconProvider(Context context, boolean isTabStrip, int faviconCornerRadiusId) {
        mContext = context;
        mDefaultFaviconSize =
                context.getResources().getDimensionPixelSize(R.dimen.tab_grid_favicon_size);
        mStripFaviconSize =
                context.getResources().getDimensionPixelSize(R.dimen.tab_strip_favicon_size);
        mFaviconSize = isTabStrip ? mStripFaviconSize : mDefaultFaviconSize;
        mFaviconInset =
                ViewUtils.dpToPx(
                        context,
                        context.getResources()
                                .getDimensionPixelSize(R.dimen.tab_strip_favicon_inset));
        mIsTabStrip = isTabStrip;
        mFaviconCornerRadius = context.getResources().getDimensionPixelSize(faviconCornerRadiusId);

        @ColorInt
        int defaultIconColor =
                TabUiThemeUtils.getChromeOwnedFaviconTintColor(context, false, false);
        mSelectedIconColor = TabUiThemeUtils.getChromeOwnedFaviconTintColor(context, false, true);
        @ColorInt
        int incognitoIconColor =
                TabUiThemeUtils.getChromeOwnedFaviconTintColor(context, true, false);
        mIncognitoSelectedIconColor =
                TabUiThemeUtils.getChromeOwnedFaviconTintColor(context, true, true);
        maybeSetUpLazyTabFaviconResolvers(
                defaultIconColor,
                mSelectedIconColor,
                incognitoIconColor,
                mIncognitoSelectedIconColor,
                mDefaultFaviconSize,
                mStripFaviconSize,
                mFaviconCornerRadius,
                mFaviconInset);
    }

    private static void maybeSetUpLazyTabFaviconResolvers(
            @ColorInt int defaultIconColor,
            @ColorInt int selectedIconColor,
            @ColorInt int incognitoIconColor,
            @ColorInt int incognitoSelectedIconColor,
            int defaultFaviconSize,
            int stripFaviconSize,
            int cornerRadius,
            int inset) {
        if (sRoundedGlobeFavicon == null) {
            sRoundedGlobeFavicon =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                // TODO(crbug.com/40682607): From Android Developer Documentation,
                                // we should avoid resizing vector drawables.
                                Bitmap globeBitmap =
                                        getResizedBitmapFromDrawable(
                                                AppCompatResources.getDrawable(
                                                        context, R.drawable.ic_globe_24dp),
                                                defaultFaviconSize);
                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        globeBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        defaultIconColor,
                                        selectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_GLOBE);
                            });
        }
        if (sRoundedChromeFavicon == null) {
            sRoundedChromeFavicon =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Bitmap chromeBitmap =
                                        BitmapFactory.decodeResource(
                                                context.getResources(), R.drawable.chromelogo16);
                                if (chromeBitmap == null) return null;

                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        chromeBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        defaultIconColor,
                                        selectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_CHROME);
                            });
        }
        if (sRoundedComposedDefaultFavicon == null) {
            sRoundedComposedDefaultFavicon =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Bitmap composedBitmap =
                                        getResizedBitmapFromDrawable(
                                                AppCompatResources.getDrawable(
                                                        context, R.drawable.ic_group_icon_16dp),
                                                defaultFaviconSize);
                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        composedBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        defaultIconColor,
                                        selectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT);
                            });
        }
        if (sRoundedGlobeFaviconIncognito == null) {
            sRoundedGlobeFaviconIncognito =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Bitmap globeBitmap =
                                        getResizedBitmapFromDrawable(
                                                AppCompatResources.getDrawable(
                                                        context, R.drawable.ic_globe_24dp),
                                                defaultFaviconSize);
                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        globeBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        incognitoIconColor,
                                        incognitoSelectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_GLOBE_INCOGNITO);
                            });
        }
        if (sRoundedChromeFaviconIncognito == null) {
            sRoundedChromeFaviconIncognito =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Bitmap chromeBitmap =
                                        BitmapFactory.decodeResource(
                                                context.getResources(), R.drawable.chromelogo16);
                                if (chromeBitmap == null) return null;

                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        chromeBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        incognitoIconColor,
                                        incognitoSelectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_CHROME_INCOGNITO);
                            });
        }
        if (sRoundedComposedDefaultFaviconIncognito == null) {
            sRoundedComposedDefaultFaviconIncognito =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Bitmap composedBitmap =
                                        getResizedBitmapFromDrawable(
                                                AppCompatResources.getDrawable(
                                                        context, R.drawable.ic_group_icon_16dp),
                                                defaultFaviconSize);
                                return createChromeOwnedResourceTabFavicon(
                                        context,
                                        composedBitmap,
                                        defaultFaviconSize,
                                        cornerRadius,
                                        incognitoIconColor,
                                        incognitoSelectedIconColor,
                                        false,
                                        StaticTabFaviconType.ROUNDED_COMPOSED_DEFAULT_INCOGNITO);
                            });
        }

        // Tab strip favicons do not recolor when selected.
        if (sRoundedGlobeFaviconForStrip == null) {
            sRoundedGlobeFaviconForStrip =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Drawable globeDrawable =
                                        AppCompatResources.getDrawable(
                                                context, R.drawable.ic_globe_24dp);
                                return new ResourceTabFavicon(
                                        processBitampWithBackground(
                                                context,
                                                getResizedBitmapFromDrawable(
                                                        globeDrawable, stripFaviconSize),
                                                stripFaviconSize,
                                                cornerRadius,
                                                inset),
                                        StaticTabFaviconType.ROUNDED_GLOBE_FOR_STRIP);
                            });
        }
        if (sRoundedChromeFaviconForStrip == null) {
            sRoundedChromeFaviconForStrip =
                    new LazyTabFaviconResolver(
                            (context) -> {
                                Drawable chromeDrawable =
                                        AppCompatResources.getDrawable(
                                                context, R.drawable.chromelogo16);
                                return new ResourceTabFavicon(
                                        processBitampWithBackground(
                                                context,
                                                getResizedBitmapFromDrawable(
                                                        chromeDrawable, stripFaviconSize),
                                                stripFaviconSize,
                                                cornerRadius,
                                                inset),
                                        StaticTabFaviconType.ROUNDED_CHROME_FOR_STRIP);
                            });
        }
    }

    public void initForTesting(Profile profile, FaviconHelper helper) {
        assert !mIsInitialized;
        mProfile = profile;
        mFaviconHelper = helper;
        mIsInitialized = true;
    }

    public void initWithNative(Profile profile) {
        if (mIsInitialized) return;

        mProfile = profile;
        assert mProfile != null : "Profile must exist for favicon fetching.";
        mFaviconHelper = new FaviconHelper();
        mIsInitialized = true;
    }

    private Profile getProfile(boolean isIncognito) {
        if (!isIncognito) return mProfile;

        Profile otrProfile = mProfile.getPrimaryOTRProfile(/* createIfNeeded= */ false);
        assert otrProfile != null : "Requesting favicon for OTR Profile when none exists.";
        return otrProfile;
    }

    public boolean isInitialized() {
        return mIsInitialized;
    }

    private static Bitmap getResizedBitmapFromDrawable(Drawable drawable, int size) {
        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, size, size);
        drawable.draw(canvas);
        return bitmap;
    }

    private Drawable processBitmap(Bitmap bitmap, boolean isTabStrip) {
        if (isTabStrip) {
            return processBitampWithBackground(
                    mContext, bitmap, mStripFaviconSize, mFaviconCornerRadius, mFaviconInset);
        } else {
            return processBitmapNoBackground(
                    mContext, bitmap, mDefaultFaviconSize, mFaviconCornerRadius);
        }
    }

    private static Drawable processBitmapNoBackground(
            Context context, Bitmap bitmap, int size, int cornerRadius) {
        return ViewUtils.createRoundedBitmapDrawable(
                context.getResources(),
                Bitmap.createScaledBitmap(bitmap, size, size, true),
                cornerRadius);
    }

    private static Drawable processBitampWithBackground(
            Context context, Bitmap bitmap, int size, int cornerRadius, int inset) {
        Drawable favicon = processBitmapNoBackground(context, bitmap, size, cornerRadius);
        Drawable circleBackground =
                AppCompatResources.getDrawable(context, R.drawable.tab_strip_favicon_circle);
        Drawable[] layers = {circleBackground, favicon};
        LayerDrawable layerDrawable = new LayerDrawable(layers);
        layerDrawable.setLayerInset(1, inset, inset, inset, inset);
        return layerDrawable;
    }

    /**
     * Interface for lazily fetching favicons. Instances of this class should implement the fetch
     * method to resolve to an appropriate favicon returned via callback when invoked.
     */
    public interface TabFaviconFetcher {
        /**
         * Asynchronously fetches a tab favicon.
         * @param faviconCallback Called once with a favicon for the tab. Payload may be null.
         */
        public void fetch(Callback<TabFavicon> faviconCallback);
    }

    public TabFaviconFetcher getDefaultFaviconFetcher(boolean isIncognito) {
        return new TabFaviconFetcher() {
            @Override
            public void fetch(Callback<TabFavicon> faviconCallback) {
                faviconCallback.onResult(getDefaultFavicon(isIncognito));
            }
        };
    }

    /**
     * Returns the scaled rounded globe drawable used for default favicon. Used when favicon is
     * static and not changing colors when its parent component is selected.
     * @see #getDefaultFavicon(boolean)
     */
    public Drawable getDefaultFaviconDrawable(boolean isIncognito) {
        return getDefaultFavicon(isIncognito).getDefaultDrawable();
    }

    /**
     * @return The scaled rounded Globe {@link TabFavicon} as default favicon.
     * @param isIncognito Whether the {@link TabFavicon} is used for incognito mode.
     */
    public TabFavicon getDefaultFavicon(boolean isIncognito) {
        return getRoundedGlobeFavicon(isIncognito);
    }

    /**
     * Asynchronously get the processed {@link Drawable}. Used when favicon is static and not
     * changing colors when its parent component is selected.
     * @see #getFaviconForUrlAsync(GURL, boolean, Callback)
     */
    public void getFaviconDrawableForUrlAsync(
            GURL url, boolean isIncognito, Callback<Drawable> faviconCallback) {
        getFaviconForUrlAsync(
                url,
                isIncognito,
                tabFavicon -> faviconCallback.onResult(tabFavicon.getDefaultDrawable()));
    }

    public TabFaviconFetcher getFaviconForUrlFetcher(GURL url, boolean isIncognito) {
        return new TabFaviconFetcher() {
            @Override
            public void fetch(Callback<TabFavicon> faviconCallback) {
                getFaviconForUrlAsync(url, isIncognito, faviconCallback);
            }
        };
    }

    /**
     * Asynchronously get the processed {@link TabFavicon}.
     * @param url The URL of the tab whose favicon is being requested.
     * @param isIncognito Whether the tab is incognito or not.
     * @param faviconCallback The callback that requests for favicon.
     */
    public void getFaviconForUrlAsync(
            GURL url, boolean isIncognito, Callback<TabFavicon> faviconCallback) {
        if (mFaviconHelper == null || UrlUtilities.isNtpUrl(url)) {
            faviconCallback.onResult(getRoundedChromeFavicon(isIncognito));
        } else {
            mFaviconHelper.getLocalFaviconImageForURL(
                    getProfile(isIncognito),
                    url,
                    mFaviconSize,
                    (image, iconUrl) -> {
                        TabFavicon favicon;
                        if (image == null) {
                            favicon = getRoundedGlobeFavicon(isIncognito);
                        } else if (UrlUtilities.isInternalScheme(url) && !mIsTabStrip) {
                            Bitmap resizedFavicon =
                                    getResizedBitmapFromDrawable(
                                            processBitmap(image, false), mDefaultFaviconSize);
                            favicon =
                                    isIncognito
                                            ? createChromeOwnedUrlTabFavicon(
                                                    resizedFavicon,
                                                    0,
                                                    mIncognitoSelectedIconColor,
                                                    true,
                                                    iconUrl)
                                            : createChromeOwnedUrlTabFavicon(
                                                    resizedFavicon,
                                                    0,
                                                    mSelectedIconColor,
                                                    true,
                                                    iconUrl);
                        } else {
                            favicon = new UrlTabFavicon(processBitmap(image, mIsTabStrip), iconUrl);
                        }
                        faviconCallback.onResult(favicon);
                    });
        }
    }

    public TabFaviconFetcher getFaviconFromBitmapFetcher(
            @NonNull Bitmap icon, @NonNull GURL iconUrl) {
        Drawable processedBitmap = processBitmap(icon, mIsTabStrip);
        return new TabFaviconFetcher() {
            @Override
            public void fetch(Callback<TabFavicon> faviconCallback) {
                faviconCallback.onResult(new UrlTabFavicon(processedBitmap, iconUrl));
            }
        };
    }

    /**
     * Synchronously get the processed favicon, assuming it is not recolor allowed.
     * @param icon The favicon that was received.
     * @param iconUrl The url the favicon came from.
     * @return The processed {@link TabFavicon}.
     */
    public TabFavicon getFaviconFromBitmap(@NonNull Bitmap icon, @NonNull GURL iconUrl) {
        return new UrlTabFavicon(processBitmap(icon, mIsTabStrip), iconUrl);
    }

    public TabFaviconFetcher getComposedFaviconImageFetcher(List<GURL> urls, boolean isIncognito) {
        return new TabFaviconFetcher() {
            @Override
            public void fetch(Callback<TabFavicon> faviconCallback) {
                getComposedFaviconImageAsync(urls, isIncognito, faviconCallback);
            }
        };
    }

    /**
     * Asynchronously get the composed, up to 4, {{@link TabFavicon}}.
     * @param urls List of urls, up to 4, whose favicon are requested to be composed.
     * @param isIncognito Whether the processed composed favicon is used for incognito or not.
     * @param faviconCallback The callback that requests for the composed favicon.
     */
    void getComposedFaviconImageAsync(
            List<GURL> urls, boolean isIncognito, Callback<TabFavicon> faviconCallback) {
        assert urls != null && urls.size() > 1 && urls.size() <= 4;
        mFaviconHelper.getComposedFaviconImage(
                getProfile(isIncognito),
                urls,
                mFaviconSize,
                (image, iconUrls) -> {
                    if (image == null) {
                        faviconCallback.onResult(getDefaultComposedImageFavicon(isIncognito));
                    } else {
                        faviconCallback.onResult(
                                new ComposedTabFavicon(
                                        processBitmap(image, mIsTabStrip), iconUrls));
                    }
                });
    }

    private TabFavicon getDefaultComposedImageFavicon(boolean isIncognito) {
        return isIncognito
                ? sRoundedComposedDefaultFaviconIncognito.get(mContext)
                : colorFaviconWithTheme(sRoundedComposedDefaultFavicon.get(mContext));
    }

    public TabFavicon getRoundedChromeFavicon(boolean isIncognito) {
        if (mIsTabStrip) {
            return sRoundedChromeFaviconForStrip.get(mContext);
        }
        // Fallback if the bitmap decoding failed.
        if (isIncognito
                ? (sRoundedChromeFaviconIncognito.get(mContext) == null)
                : (sRoundedChromeFavicon.get(mContext) == null)) {
            return getRoundedGlobeFavicon(isIncognito);
        }
        return isIncognito
                ? sRoundedChromeFaviconIncognito.get(mContext)
                : colorFaviconWithTheme(sRoundedChromeFavicon.get(mContext));
    }

    private TabFavicon getRoundedGlobeFavicon(boolean isIncognito) {
        if (mIsTabStrip) {
            return sRoundedGlobeFaviconForStrip.get(mContext);
        }
        return isIncognito
                ? sRoundedGlobeFaviconIncognito.get(mContext)
                : colorFaviconWithTheme(sRoundedGlobeFavicon.get(mContext));
    }

    private TabFavicon createChromeOwnedUrlTabFavicon(
            Bitmap bitmap,
            @ColorInt int colorDefault,
            @ColorInt int colorSelected,
            boolean useBitmapColorInDefault,
            GURL gurl) {
        Drawable defaultDrawable =
                processBitmapMaybeColor(
                        mContext,
                        bitmap,
                        mDefaultFaviconSize,
                        mFaviconCornerRadius,
                        !useBitmapColorInDefault,
                        colorDefault);
        Drawable selectedDrawable =
                processBitmapMaybeColor(
                        mContext,
                        bitmap,
                        mDefaultFaviconSize,
                        mFaviconCornerRadius,
                        true,
                        colorSelected);
        return new UrlTabFavicon(defaultDrawable, selectedDrawable, true, gurl);
    }

    private static TabFavicon createChromeOwnedResourceTabFavicon(
            Context context,
            Bitmap bitmap,
            int size,
            int cornerRadius,
            @ColorInt int colorDefault,
            @ColorInt int colorSelected,
            boolean useBitmapColorInDefault,
            @StaticTabFaviconType int type) {
        Drawable defaultDrawable =
                processBitmapMaybeColor(
                        context,
                        bitmap,
                        size,
                        cornerRadius,
                        !useBitmapColorInDefault,
                        colorDefault);
        Drawable selectedDrawable =
                processBitmapMaybeColor(context, bitmap, size, cornerRadius, true, colorSelected);
        return new ResourceTabFavicon(defaultDrawable, selectedDrawable, true, type);
    }

    private static Drawable processBitmapMaybeColor(
            Context context,
            Bitmap bitmap,
            int size,
            int cornerRadius,
            boolean shouldSetColor,
            @ColorInt int color) {
        Drawable drawable = processBitmapNoBackground(context, bitmap, size, cornerRadius);
        if (shouldSetColor) {
            drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
        }
        return drawable;
    }

    /**
     * Update the favicon color used in normal mode (non-incognito) with latest color setting.
     * Return the same {@link TabFavicon} with updated color in its drawable(s).
     *
     * <p>TODO(crbug.com/40781763): Avoid creating color filter every time.
     */
    private TabFavicon colorFaviconWithTheme(TabFavicon favicon) {
        assert favicon.isRecolorAllowed();

        int colorDefault = TabUiThemeUtils.getChromeOwnedFaviconTintColor(mContext, false, false);
        favicon.getDefaultDrawable()
                .setColorFilter(new PorterDuffColorFilter(colorDefault, PorterDuff.Mode.SRC_IN));

        if (favicon.hasSelectedState()) {
            int colorSelected =
                    TabUiThemeUtils.getChromeOwnedFaviconTintColor(mContext, false, true);
            favicon.getSelectedDrawable()
                    .setColorFilter(
                            new PorterDuffColorFilter(colorSelected, PorterDuff.Mode.SRC_IN));
        }

        return favicon;
    }
}