chromium/chrome/android/java/src/org/chromium/chrome/browser/tab/TabFavicon.java

// Copyright 2019 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;

import android.content.res.Resources;
import android.graphics.Bitmap;

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

import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;

import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.chrome.R;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;

/** Fetches a favicon for active WebContents in a Tab. */
public class TabFavicon extends TabWebContentsUserData {
    private static final Class<TabFavicon> USER_DATA_KEY = TabFavicon.class;

    private final TabImpl mTab;
    private final long mNativeTabFavicon;

    /**
     * The size in pixels at which favicons will be drawn. Ideally mFavicon will have this size to
     * avoid scaling artifacts.
     */
    private final int mIdealFaviconSize;

    // The ideal favicon size for navigation transitions, in DIP.
    private final int mNavigationTransitionsIdealFaviconSize;
    // The current favicon width and height for navigation transitions.
    private int mNavigationTransitionsFaviconWidth;
    private int mNavigationTransitionsFaviconHeight;
    // The URL of the tab when the favicon was fetch for navigation transitions.
    private GURL mFaviconTabUrlForNavigationTransition;

    private Bitmap mFavicon;
    private int mFaviconWidth;
    private int mFaviconHeight;
    // The URL of the tab when mFavicon was fetched.
    private GURL mFaviconTabUrl;

    static TabFavicon from(Tab tab) {
        TabFavicon favicon = get(tab);
        if (favicon == null) {
            favicon = tab.getUserDataHost().setUserData(USER_DATA_KEY, new TabFavicon(tab));
        }
        return favicon;
    }

    private static TabFavicon get(Tab tab) {
        if (tab == null || !tab.isInitialized()) return null;
        return tab.getUserDataHost().getUserData(USER_DATA_KEY);
    }

    /**
     * @param tab Tab containing the web contents's favicon.
     * @return {@link Bitmap} of the favicon.
     */
    public static @Nullable Bitmap getBitmap(Tab tab) {
        TabFavicon tabFavicon = get(tab);
        return tabFavicon != null ? tabFavicon.getFavicon() : null;
    }

    private TabFavicon(Tab tab) {
        super(tab);
        mTab = (TabImpl) tab;
        Resources resources = mTab.getThemedApplicationContext().getResources();
        mIdealFaviconSize = resources.getDimensionPixelSize(R.dimen.default_favicon_size);
        mNavigationTransitionsIdealFaviconSize =
                resources.getDimensionPixelSize(R.dimen.navigation_transitions_favicon_size);
        mNativeTabFavicon =
                TabFaviconJni.get().init(TabFavicon.this, mNavigationTransitionsIdealFaviconSize);
    }

    @Override
    public void initWebContents(WebContents webContents) {
        TabFaviconJni.get().setWebContents(mNativeTabFavicon, TabFavicon.this, webContents);
    }

    @Override
    public void cleanupWebContents(WebContents webContents) {
        TabFaviconJni.get().resetWebContents(mNativeTabFavicon, TabFavicon.this);
    }

    @Override
    public void destroyInternal() {
        TabFaviconJni.get().onDestroyed(mNativeTabFavicon, TabFavicon.this);
    }

    /**
     * @return The bitmap of the favicon scaled to 16x16dp. null if no favicon
     *         is specified or it requires the default favicon.
     */
    private Bitmap getFavicon() {
        // If we have no content or a native page, return null.
        if (mTab.isNativePage() || mTab.getWebContents() == null) return null;

        // Use the cached favicon only if the page wasn't changed.
        if (mFavicon != null && mFaviconTabUrl != null && mFaviconTabUrl.equals(mTab.getUrl())) {
            return mFavicon;
        }

        return TabFaviconJni.get().getFavicon(mNativeTabFavicon, TabFavicon.this);
    }

