chromium/chrome/browser/tab_ui/android/java/src/org/chromium/chrome/browser/tab_ui/TabContentManager.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.chrome.browser.tab_ui;

import static java.lang.Math.min;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.Size;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

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

import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.PathUtils;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabWindowManager;
import org.chromium.chrome.browser.ui.native_page.FrozenNativePage;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.display.DisplayAndroid;
import org.chromium.url.GURL;

import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * The TabContentManager is responsible for serving tab contents to the UI components. Contents
 * could be live or static thumbnails.
 */
@JNINamespace("android")
public class TabContentManager {
    private static final int WAIT_FOR_NATIVE_BACKOFF_MS = 50;
    private static final int WAIT_FOR_NATIVE_MAX_BACKOFF_ATTEMPTS = 2;

    // These are used for UMA logging, so append only. Please update the
    // GridTabSwitcherThumbnailFetchingResult enum in enums.xml if these change.
    @IntDef({
        ThumbnailFetchingResult.GOT_JPEG,
        ThumbnailFetchingResult.GOT_ETC1,
        ThumbnailFetchingResult.GOT_NOTHING,
        ThumbnailFetchingResult.GOT_DIFFERENT_ASPECT_RATIO_JPEG,
        ThumbnailFetchingResult.GOT_JPEG_ON_REFETCH
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ThumbnailFetchingResult {
        int GOT_JPEG = 0;
        int GOT_ETC1 = 1;
        int GOT_NOTHING = 2;
        int GOT_DIFFERENT_ASPECT_RATIO_JPEG = 3;
        int GOT_JPEG_ON_REFETCH = 4;
        int NUM_ENTRIES = 5;
    }

    // This is to accommodate for pixel rounding errors.
    public static final double PIXEL_TOLERANCE_PERCENT = 0.02;

    @VisibleForTesting
    public static final String UMA_THUMBNAIL_FETCHING_RESULT =
            "Android.GridTabSwitcher.ThumbnailFetchingResult";

    private float mThumbnailScale;

    /**
     * The limit on the number of fullsized or ETC1 compressed thumbnails in the in-memory cache.
     * If in future there is a need for more bitmaps to be visible on the screen at once this value
     * can be increased.
     */
    private int mFullResThumbnailsMaxSize;

    private final BrowserControlsStateProvider mBrowserControlsStateProvider;
    private long mNativeTabContentManager;

    private final ArrayList<ThumbnailChangeListener> mListeners = new ArrayList<>();

    private final boolean mSnapshotsEnabled;
    private final TabFinder mTabFinder;
    private final Context mContext;
    private final TabWindowManager mTabWindowManager;

    /** The Java interface for listening to thumbnail changes. */
    public interface ThumbnailChangeListener {
        /**
         * @param id The tab id.
         */
        void onThumbnailChange(int id);
    }

    /** The interface to get a {@link Tab} from a tab ID. */
    public interface TabFinder {
        Tab getTabById(int id);
    }

    /**
     * @param context               The context that this cache is created in.
     * @param resourceId            The resource that this value might be defined in.
     * @param commandLineSwitch     The switch for which we would like to extract value from.
     * @return the value of an integer resource.  If the value is overridden on the command line
     * with the given switch, return the override instead.
     */
    private static int getIntegerResourceWithOverride(
            Context context, int resourceId, String commandLineSwitch) {
        String switchCount = CommandLine.getInstance().getSwitchValue(commandLineSwitch);
        if (switchCount != null) {
            return Integer.parseInt(switchCount);
        }
        return context.getResources().getInteger(resourceId);
    }

    /**
     * @param context The context that this cache is created in.
     * @param browserControlsStateProvider The provider of offsets.
     * @param snapshotsEnabled When false, causes many operations to no-op to save resources.
     * @param tabFinder The helper function to get tab from an ID.
     */
    public TabContentManager(
            Context context,
            BrowserControlsStateProvider browserControlsStateProvider,
            boolean snapshotsEnabled,
            TabFinder tabFinder,
            TabWindowManager tabWindowManager) {
        mContext = context;
        mBrowserControlsStateProvider = browserControlsStateProvider;
        mTabFinder = tabFinder;
        mSnapshotsEnabled = snapshotsEnabled;
        mTabWindowManager = tabWindowManager;

        // Override the cache size on the command line with --thumbnails=100
        int defaultCacheSize =
                getIntegerResourceWithOverride(
                        mContext,
                        R.integer.default_thumbnail_cache_size,
                        ChromeSwitches.THUMBNAILS);

        mFullResThumbnailsMaxSize = defaultCacheSize;

        float thumbnailScale = 1.f;
        DisplayAndroid display = DisplayAndroid.getNonMultiDisplay(mContext);
        float deviceDensity = display.getDipScale();
        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext)) {
            // Scale all tablets to MDPI.
            thumbnailScale = 1.f / deviceDensity;
        } else {
            // For phones, reduce the amount of memory usage by capturing a lower-res thumbnail for
            // devices with resolution higher than HDPI (crbug.com/357740).
            if (deviceDensity > 1.5f) {
                thumbnailScale = 1.5f / deviceDensity;
            }
        }
        mThumbnailScale = thumbnailScale;
    }