    /**
     * @param currentWidth current favicon's width.
     * @param currentHeight current favicon's height.
     * @param width new favicon's width.
     * @param height new favicon's height.
     * @param idealFaviconSize the size of the ideal favicon (a square favicon).
     * @return true iff the new favicon should replace the current one.
     */
    private static boolean isBetterFavicon(
            int currentWidth, int currentHeight, int width, int height, int idealFaviconSize) {
        assert width >= 0 && height >= 0;

        if (isIdealFaviconSize(idealFaviconSize, width, height)) return true;

        // The page may be dynamically updating its URL, let it through.
        if (currentWidth == width && currentHeight == height) return true;

        // Prefer square favicons over rectangular ones
        if (currentWidth != currentHeight && width == height) return true;
        if (currentWidth == currentHeight && width != height) return false;

        // Do not update favicon if it's already at least as big as the ideal size in both dimens
        if (currentWidth >= idealFaviconSize && currentHeight >= idealFaviconSize) return false;

        // Update favicon if the new one is larger in one dimen, but not smaller in the other
        return (width > currentWidth && !(height < currentHeight))
                || (!(width < currentWidth) && height > currentHeight);
    }

    private static boolean isIdealFaviconSize(int idealFaviconSize, int width, int height) {
        return width == idealFaviconSize && height == idealFaviconSize;
    }

    private boolean pageUrlChanged() {
        GURL currentTabUrl = mTab.getUrl();
        return !currentTabUrl.equals(mFaviconTabUrl);
    }

    private boolean pageUrlChangedForNavigationTransitions() {
        GURL currentTabUrl = mTab.getUrl();
        return !currentTabUrl.equals(mFaviconTabUrlForNavigationTransition);
    }

    @CalledByNative
    @VisibleForTesting
    void onFaviconAvailable(Bitmap icon, GURL iconUrl) {
        assert icon != null;
        // Bitmap#createScaledBitmap will return the original bitmap if it is already
        // |mIdealFaviconSize|x|mIdealFaviconSize| DP.
        mFavicon = Bitmap.createScaledBitmap(icon, mIdealFaviconSize, mIdealFaviconSize, true);
        mFaviconWidth = icon.getWidth();
        mFaviconHeight = icon.getHeight();
        mFaviconTabUrl = mTab.getUrl();
        RewindableIterator<TabObserver> observers = mTab.getTabObservers();
        while (observers.hasNext()) observers.next().onFaviconUpdated(mTab, icon, iconUrl);
    }

    @CalledByNative
    @VisibleForTesting
    boolean shouldUpdateFaviconForBrowserUI(int newIconWidth, int newIconHeight) {
        return pageUrlChanged()
                || isBetterFavicon(
                        mFaviconWidth,
                        mFaviconHeight,
                        newIconWidth,
                        newIconHeight,
                        mIdealFaviconSize);
    }

    @CalledByNative
    private boolean shouldUpdateFaviconForNavigationTransitions(
            int newIconWidth, int newIconHeight) {
        boolean shouldUpdate =
                pageUrlChangedForNavigationTransitions()
                        || isBetterFavicon(
                                mNavigationTransitionsFaviconWidth,
                                mNavigationTransitionsFaviconHeight,
                                newIconWidth,
                                newIconHeight,
                                mNavigationTransitionsIdealFaviconSize);
        if (shouldUpdate) {
            mNavigationTransitionsFaviconWidth = newIconWidth;
            mNavigationTransitionsFaviconHeight = newIconHeight;
            mFaviconTabUrlForNavigationTransition = mTab.getUrl();
        }
        return shouldUpdate;
    }

    @NativeMethods
    interface Natives {
        long init(TabFavicon caller, int navigaionTransitionFaviconSize);

        void onDestroyed(long nativeTabFavicon, TabFavicon caller);

        void setWebContents(long nativeTabFavicon, TabFavicon caller, WebContents webContents);

        void resetWebContents(long nativeTabFavicon, TabFavicon caller);

        Bitmap getFavicon(long nativeTabFavicon, TabFavicon caller);
    }
}