    /** Called after native library is loaded. */
    public void initWithNative() {
        int compressionQueueMaxSize =
                mContext.getResources().getInteger(R.integer.default_compression_queue_size);
        int writeQueueMaxSize =
                mContext.getResources().getInteger(R.integer.default_write_queue_size);

        mNativeTabContentManager =
                TabContentManagerJni.get()
                        .init(
                                TabContentManager.this,
                                mFullResThumbnailsMaxSize,
                                compressionQueueMaxSize,
                                writeQueueMaxSize,
                                /* saveJpegThumbnails= */ !SysUtils.isLowEndDevice());
    }

    /** Destroy the native component. */
    public void destroy() {
        if (mNativeTabContentManager != 0) {
            TabContentManagerJni.get().destroy(mNativeTabContentManager);
            mNativeTabContentManager = 0;
        }
    }

    @CalledByNative
    private Tab getTabById(int tabId) {
        if (mTabFinder == null) return null;

        return mTabFinder.getTabById(tabId);
    }

    @CalledByNative
    private long getNativePtr() {
        return mNativeTabContentManager;
    }

    /**
     * Add a listener to thumbnail changes.
     * @param listener The listener of thumbnail change events.
     */
    public void addThumbnailChangeListener(ThumbnailChangeListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    /**
     * Remove a listener to thumbnail changes.
     * @param listener The listener of thumbnail change events.
     */
    public void removeThumbnailChangeListener(ThumbnailChangeListener listener) {
        mListeners.remove(listener);
    }

    private Bitmap readbackNativeBitmap(final Tab tab, float scale) {
        NativePage nativePage = tab.getNativePage();
        boolean isNativeViewShowing = isNativeViewShowing(tab);
        if (nativePage == null && !isNativeViewShowing) {
            return null;
        }

        View viewToDraw = null;
        if (isNativeViewShowing) {
            viewToDraw = tab.getView();
        } else if (!(nativePage instanceof FrozenNativePage)) {
            viewToDraw = nativePage.getView();
        }
        if (viewToDraw == null || viewToDraw.getWidth() == 0 || viewToDraw.getHeight() == 0) {
            return null;
        }

        if (nativePage != null && nativePage instanceof InvalidationAwareThumbnailProvider) {
            if (!((InvalidationAwareThumbnailProvider) nativePage).shouldCaptureThumbnail()) {
                return null;
            }
        }

        return readbackNativeView(viewToDraw, scale, nativePage);
    }

    private Bitmap readbackNativeView(View viewToDraw, float scale, NativePage nativePage) {
        Bitmap bitmap;
        float overlayTranslateY = mBrowserControlsStateProvider.getTopVisibleContentOffset();

        float leftMargin = 0.f;
        float topMargin = 0.f;
        if (viewToDraw.getLayoutParams() instanceof MarginLayoutParams) {
            MarginLayoutParams params = (MarginLayoutParams) viewToDraw.getLayoutParams();
            leftMargin = params.leftMargin;
            topMargin = params.topMargin;
        }

        int width = (int) ((viewToDraw.getMeasuredWidth() + leftMargin) * mThumbnailScale);
        int height =
                (int)
                        ((viewToDraw.getMeasuredHeight() + topMargin - overlayTranslateY)
                                * mThumbnailScale);
        if (width <= 0 || height <= 0) {
            return null;
        }

        try {
            bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        } catch (OutOfMemoryError ex) {
            return null;
        }

        Canvas c = new Canvas(bitmap);
        c.scale(scale, scale);
        c.translate(leftMargin, -overlayTranslateY + topMargin);
        if (nativePage != null && nativePage instanceof InvalidationAwareThumbnailProvider) {
            ((InvalidationAwareThumbnailProvider) nativePage).captureThumbnail(c);
        } else {
            viewToDraw.draw(c);
        }

        return bitmap;
    }

    /**
     * Call to get an ETC1 thumbnail for a given tab through a {@link Callback}. If there is
     * no up-to-date thumbnail on disk for the given tab, callback returns null.
     * @param tabId The ID of the tab to get the thumbnail for.
     * @param callback The callback to send the {@link Bitmap} with. Can be called up to twice when
     *                 {@code forceUpdate}; otherwise always called exactly once.
     */
    public void getEtc1TabThumbnailWithCallback(int tabId, @NonNull Callback<Bitmap> callback) {
        if (!mSnapshotsEnabled || mNativeTabContentManager == 0) {
            callback.onResult(null);
            return;
        }

        // Do not capture a JPEG here because we likely already created one when capturing. We just
        // want to fetch the ETC1 off of disk for a higher resolution image to use for animations.
        TabContentManagerJni.get()
                .getEtc1TabThumbnail(
                        mNativeTabContentManager, tabId, /* saveJpeg= */ false, callback);
    }

    /**
     * Call to get a thumbnail for a given tab through a {@link Callback}. If there is no up-to-date
     * thumbnail on disk for the given tab, callback returns null.
     *
     * @param tabId The ID of the tab to get the thumbnail for.
     * @param thumbnailSize Desired size of thumbnail received by callback.
     * @param callback The callback to send the {@link Bitmap} with.
     */
    public void getTabThumbnailWithCallback(
            @NonNull int tabId, @NonNull Size thumbnailSize, @NonNull Callback<Bitmap> callback) {
        if (!mSnapshotsEnabled) {
            callback.onResult(null);
            return;
        }

        getTabThumbnailFromDisk(tabId, thumbnailSize, callback);
    }

    /**
     * @param tab The {@link Tab} the thumbnail is for.
     * @return The file storing the thumbnail in ETC1 format of a certain {@link Tab}.
     */
    public static File getTabThumbnailFileEtc1(Tab tab) {
        return new File(PathUtils.getThumbnailCacheDirectory(), String.valueOf(tab.getId()));
    }

    /**
     * @param tabId The ID of the {@link Tab} the thumbnail is for.
     * @return The file storing the thumbnail in JPEG format of a certain {@link Tab}.
     */
    public static File getTabThumbnailFileJpeg(int tabId) {
        return new File(PathUtils.getThumbnailCacheDirectory(), tabId + ".jpeg");
    }

    @VisibleForTesting
    public static Bitmap getJpegForTab(int tabId, @NonNull Size thumbnailSize) {
        File file = getTabThumbnailFileJpeg(tabId);
        if (!file.isFile()) return null;
        if (thumbnailSize.getWidth() <= 0 || thumbnailSize.getHeight() <= 0) {
            return BitmapFactory.decodeFile(file.getPath());
        }
        return resizeJpeg(file.getPath(), thumbnailSize);
    }

    /**
     * See https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap.
     * @param path Path of jpeg file.
     * @param thumbnailSize Desired thumbnail size to resize to.
     * @return Resized bitmap.
     */
    private static Bitmap resizeJpeg(String path, Size thumbnailSize) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);

        // Raw height and width of image.
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > thumbnailSize.getHeight() || width > thumbnailSize.getWidth()) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= thumbnailSize.getHeight()
                    && (halfWidth / inSampleSize) >= thumbnailSize.getWidth()) {
                inSampleSize *= 2;
            }
        }
        options.inSampleSize = inSampleSize;

        // Decode bitmap with inSampleSize set.
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(path, options);
    }

    private void getTabThumbnailFromDisk(
            @NonNull int tabId, @NonNull Size thumbnailSize, @NonNull Callback<Bitmap> callback) {
        // Get the JPEG once it is ready if a capture is ongoing.
        if (mNativeTabContentManager != 0) {
            TraceEvent.startAsync("GetTabThumbnailFromDiskJpegAwait", tabId);
            fetchJpeg(
                    tabId,
                    thumbnailSize,
                    (bitmap) -> {
                        TraceEvent.finishAsync("GetTabThumbnailFromDiskJpegAwait", tabId);
                        callback.onResult(bitmap);
                    });
            return;
        }

        getJpegForTabWithRefetch(tabId, thumbnailSize, /* attempts= */ 0, callback);
    }

    /**
     * Read the JPEG in java and report back with refetch.
     * @param tabId The Tab ID to wait for a JPEG of.
     * @param thumbnailSize The size of thumbnail that will be shown.
     * @param attempts The number of pre-native refetch attempts.
     * @param callback The callback to execute once native has finished any pending JPEG capture
     *                 tasks for the tab.
     */
    private void getJpegForTabWithRefetch(
            int tabId,
            @NonNull Size thumbnailSize,
            int attempts,
            @NonNull Callback<Bitmap> callback) {
        // Try JPEG thumbnail with backoff while pre-native.
        TraceEvent.startAsync("GetTabThumbnailFromDisk", tabId);
        PostTask.postDelayedTask(
                TaskTraits.USER_VISIBLE_MAY_BLOCK,
                () -> {
                    Bitmap bitmap = getJpegForTab(tabId, thumbnailSize);
                    PostTask.postTask(
                            TaskTraits.UI_USER_VISIBLE,
                            () -> onBitmapRead(tabId, thumbnailSize, attempts, bitmap, callback));
                },
                attempts == 0 ? 0 : WAIT_FOR_NATIVE_BACKOFF_MS);
    }

    /**
     * Read the JPEG in java and report back without refetch.
     * @param tabId The Tab ID to wait for a JPEG of.
     * @param thumbnailSize The size of thumbnail that will be shown.
     * @param callback The callback to execute once native has finished any pending JPEG capture
     *                 tasks for the tab.
     */
    private void getJpegForTabNoRefetch(
            int tabId, @NonNull Size thumbnailSize, @NonNull Callback<Bitmap> callback) {
        PostTask.postTask(
                TaskTraits.USER_VISIBLE_MAY_BLOCK,
                () -> {
                    Bitmap bitmap = getJpegForTab(tabId, thumbnailSize);
                    PostTask.postTask(
                            TaskTraits.UI_USER_VISIBLE,
                            () -> {
                                if (bitmap == null) {
                                    recordThumbnailFetchingResult(
                                            ThumbnailFetchingResult.GOT_NOTHING);
                                } else {
                                    recordThumbnailFetchingResult(ThumbnailFetchingResult.GOT_JPEG);
                                }
                                callback.onResult(bitmap);
                            });
                });
    }

    /**
     * Wait for the JPEG in native by using the capture progress tracker. Once available execute the
     * callback.
     * @param tabId The Tab ID to wait for a JPEG of.
     * @param thumbnailSize The size of thumbnail that will be shown.
     * @param callback The callback to execute once native has finished any pending JPEG capture
     *                 tasks for the tab.
     */
    private void fetchJpeg(
            int tabId, @NonNull Size thumbnailSize, @NonNull Callback<Bitmap> callback) {
        if (!mSnapshotsEnabled) {
            callback.onResult(null);
            return;
        }

        // Wait for the JPEG in native to be ready. There are two possibilities.
        // 1. A capture is ongoing. Wait for it.
        // 2. A capture is not-ongoing. Proceed under the assumption a thumbnail exists, but if
        //    it is missing fallback to null.
        assert mNativeTabContentManager != 0;
        TabContentManagerJni.get()
                .waitForJpegTabThumbnail(
                        mNativeTabContentManager,
                        tabId,
                        (maybeAvailable) -> {
                            if (!maybeAvailable) {
                                recordThumbnailFetchingResult(ThumbnailFetchingResult.GOT_NOTHING);
                                callback.onResult(null);
                                return;
                            }
                            getJpegForTabNoRefetch(tabId, thumbnailSize, callback);
                        });
    }

    private void onBitmapRead(
            int tabId,
            @NonNull Size thumbnailSize,
            int attempts,
            Bitmap jpeg,
            @NonNull Callback<Bitmap> callback) {
        TraceEvent.finishAsync("GetTabThumbnailFromDisk", tabId);
        if (jpeg != null) {
            recordThumbnailFetchingResult(ThumbnailFetchingResult.GOT_JPEG);
            callback.onResult(jpeg);
            return;
        }
        if (!mSnapshotsEnabled) return;

        if (mNativeTabContentManager == 0) {
            // Retry to wait for native to load.
            if (attempts < WAIT_FOR_NATIVE_MAX_BACKOFF_ATTEMPTS) {
                getJpegForTabWithRefetch(tabId, thumbnailSize, attempts + 1, callback);
            }
            return;
        }

        // Native is ready, try one more time and wait for the thumbnail to be ready.
        fetchJpeg(tabId, thumbnailSize, callback);
    }

    private static void recordThumbnailFetchingResult(@ThumbnailFetchingResult int result) {
        RecordHistogram.recordEnumeratedHistogram(
                UMA_THUMBNAIL_FETCHING_RESULT, result, ThumbnailFetchingResult.NUM_ENTRIES);
    }

    /**
     * Cache the content of a tab as a thumbnail.
     * @param tab The tab whose content we will cache.
     */
    public void cacheTabThumbnail(@NonNull final Tab tab) {
        cacheTabThumbnailWithCallback(tab, /* returnBitmap= */ false, null);
    }

    /**
     * Cache the content of a tab as a thumbnail and call the {@code callback} when finished.
     * @param tab The tab whose content we will cache.
     * @param returnBitmap Whether to return a bitmap to the callback. Setting to false avoids an
     *                     expensive bitmap copy if not required.
     * @param callback Called when the caching is finished. The bitmap argument may be null if
     *                 unsuccessful or {@code returnBitmap} is false.
     */
    public void cacheTabThumbnailWithCallback(
            @NonNull final Tab tab, boolean returnBitmap, Callback<Bitmap> callback) {
        if (mNativeTabContentManager == 0 || !mSnapshotsEnabled) return;

        captureThumbnail(tab, returnBitmap, callback);
    }

    private Bitmap cacheNativeTabThumbnail(final Tab tab) {
        assert tab.getNativePage() != null || isNativeViewShowing(tab);

        Bitmap nativeBitmap = readbackNativeBitmap(tab, mThumbnailScale);
        if (nativeBitmap == null) return null;
        TabContentManagerJni.get()
                .cacheTabWithBitmap(mNativeTabContentManager, tab, nativeBitmap, mThumbnailScale);
        return nativeBitmap;
    }

    /**
     * Capture the content of a tab as a thumbnail.
     *
     * @param tab The tab whose content we will capture.
     * @param returnBitmap Whether to return a bitmap to the callback.
     * @param callback The callback to send the {@link Bitmap} with.
     */
    private void captureThumbnail(
            @NonNull final Tab tab, boolean returnBitmap, @Nullable Callback<Bitmap> callback) {
        assert mNativeTabContentManager != 0;
        assert mSnapshotsEnabled;

        if (tab.getNativePage() != null || isNativeViewShowing(tab)) {
            // If we use readbackNativeBitmap() with a downsampled scale and not saving it through
            // TabContentManagerJni.get().cacheTabWithBitmap(), the logic
            // of InvalidationAwareThumbnailProvider might prevent captureThumbnail() from getting
            // the latest thumbnail. Therefore, we have to also call cacheNativeTabThumbnail(), and
            // do the downsampling here ourselves. This is less efficient than capturing a
            // downsampled bitmap, but the performance here is not the bottleneck.
            Bitmap bitmap = cacheNativeTabThumbnail(tab);
            if (callback == null) return;
            if (bitmap == null || !returnBitmap) {
                callback.onResult(null);
                return;
            }

            final float downsamplingScale = 0.5f;
            Matrix matrix = new Matrix();
            matrix.setScale(downsamplingScale, downsamplingScale);
            Bitmap resized =
                    Bitmap.createBitmap(
                            bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            callback.onResult(resized);
        } else {
            if (tab.getWebContents() == null || tab.isHidden()) {
                if (callback != null) {
                    callback.onResult(null);
                }
                return;
            }
            TabContentManagerJni.get()
                    .captureThumbnail(
                            mNativeTabContentManager, tab, mThumbnailScale, returnBitmap, callback);
        }
    }

    /**
     * Invalidate a thumbnail if the content of the tab has been changed.
     * @param tabId The id of the {@link Tab} thumbnail to check.
     * @param url   The current URL of the {@link Tab}.
     */
    public void invalidateIfChanged(int tabId, GURL url) {
        if (mNativeTabContentManager != 0) {
            TabContentManagerJni.get().invalidateIfChanged(mNativeTabContentManager, tabId, url);
        }
    }

    /**
     * Update the priority-ordered list of visible tabs. This should only be called directly via the
     * active {@link Layout} to avoid invalidating visible tab IDs that are in use.
     *
     * @param priority The list of tab ids to load cached thumbnails for. Only the first {@link
     *     mFullResThumbnailsMaxSize} thumbnails will be loaded.
     * @param primaryTabId The id of the current tab this is not loaded under the assumption it will
     *     have a live layer. If this is not the case it should be the first tab in the priority
     *     list.
     */
    public void updateVisibleIds(List<Integer> priority, int primaryTabId) {
        if (mNativeTabContentManager == 0) return;

        int idsSize = min(mFullResThumbnailsMaxSize, priority.size());
        int[] priorityIds = new int[idsSize];
        for (int i = 0; i < idsSize; i++) {
            priorityIds[i] = priority.get(i);
        }

        TabContentManagerJni.get()
                .updateVisibleIds(mNativeTabContentManager, priorityIds, primaryTabId);
    }

    /**
     * Removes a thumbnail of the tab whose id is |tabId|.
     *
     * @param tabId The Id of the tab whose thumbnail is being removed.
     */
    public void removeTabThumbnail(int tabId) {
        if (!mTabWindowManager.canTabThumbnailBeDeleted(tabId)) return;

        if (mNativeTabContentManager != 0) {
            TabContentManagerJni.get().removeTabThumbnail(mNativeTabContentManager, tabId);
        }
    }

    public void setCaptureMinRequestTimeForTesting(int timeMs) {
        TabContentManagerJni.get()
                .setCaptureMinRequestTimeForTesting(mNativeTabContentManager, timeMs);
    }

    /** Returns whether a thumbnail capture for a tab is in flight for testing. */
    public boolean isTabCaptureInFlightForTesting(int tabId) {
        return TabContentManagerJni.get()
                .isTabCaptureInFlightForTesting(mNativeTabContentManager, tabId);
    }

    @CalledByNative
    protected void notifyListenersOfThumbnailChange(int tabId) {
        for (ThumbnailChangeListener listener : mListeners) {
            listener.onThumbnailChange(tabId);
        }
    }

    private boolean isNativeViewShowing(Tab tab) {
        return tab != null && tab.isShowingCustomView();
    }

    @NativeMethods
    interface Natives {
        // Class Object Methods
        long init(
                TabContentManager caller,
                int defaultCacheSize,
                int compressionQueueMaxSize,
                int writeQueueMaxSize,
                boolean saveJpegThumbnails);

        void captureThumbnail(
                long nativeTabContentManager,
                Object tab,
                float thumbnailScale,
                boolean returnBitmap,
                Callback<Bitmap> callback);

        void cacheTabWithBitmap(
                long nativeTabContentManager, Object tab, Object bitmap, float thumbnailScale);

        void invalidateIfChanged(long nativeTabContentManager, int tabId, GURL url);

        void updateVisibleIds(long nativeTabContentManager, int[] priority, int primaryTabId);

        void removeTabThumbnail(long nativeTabContentManager, int tabId);

        void waitForJpegTabThumbnail(
                long nativeTabContentManager, int tabId, Callback<Boolean> callback);

        void getEtc1TabThumbnail(
                long nativeTabContentManager,
                int tabId,
                boolean saveJpeg,
                Callback<Bitmap> callback);

        void setCaptureMinRequestTimeForTesting(long nativeTabContentManager, int timeMs);

        boolean isTabCaptureInFlightForTesting(long nativeTabContentManager, int tabId);

        void destroy(long nativeTabContentManager);
    }
}