Fix blank thumbnail in GTS with Tab-to-GTS animation off properly

When TabToGTSAnimation flag is off, the thumbnails are not showing.

http://crrev.com/c/1650334 was a partial fix before
http://crrev.com/c/1637979.

This CL fixes it after http://crrev.com/c/1637979 and
http://crrev.com/c/1643070.

Bug: 972301
Change-Id: I5a64e6e40495f41f0b4ad4a38c9567a96c6b8504
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1650508
Reviewed-by: Matthew Jones <mdjones@chromium.org>
Reviewed-by: Yusuf Ozuysal <yusufo@chromium.org>
Commit-Queue: Wei-Yin Chen (陳威尹) <wychen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#670574}
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
index f52d328..f0efccf 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
@@ -214,7 +214,7 @@
         if (mSoftCleanupDelayMsForTesting != null) return mSoftCleanupDelayMsForTesting;
 
         String delay = ChromeFeatureList.getFieldTrialParamByFeature(
-                ChromeFeatureList.TAB_TO_GTS_ANIMATION, SOFT_CLEANUP_DELAY_PARAM);
+                ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, SOFT_CLEANUP_DELAY_PARAM);
         try {
             return Integer.valueOf(delay);
         } catch (NumberFormatException e) {
@@ -226,7 +226,7 @@
         if (mCleanupDelayMsForTesting != null) return mCleanupDelayMsForTesting;
 
         String delay = ChromeFeatureList.getFieldTrialParamByFeature(
-                ChromeFeatureList.TAB_TO_GTS_ANIMATION, CLEANUP_DELAY_PARAM);
+                ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, CLEANUP_DELAY_PARAM);
         try {
             return Integer.valueOf(delay);
         } catch (NumberFormatException e) {
@@ -273,8 +273,12 @@
     boolean prepareOverview() {
         mHandler.removeCallbacks(mSoftClearTabListRunnable);
         mHandler.removeCallbacks(mClearTabListRunnable);
-        boolean quick = mResetHandler.resetWithTabList(
-                mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), false);
+        boolean quick = false;
+        if (ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)) {
+            quick = mResetHandler.resetWithTabList(
+                    mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(),
+                    false);
+        }
         int initialPosition = Math.max(
                 mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter().index()
                         - INITIAL_SCROLL_INDEX_OFFSET,
@@ -286,7 +290,8 @@
     @Override
     public void showOverview(boolean animate) {
         mResetHandler.resetWithTabList(
-                mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), true);
+                mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(),
+                ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION));
         if (!animate) mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false);
         setVisibility(true);
         mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true);
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/MultiThumbnailCardProvider.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/MultiThumbnailCardProvider.java
index 05399bf..dfc38ca 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/MultiThumbnailCardProvider.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/MultiThumbnailCardProvider.java
@@ -54,6 +54,7 @@
         private final Tab mInitialTab;
         private final Callback<Bitmap> mFinalCallback;
         private final boolean mForceUpdate;
+        private final boolean mWriteToCache;
         private final List<Tab> mTabs = new ArrayList<>(4);
         private final AtomicInteger mThumbnailsToFetch = new AtomicInteger();
 
@@ -61,10 +62,15 @@
         private Bitmap mMultiThumbnailBitmap;
         private String mText;
 
-        MultiThumbnailFetcher(Tab initialTab, Callback<Bitmap> finalCallback, boolean forceUpdate) {
+        /**
+         * @see TabContentManager#getTabThumbnailWithCallback
+         */
+        MultiThumbnailFetcher(Tab initialTab, Callback<Bitmap> finalCallback, boolean forceUpdate,
+                boolean writeToCache) {
             mFinalCallback = finalCallback;
             mInitialTab = initialTab;
             mForceUpdate = forceUpdate;
+            mWriteToCache = writeToCache;
         }
 
         private void initializeAndStartFetching(Tab tab) {
@@ -123,7 +129,7 @@
                                         drawFaviconThenMaybeSendBack(favicon, index);
                                     });
                         }
-                    }, mForceUpdate && i == 0);
+                    }, mForceUpdate && i == 0, mWriteToCache && i == 0);
                 } else {
                     drawThumbnailBitmapOnCanvasWithFrame(null, i);
                     if (mText != null && i == 3) {
@@ -262,16 +268,17 @@
 
     @Override
     public void getTabThumbnailWithCallback(
-            Tab tab, Callback<Bitmap> finalCallback, boolean forceUpdate) {
+            Tab tab, Callback<Bitmap> finalCallback, boolean forceUpdate, boolean writeToCache) {
         if (mTabModelSelector.getTabModelFilterProvider()
                         .getCurrentTabModelFilter()
                         .getRelatedTabList(tab.getId())
                         .size()
                 == 1) {
-            mTabContentManager.getTabThumbnailWithCallback(tab, finalCallback, forceUpdate);
+            mTabContentManager.getTabThumbnailWithCallback(
+                    tab, finalCallback, forceUpdate, writeToCache);
             return;
         }
 
-        new MultiThumbnailFetcher(tab, finalCallback, forceUpdate).fetch();
+        new MultiThumbnailFetcher(tab, finalCallback, forceUpdate, writeToCache).fetch();
     }
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
index 1517b5c..6d6295a 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridViewBinder.java
@@ -59,8 +59,6 @@
                 holder.itemView.setForeground(
                         item.get(TabProperties.IS_SELECTED) ? drawable : null);
             }
-        } else if (TabProperties.IS_HIDDEN == propertyKey) {
-            updateThumbnail(holder, item);
         } else if (TabProperties.FAVICON == propertyKey) {
             holder.favicon.setImageDrawable(item.get(TabProperties.FAVICON));
         } else if (TabProperties.THUMBNAIL_FETCHER == propertyKey) {
@@ -152,14 +150,12 @@
     }
 
     private static void updateThumbnail(TabGridViewHolder holder, PropertyModel item) {
-        if (item.get(TabProperties.IS_HIDDEN)) {
+        TabListMediator.ThumbnailFetcher fetcher = item.get(TabProperties.THUMBNAIL_FETCHER);
+        if (fetcher == null) {
             // Release the thumbnail to save memory.
             holder.resetThumbnail();
             return;
         }
-
-        TabListMediator.ThumbnailFetcher fetcher = item.get(TabProperties.THUMBNAIL_FETCHER);
-        if (fetcher == null) return;
         Callback<Bitmap> callback = result -> {
             if (result == null) {
                 holder.resetThumbnail();
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
index e9aa6600..edc17b3 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
@@ -21,6 +21,8 @@
 import org.chromium.base.Log;
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.chrome.browser.ChromeFeatureList;
+import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
 import org.chromium.chrome.browser.native_page.NativePageFactory;
 import org.chromium.chrome.browser.tab.EmptyTabObserver;
 import org.chromium.chrome.browser.tab.Tab;
@@ -60,7 +62,11 @@
      * An interface to get the thumbnails to be shown inside the tab grid cards.
      */
     public interface ThumbnailProvider {
-        void getTabThumbnailWithCallback(Tab tab, Callback<Bitmap> callback, boolean forceUpdate);
+        /**
+         * @see TabContentManager#getTabThumbnailWithCallback
+         */
+        void getTabThumbnailWithCallback(
+                Tab tab, Callback<Bitmap> callback, boolean forceUpdate, boolean writeToCache);
     }
 
     /**
@@ -96,15 +102,19 @@
         private ThumbnailProvider mThumbnailProvider;
         private Tab mTab;
         private boolean mForceUpdate;
+        private boolean mWriteToCache;
 
-        ThumbnailFetcher(ThumbnailProvider provider, Tab tab, boolean forceUpdate) {
+        ThumbnailFetcher(
+                ThumbnailProvider provider, Tab tab, boolean forceUpdate, boolean writeToCache) {
             mThumbnailProvider = provider;
             mTab = tab;
             mForceUpdate = forceUpdate;
+            mWriteToCache = writeToCache;
         }
 
         void fetch(Callback<Bitmap> callback) {
-            mThumbnailProvider.getTabThumbnailWithCallback(mTab, callback, mForceUpdate);
+            mThumbnailProvider.getTabThumbnailWithCallback(
+                    mTab, callback, mForceUpdate, mWriteToCache);
         }
     }
 
@@ -661,7 +671,7 @@
      */
     void softCleanup() {
         for (int i = 0; i < mModel.size(); i++) {
-            mModel.get(i).set(TabProperties.IS_HIDDEN, true);
+            mModel.get(i).set(TabProperties.THUMBNAIL_FETCHER, null);
         }
     }
 
@@ -670,7 +680,10 @@
         if (index < 0 || index >= mModel.size()) return;
         if (isUpdatingId) {
             mModel.get(index).set(TabProperties.TAB_ID, tab.getId());
+        } else {
+            assert mModel.get(index).get(TabProperties.TAB_ID) == tab.getId();
         }
+
         TabActionListener tabSelectedListener;
         if (mGridCardOnClickListenerProvider == null
                 || getRelatedTabsForId(tab.getId()).size() == 1) {
@@ -683,7 +696,7 @@
                 TabProperties.CREATE_GROUP_LISTENER, getCreateGroupButtonListener(tab, isSelected));
         mModel.get(index).set(TabProperties.IS_SELECTED, isSelected);
         mModel.get(index).set(TabProperties.TITLE, mTitleProvider.getTitle(tab));
-        mModel.get(index).set(TabProperties.IS_HIDDEN, false);
+
         Callback<Drawable> faviconCallback = drawable -> {
             int modelIndex = mModel.indexFromId(tab.getId());
             if (modelIndex != Tab.INVALID_TAB_ID && drawable != null) {
@@ -693,8 +706,14 @@
 
         mTabListFaviconProvider.getFaviconForUrlAsync(
                 tab.getUrl(), tab.isIncognito(), faviconCallback);
-        if (mThumbnailProvider != null && !quickMode && (isSelected || isUpdatingId)) {
-            ThumbnailFetcher callback = new ThumbnailFetcher(mThumbnailProvider, tab, isSelected);
+        if (mThumbnailProvider != null
+                && (mModel.get(index).get(TabProperties.THUMBNAIL_FETCHER) == null || isSelected
+                        || isUpdatingId)) {
+            boolean forceUpdate = isSelected && !quickMode;
+            ThumbnailFetcher callback = new ThumbnailFetcher(mThumbnailProvider, tab, forceUpdate,
+                    forceUpdate
+                            && !ChromeFeatureList.isEnabled(
+                                    ChromeFeatureList.TAB_TO_GTS_ANIMATION));
             mModel.get(index).set(TabProperties.THUMBNAIL_FETCHER, callback);
         }
     }
@@ -773,7 +792,6 @@
                         .with(TabProperties.FAVICON,
                                 mTabListFaviconProvider.getDefaultFaviconDrawable())
                         .with(TabProperties.IS_SELECTED, isSelected)
-                        .with(TabProperties.IS_HIDDEN, false)
                         .with(TabProperties.IPH_PROVIDER, showIPH ? mIphProvider : null)
                         .with(TabProperties.TAB_SELECTED_LISTENER, tabSelectedListener)
                         .with(TabProperties.TAB_CLOSED_LISTENER, mTabClosedListener)
@@ -803,7 +821,10 @@
                 tab.getUrl(), tab.isIncognito(), faviconCallback);
 
         if (mThumbnailProvider != null) {
-            ThumbnailFetcher callback = new ThumbnailFetcher(mThumbnailProvider, tab, isSelected);
+            ThumbnailFetcher callback = new ThumbnailFetcher(mThumbnailProvider, tab, isSelected,
+                    isSelected
+                            && !ChromeFeatureList.isEnabled(
+                                    ChromeFeatureList.TAB_TO_GTS_ANIMATION));
             tabInfo.set(TabProperties.THUMBNAIL_FETCHER, callback);
         }
         tab.addObserver(mTabObserver);
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
index 82e4575..bbcc011 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabProperties.java
@@ -29,7 +29,7 @@
             new WritableObjectPropertyKey<>();
 
     public static final WritableObjectPropertyKey<TabListMediator.ThumbnailFetcher>
-            THUMBNAIL_FETCHER = new WritableObjectPropertyKey<>();
+            THUMBNAIL_FETCHER = new WritableObjectPropertyKey<>(true);
 
     public static final WritableObjectPropertyKey<TabListMediator.IphProvider> IPH_PROVIDER =
             new WritableObjectPropertyKey<>();
@@ -38,8 +38,6 @@
 
     public static final WritableBooleanPropertyKey IS_SELECTED = new WritableBooleanPropertyKey();
 
-    public static final WritableBooleanPropertyKey IS_HIDDEN = new WritableBooleanPropertyKey();
-
     public static final WritableObjectPropertyKey<TabListMediator.TabActionListener>
             CREATE_GROUP_LISTENER = new WritableObjectPropertyKey<>();
 
@@ -57,7 +55,7 @@
 
     public static final PropertyKey[] ALL_KEYS_TAB_GRID = new PropertyKey[] {TAB_ID,
             TAB_SELECTED_LISTENER, TAB_CLOSED_LISTENER, FAVICON, THUMBNAIL_FETCHER, IPH_PROVIDER,
-            TITLE, IS_SELECTED, IS_HIDDEN, CREATE_GROUP_LISTENER, ALPHA, CARD_ANIMATION_STATUS,
+            TITLE, IS_SELECTED, CREATE_GROUP_LISTENER, ALPHA, CARD_ANIMATION_STATUS,
             SELECTABLE_TAB_CLICKED_LISTENER, TAB_SELECTION_DELEGATE};
 
     public static final PropertyKey[] ALL_KEYS_TAB_STRIP = new PropertyKey[] {
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutPerfTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutPerfTest.java
index 3fcd18c..ce31ced 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutPerfTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutPerfTest.java
@@ -56,7 +56,7 @@
         "force-fieldtrials=Study/Group"})
 @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
 public class GridTabSwitcherLayoutPerfTest {
-    private static final String TAG = "GTSLayoutTest";
+    private static final String TAG = "GTSLayoutPerfTest";
 
     /** Flip this to {@code true} to run performance tests locally. */
     private static final boolean PERF_RUN = false;
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutTest.java
new file mode 100644
index 0000000..d3d8a38
--- /dev/null
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutTest.java
@@ -0,0 +1,362 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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.tasks.tab_management;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static org.chromium.base.test.util.CallbackHelper.WAIT_TIMEOUT_SECONDS;
+import static org.chromium.chrome.browser.tabmodel.TabSelectionType.FROM_USER;
+import static org.chromium.chrome.browser.util.UrlConstants.NTP_URL;
+import static org.chromium.content_public.browser.test.util.CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL;
+import static org.chromium.content_public.browser.test.util.CriteriaHelper.DEFAULT_POLLING_INTERVAL;
+
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.text.TextUtils;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Restriction;
+import org.chromium.chrome.browser.ChromeFeatureList;
+import org.chromium.chrome.browser.ChromeSwitches;
+import org.chromium.chrome.browser.compositor.layouts.Layout;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.util.FeatureUtilities;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+import org.chromium.chrome.test.util.ChromeTabUtils;
+import org.chromium.chrome.test.util.MenuUtils;
+import org.chromium.chrome.test.util.browser.Features;
+import org.chromium.content_public.browser.test.util.Criteria;
+import org.chromium.content_public.browser.test.util.CriteriaHelper;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.content_public.browser.test.util.WebContentsUtils;
+import org.chromium.net.test.EmbeddedTestServer;
+import org.chromium.ui.test.util.UiRestriction;
+
+/** Tests for the {@link GridTabSwitcherLayout} */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
+        "enable-features=" + ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID + "<Study",
+        "force-fieldtrials=Study/Group"})
+@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
+public class GridTabSwitcherLayoutTest {
+    private static final String TAG = "GTSLayoutTest";
+
+    @Rule
+    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
+
+    @Rule
+    public TestRule mProcessor = new Features.InstrumentationProcessor();
+
+    private GridTabSwitcherLayout mGtsLayout;
+    private String mUrl;
+    private int mRepeat;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        FeatureUtilities.setGridTabSwitcherEnabledForTesting(true);
+        EmbeddedTestServer testServer =
+                EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
+        mActivityTestRule.startMainActivityFromLauncher();
+
+        Layout layout = mActivityTestRule.getActivity().getLayoutManager().getOverviewLayout();
+        assertTrue(layout instanceof GridTabSwitcherLayout);
+        mGtsLayout = (GridTabSwitcherLayout) layout;
+        mUrl = testServer.getURL("/chrome/test/data/android/navigate/simple.html");
+        mRepeat = 3;
+    }
+
+    @Test
+    @MediumTest
+    @CommandLineFlags.
+    Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/0/cleanup-delay/0"})
+    public void testTabToGridFromLiveTab() throws InterruptedException {
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    // clang-format off
+    @Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+    @CommandLineFlags.
+            Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/0/cleanup-delay/0"})
+    public void testTabToGridFromLiveTabAnimation() throws InterruptedException {
+        // clang-format on
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    @CommandLineFlags.
+    Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/10000/cleanup-delay/10000"})
+    public void testTabToGridFromLiveTabWarm() throws InterruptedException {
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    // clang-format off
+    @Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+    @CommandLineFlags.
+    Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/10000/cleanup-delay/10000"})
+    public void testTabToGridFromLiveTabWarmAnimation() throws InterruptedException {
+        // clang-format on
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    @CommandLineFlags.
+    Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/0/cleanup-delay/10000"})
+    public void testTabToGridFromLiveTabSoft() throws InterruptedException {
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    // clang-format off
+    @Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+    @CommandLineFlags.
+            Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/0/cleanup-delay/10000"})
+    public void testTabToGridFromLiveTabSoftAnimation() throws InterruptedException {
+        // clang-format on
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(mUrl);
+    }
+
+    @Test
+    @MediumTest
+    @CommandLineFlags.
+    Add({"force-fieldtrial-params=Study.Group:soft-cleanup-delay/0/cleanup-delay/0"})
+    public void testTabToGridFromNtp() throws InterruptedException {
+        prepareTabs(2, NTP_URL);
+        testTabToGrid(NTP_URL);
+    }
+
+    /**
+     * Make Chrome have {@code numTabs} or Tabs with {@code url} loaded.
+     * @param url The URL to load. Skip loading when null, but the thumbnail for the NTP might not
+     *            be saved.
+     */
+    private void prepareTabs(int numTabs, @Nullable String url) throws InterruptedException {
+        assertTrue(numTabs >= 1);
+        assertEquals(1, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
+
+        if (url != null) mActivityTestRule.loadUrl(url);
+        for (int i = 0; i < numTabs - 1; i++) {
+            MenuUtils.invokeCustomMenuActionSync(InstrumentationRegistry.getInstrumentation(),
+                    mActivityTestRule.getActivity(), org.chromium.chrome.R.id.new_tab_menu_id);
+            if (url != null) mActivityTestRule.loadUrl(url);
+        }
+        ChromeTabUtils.waitForTabPageLoaded(mActivityTestRule.getActivity().getActivityTab(), null,
+                null, WAIT_TIMEOUT_SECONDS * 10);
+        assertEquals(
+                numTabs, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
+    }
+
+    private void testTabToGrid(String fromUrl) throws InterruptedException {
+        mActivityTestRule.loadUrl(fromUrl);
+
+        int initCount = getCaptureCount();
+
+        GridTabSwitcher gts = mGtsLayout.getGridTabSwitcherForTesting();
+        for (int i = 0; i < mRepeat; i++) {
+            int count = getCaptureCount();
+            waitForCaptureRateControl();
+            TestThreadUtils.runOnUiThreadBlocking(
+                    () -> mActivityTestRule.getActivity().getLayoutManager().showOverview(true));
+            assertTrue(mActivityTestRule.getActivity().getLayoutManager().overviewVisible());
+
+            // Make sure the fading animation is done.
+            int delta;
+            if (TextUtils.equals(mActivityTestRule.getActivity()
+                                         .getCurrentWebContents()
+                                         .getLastCommittedUrl(),
+                        NTP_URL)) {
+                delta = 0;
+            } else {
+                delta = 1;
+                // TODO(wychen): refactor areAnimatorsEnabled() to a util class.
+                if (ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+                        && TabGridContainerViewBinderTest.areAnimatorsEnabled()) {
+                    delta += 1;
+                }
+            }
+            CriteriaHelper.pollUiThread(Criteria.equals(delta, () -> getCaptureCount() - count));
+
+            // clang-format off
+            TestThreadUtils.runOnUiThreadBlocking(
+                    () -> gts.getGridController().hideOverview(false));
+            CriteriaHelper.pollInstrumentationThread(
+                    () -> !mActivityTestRule.getActivity().getLayoutManager().overviewVisible(),
+                    "Overview not hidden yet", DEFAULT_MAX_TIME_TO_POLL * 10,
+                    DEFAULT_POLLING_INTERVAL);
+            // clang-format on
+        }
+        int expected;
+        if (TextUtils.equals(
+                    mActivityTestRule.getActivity().getCurrentWebContents().getLastCommittedUrl(),
+                    NTP_URL)) {
+            expected = 0;
+        } else {
+            expected = mRepeat;
+            if (ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+                    && TabGridContainerViewBinderTest.areAnimatorsEnabled()) {
+                expected += mRepeat;
+            }
+        }
+        Assert.assertEquals(expected, getCaptureCount() - initCount);
+    }
+
+    @Test
+    @MediumTest
+    public void testGridToTabToCurrentNTP() throws InterruptedException {
+        prepareTabs(1, NTP_URL);
+        testGridToTab(false, false);
+    }
+
+    @Test
+    @MediumTest
+    public void testGridToTabToOtherNTP() throws InterruptedException {
+        prepareTabs(2, NTP_URL);
+        testGridToTab(true, false);
+    }
+
+    @Test
+    @MediumTest
+    public void testGridToTabToCurrentLive() throws InterruptedException {
+        prepareTabs(1, mUrl);
+        testGridToTab(false, false);
+    }
+
+    @Test
+    @MediumTest
+    @Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+    public void testGridToTabToCurrentLiveWithAnimation() throws InterruptedException {
+        prepareTabs(1, mUrl);
+        testGridToTab(false, false);
+    }
+
+    @Test
+    @MediumTest
+    public void testGridToTabToOtherLive() throws InterruptedException {
+        prepareTabs(2, mUrl);
+        testGridToTab(true, false);
+    }
+
+    @Test
+    @MediumTest
+    @Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+    public void testGridToTabToOtherLiveWithAnimation() throws InterruptedException {
+        prepareTabs(2, mUrl);
+        testGridToTab(true, false);
+    }
+
+    @Test
+    @MediumTest
+    public void testGridToTabToOtherFrozen() throws InterruptedException {
+        prepareTabs(2, mUrl);
+        testGridToTab(true, true);
+    }
+
+    private void testGridToTab(boolean switchToAnotherTab, boolean killBeforeSwitching)
+            throws InterruptedException {
+        final int initCount = getCaptureCount();
+
+        for (int i = 0; i < mRepeat; i++) {
+            int count = getCaptureCount();
+            waitForCaptureRateControl();
+            TestThreadUtils.runOnUiThreadBlocking(
+                    () -> mActivityTestRule.getActivity().getLayoutManager().showOverview(true));
+            assertTrue(mActivityTestRule.getActivity().getLayoutManager().overviewVisible());
+            int delta;
+            if (TextUtils.equals(mActivityTestRule.getActivity()
+                                         .getCurrentWebContents()
+                                         .getLastCommittedUrl(),
+                        NTP_URL)) {
+                delta = 0;
+            } else {
+                delta = 1;
+                if (ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+                        && TabGridContainerViewBinderTest.areAnimatorsEnabled()) {
+                    delta += 1;
+                }
+            }
+            CriteriaHelper.pollUiThread(Criteria.equals(delta, () -> getCaptureCount() - count));
+
+            int index = mActivityTestRule.getActivity().getCurrentTabModel().index();
+            final int targetIndex = switchToAnotherTab ? 1 - index : index;
+            Tab targetTab =
+                    mActivityTestRule.getActivity().getCurrentTabModel().getTabAt(targetIndex);
+            if (killBeforeSwitching) {
+                WebContentsUtils.simulateRendererKilled(targetTab.getWebContents(), false);
+            }
+
+            if (switchToAnotherTab) {
+                waitForCaptureRateControl();
+            }
+            int count2 = getCaptureCount();
+            // clang-format off
+            TestThreadUtils.runOnUiThreadBlocking(
+                    () -> mActivityTestRule.getActivity().getCurrentTabModel().setIndex(
+                            targetIndex, FROM_USER));
+            CriteriaHelper.pollInstrumentationThread(
+                    () -> !mActivityTestRule.getActivity().getLayoutManager().overviewVisible(),
+                    "Overview not hidden yet");
+            // clang-format on
+            if (switchToAnotherTab
+                    && !TextUtils.equals(mActivityTestRule.getActivity()
+                                                 .getCurrentWebContents()
+                                                 .getLastCommittedUrl(),
+                            NTP_URL)) {
+                delta = 1;
+            } else {
+                delta = 0;
+            }
+            CriteriaHelper.pollUiThread(Criteria.equals(delta, () -> getCaptureCount() - count2));
+        }
+        int expected;
+        if (TextUtils.equals(
+                    mActivityTestRule.getActivity().getCurrentWebContents().getLastCommittedUrl(),
+                    NTP_URL)) {
+            expected = 0;
+        } else {
+            expected = mRepeat;
+            if (ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
+                    && TabGridContainerViewBinderTest.areAnimatorsEnabled()) {
+                expected += mRepeat;
+            }
+            if (switchToAnotherTab) {
+                expected += mRepeat;
+            }
+        }
+        Assert.assertEquals(expected, getCaptureCount() - initCount);
+    }
+
+    private int getCaptureCount() {
+        return RecordHistogram.getHistogramTotalCountForTesting("Compositing.CopyFromSurfaceTime");
+    }
+
+    private void waitForCaptureRateControl() throws InterruptedException {
+        // Needs to wait for |kCaptureMinRequestTimeMs| in order to capture another one.
+        // TODO(wychen): mock |kCaptureMinRequestTimeMs| to 0 in tests?
+        Thread.sleep(2000);
+    }
+}
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
index 246484b..cc9a2f9 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java
@@ -59,15 +59,15 @@
     private TabListMediator.ThumbnailFetcher mMockThumbnailProvider =
             new TabListMediator.ThumbnailFetcher(new TabListMediator.ThumbnailProvider() {
                 @Override
-                public void getTabThumbnailWithCallback(
-                        Tab tab, Callback<Bitmap> callback, boolean forceUpdate) {
+                public void getTabThumbnailWithCallback(Tab tab, Callback<Bitmap> callback,
+                        boolean forceUpdate, boolean writeToCache) {
                     Bitmap bitmap = mShouldReturnBitmap
                             ? Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
                             : null;
                     callback.onResult(bitmap);
                     mThumbnailFetchedCount.incrementAndGet();
                 }
-            }, null, false);
+            }, null, false, false);
     private AtomicInteger mThumbnailFetchedCount = new AtomicInteger();
 
     private TabListMediator.TabActionListener mMockCloseListener =
@@ -218,7 +218,6 @@
         WeakReference<Bitmap> ref = new WeakReference<>(bitmap);
         bitmap = null;
 
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
         Assert.assertFalse(canBeGarbageCollected(ref));
 
         mShouldReturnBitmap = false;
@@ -238,7 +237,6 @@
         WeakReference<Bitmap> ref = new WeakReference<>(bitmap);
         bitmap = null;
 
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
         Assert.assertFalse(canBeGarbageCollected(ref));
 
         mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
@@ -276,7 +274,7 @@
 
         Assert.assertFalse(canBeGarbageCollected(ref));
 
-        mGridModel.set(TabProperties.IS_HIDDEN, true);
+        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
         Assert.assertTrue(canBeGarbageCollected(ref));
         Assert.assertNull(mTabGridViewHolder.thumbnail.getDrawable());
         Assert.assertEquals(1, mThumbnailFetchedCount.get());
@@ -287,16 +285,15 @@
     @UiThreadTest
     public void testHiddenThenShow() throws Exception {
         mShouldReturnBitmap = true;
-        mGridModel.set(TabProperties.IS_HIDDEN, false);
         mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
         assertThat(mTabGridViewHolder.thumbnail.getDrawable(), instanceOf(BitmapDrawable.class));
         Assert.assertEquals(1, mThumbnailFetchedCount.get());
 
-        mGridModel.set(TabProperties.IS_HIDDEN, true);
+        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
         Assert.assertNull(mTabGridViewHolder.thumbnail.getDrawable());
         Assert.assertEquals(1, mThumbnailFetchedCount.get());
 
-        mGridModel.set(TabProperties.IS_HIDDEN, false);
+        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
         assertThat(mTabGridViewHolder.thumbnail.getDrawable(), instanceOf(BitmapDrawable.class));
         Assert.assertEquals(2, mThumbnailFetchedCount.get());
     }
@@ -304,33 +301,6 @@
     @Test
     @MediumTest
     @UiThreadTest
-    public void testSkipFetchingWhenHidden() throws Exception {
-        mShouldReturnBitmap = true;
-        mGridModel.set(TabProperties.IS_HIDDEN, true);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        Assert.assertNull(mTabGridViewHolder.thumbnail.getDrawable());
-        Assert.assertEquals(0, mThumbnailFetchedCount.get());
-
-        mGridModel.set(TabProperties.IS_HIDDEN, false);
-        assertThat(mTabGridViewHolder.thumbnail.getDrawable(), instanceOf(BitmapDrawable.class));
-        Assert.assertEquals(1, mThumbnailFetchedCount.get());
-
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, null);
-        mGridModel.set(TabProperties.THUMBNAIL_FETCHER, mMockThumbnailProvider);
-        Assert.assertEquals(4, mThumbnailFetchedCount.get());
-    }
-
-    @Test
-    @MediumTest
-    @UiThreadTest
     public void testClickToSelect() throws Exception {
         mTabGridViewHolder.itemView.performClick();
         Assert.assertTrue(mSelectClicked.get());
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
index 0ba4fb8..15d7ffc 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
@@ -138,7 +138,7 @@
 
         doNothing()
                 .when(mTabContentManager)
-                .getTabThumbnailWithCallback(any(), any(), anyBoolean());
+                .getTabThumbnailWithCallback(any(), any(), anyBoolean(), anyBoolean());
         doReturn(mResources).when(mContext).getResources();
 
         doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
index 86477bf..e6143f2 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
@@ -73,6 +73,7 @@
  */
 @RunWith(LocalRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
+@Features.EnableFeatures(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
 public class TabListMediatorUnitTest {
     @Rule
     public TestRule mProcessor = new Features.JUnitProcessor();
@@ -148,7 +149,7 @@
 
         doNothing()
                 .when(mTabContentManager)
-                .getTabThumbnailWithCallback(any(), any(), anyBoolean());
+                .getTabThumbnailWithCallback(any(), any(), anyBoolean(), anyBoolean());
         doReturn(mTabModel).when(mTabModelSelector).getCurrentModel();
         doReturn(tabModelList).when(mTabModelSelector).getModels();
         doReturn(mTabModelFilterProvider).when(mTabModelSelector).getTabModelFilterProvider();
diff --git a/chrome/android/features/tab_ui/tab_management_java_sources.gni b/chrome/android/features/tab_ui/tab_management_java_sources.gni
index 938dcde..e9afb2a 100644
--- a/chrome/android/features/tab_ui/tab_management_java_sources.gni
+++ b/chrome/android/features/tab_ui/tab_management_java_sources.gni
@@ -12,6 +12,7 @@
 
 tab_management_test_java_sources = [
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutPerfTest.java",
+  "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherLayoutTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabGridContainerViewBinderTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabListViewHolderTest.java",
   "//chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TestRecyclerViewSimpleViewBinder.java",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/content/TabContentManager.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/content/TabContentManager.java
index be7c3fa..4e256b4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/content/TabContentManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/content/TabContentManager.java
@@ -12,6 +12,7 @@
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
 
@@ -265,23 +266,20 @@
     }
 
     /**
-     * Call to get a thumbnail for a given tab from disk through a {@link Callback}. If there is
+     * 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.
-     * Currently this reads a compressed file from disk and sends the Bitmap over the
-     * JNI boundary after decompressing. In its current form, should be used for experimental
-     * purposes only.
-     * TODO(yusufo): Change the plumbing so that at the least a {@link android.net.Uri} is sent
-     * over JNI of an uncompressed file on disk.
      * @param tab The tab to get the thumbnail for.
      * @param callback The callback to send the {@link Bitmap} with. Can be called up to twice when
-     *                 forceUpdate; otherwise always called exactly once.
+     *                 {@code forceUpdate}; otherwise always called exactly once.
      * @param forceUpdate Whether to obtain the thumbnail from the live content.
+     * @param writeBack When {@code forceUpdate}, whether to write the thumbnail to cache.
      */
-    public void getTabThumbnailWithCallback(
-            @NonNull Tab tab, @NonNull Callback<Bitmap> callback, boolean forceUpdate) {
+    public void getTabThumbnailWithCallback(@NonNull Tab tab, @NonNull Callback<Bitmap> callback,
+            boolean forceUpdate, boolean writeBack) {
         if (mNativeTabContentManager == 0 || !mSnapshotsEnabled) return;
 
         if (!forceUpdate) {
+            assert !writeBack : "writeBack is ignored if not forceUpdate";
             getTabThumbnailFromDisk(tab, callback);
             return;
         }
@@ -289,12 +287,13 @@
         // Reading thumbnail from disk is faster than taking screenshot from live Tab, so fetch
         // that first even if |forceUpdate|.
         getTabThumbnailFromDisk(tab, (diskBitmap) -> {
-            callback.onResult(diskBitmap);
-            captureDownsampledThumbnail(tab, (bitmap) -> {
-                // Null check to avoid having a Bitmap from nativeGetTabThumbnailWithCallback() but
+            if (diskBitmap != null) callback.onResult(diskBitmap);
+
+            captureThumbnail(tab, writeBack, (bitmap) -> {
+                // Null check to avoid having a Bitmap from getTabThumbnailFromDisk() but
                 // cleared here.
-                // If invalidation is not needed, captureDownsampledThumbnail() might not do
-                // anything and send back null.
+                // If invalidation is not needed, readbackNativeBitmap() might not do anything and
+                // send back null.
                 if (bitmap != null) {
                     callback.onResult(bitmap);
                 }
@@ -303,7 +302,7 @@
     }
 
     private void getTabThumbnailFromDisk(@NonNull Tab tab, @NonNull Callback<Bitmap> callback) {
-        // Try JPEG thumbnail first before using the more costly nativeGetTabThumbnailWithCallback.
+        // Try JPEG thumbnail first before using the more costly nativeGetEtc1TabThumbnail.
         new AsyncTask<Bitmap>() {
             @Override
             public Bitmap doInBackground() {
@@ -319,7 +318,7 @@
                     return;
                 }
                 if (mNativeTabContentManager == 0 || !mSnapshotsEnabled) return;
-                nativeGetTabThumbnailWithCallback(mNativeTabContentManager, tab.getId(), callback);
+                nativeGetEtc1TabThumbnail(mNativeTabContentManager, tab.getId(), callback);
             }
         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
     }
@@ -331,12 +330,7 @@
     public void cacheTabThumbnail(@NonNull final Tab tab) {
         if (mNativeTabContentManager == 0 || !mSnapshotsEnabled) return;
 
-        if (tab.getNativePage() != null || isNativeViewShowing(tab)) {
-            cacheNativeTabThumbnail(tab);
-        } else {
-            if (tab.getWebContents() == null) return;
-            nativeCacheTab(mNativeTabContentManager, tab, mThumbnailScale);
-        }
+        captureThumbnail(tab, true, null);
     }
 
     private Bitmap cacheNativeTabThumbnail(final Tab tab) {
@@ -349,22 +343,27 @@
     }
 
     /**
-     * Capture the downsampled content of a tab as a thumbnail.
+     * Capture the content of a tab as a thumbnail.
      * @param tab The tab whose content we will capture.
+     * @param writeToCache Whether write the captured thumbnail to cache. If not, a downsampled
+     *                     thumbnail is captured instead.
      * @param callback The callback to send the {@link Bitmap} with.
      */
-    private void captureDownsampledThumbnail(final Tab tab, @NonNull Callback<Bitmap> callback) {
-        if (mNativeTabContentManager == 0 || !mSnapshotsEnabled) return;
+    private void captureThumbnail(
+            @NonNull final Tab tab, boolean writeToCache, @Nullable Callback<Bitmap> callback) {
+        assert mNativeTabContentManager != 0;
+        assert mSnapshotsEnabled;
 
-        final float downsamplingScale = 0.5f;
         if (tab.getNativePage() != null || isNativeViewShowing(tab)) {
+            final float downsamplingScale = 0.5f;
             // If we use readbackNativeBitmap() with a downsampled scale and not saving it through
             // nativeCacheTabWithBitmap(), the logic of InvalidationAwareThumbnailProvider
-            // might prevent cacheTabThumbnail() from getting the latest thumbnail.
+            // 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) {
                 callback.onResult(null);
                 return;
@@ -379,8 +378,12 @@
             callback.onResult(resized);
         } else {
             if (tab.getWebContents() == null) return;
-            nativeCaptureThumbnail(
-                    mNativeTabContentManager, tab, mThumbnailScale * downsamplingScale, callback);
+            // If we don't have to write the thumbnail back to the cache, we can use the faster
+            // path of capturing a downsampled copy.
+            // This faster path is essential to Tab-to-Grid animation to be smooth.
+            final float downsamplingScale = writeToCache ? 1 : 0.5f;
+            nativeCaptureThumbnail(mNativeTabContentManager, tab,
+                    mThumbnailScale * downsamplingScale, writeToCache, callback);
         }
     }
 
@@ -397,7 +400,7 @@
 
     /**
      * Invalidate a thumbnail of the tab whose id is |id|.
-     * @param tabId The id of the {@link Tab} thumbnail to check.
+     * @param id The id of the {@link Tab} thumbnail to check.
      * @param url   The current URL of the {@link Tab}.
      */
     public void invalidateTabThumbnail(int id, String url) {
@@ -454,9 +457,7 @@
     private native void nativeDetachTab(long nativeTabContentManager, Tab tab, int tabId);
     private native boolean nativeHasFullCachedThumbnail(long nativeTabContentManager, int tabId);
     private native void nativeCaptureThumbnail(long nativeTabContentManager, Object tab,
-            float thumbnailScale, Callback<Bitmap> callback);
-    private native void nativeCacheTab(
-            long nativeTabContentManager, Object tab, float thumbnailScale);
+            float thumbnailScale, boolean writeToCache, Callback<Bitmap> callback);
     private native void nativeCacheTabWithBitmap(long nativeTabContentManager, Object tab,
             Object bitmap, float thumbnailScale);
     private native void nativeInvalidateIfChanged(long nativeTabContentManager, int tabId,
@@ -464,7 +465,7 @@
     private native void nativeUpdateVisibleIds(
             long nativeTabContentManager, int[] priority, int primaryTabId);
     private native void nativeRemoveTabThumbnail(long nativeTabContentManager, int tabId);
-    private native void nativeGetTabThumbnailWithCallback(
+    private native void nativeGetEtc1TabThumbnail(
             long nativeTabContentManager, int tabId, Callback<Bitmap> callback);
     private static native void nativeDestroy(long nativeTabContentManager);
 }
diff --git a/chrome/browser/android/compositor/tab_content_manager.cc b/chrome/browser/android/compositor/tab_content_manager.cc
index e9e0bc1..87cd5e4 100644
--- a/chrome/browser/android/compositor/tab_content_manager.cc
+++ b/chrome/browser/android/compositor/tab_content_manager.cc
@@ -253,43 +253,25 @@
     const JavaParamRef<jobject>& obj,
     const JavaParamRef<jobject>& tab,
     jfloat thumbnail_scale,
+    jboolean write_to_cache,
     const base::android::JavaParamRef<jobject>& j_callback) {
+  TabAndroid* tab_android = TabAndroid::GetNativeTab(env, tab);
+  DCHECK(tab_android);
+  const int tab_id = tab_android->GetAndroidId();
+
   content::RenderWidgetHostView* rwhv = GetRwhvForTab(env, obj, tab);
   if (!rwhv) {
     if (j_callback)
       RunObjectCallbackAndroid(j_callback, nullptr);
     return;
   }
-
-  TabAndroid* tab_android = TabAndroid::GetNativeTab(env, tab);
-  DCHECK(tab_android);
-  const int tab_id = tab_android->GetAndroidId();
-
+  if (write_to_cache && !thumbnail_cache_->CheckAndUpdateThumbnailMetaData(
+                            tab_id, tab_android->GetURL())) {
+    return;
+  }
   TabReadbackCallback readback_done_callback = base::BindOnce(
-      &TabContentManager::SendThumbnailToJava, weak_factory_.GetWeakPtr(),
-      tab_id, base::android::ScopedJavaGlobalRef<jobject>(j_callback));
-  pending_tab_readbacks_[tab_id] = std::make_unique<TabReadbackRequest>(
-      rwhv, thumbnail_scale, std::move(readback_done_callback));
-}
-
-void TabContentManager::CacheTab(JNIEnv* env,
-                                 const JavaParamRef<jobject>& obj,
-                                 const JavaParamRef<jobject>& tab,
-                                 jfloat thumbnail_scale) {
-  content::RenderWidgetHostView* rwhv = GetRwhvForTab(env, obj, tab);
-  if (!rwhv) {
-    return;
-  }
-  TabAndroid* tab_android = TabAndroid::GetNativeTab(env, tab);
-  DCHECK(tab_android);
-  const int tab_id = tab_android->GetAndroidId();
-  if (!thumbnail_cache_->CheckAndUpdateThumbnailMetaData(
-          tab_id, tab_android->GetURL())) {
-    return;
-  }
-  TabReadbackCallback readback_done_callback =
-      base::BindOnce(&TabContentManager::PutThumbnailIntoCache,
-                     weak_factory_.GetWeakPtr(), tab_id);
+      &TabContentManager::OnTabReadback, weak_factory_.GetWeakPtr(), tab_id,
+      base::android::ScopedJavaGlobalRef<jobject>(j_callback), write_to_cache);
   pending_tab_readbacks_[tab_id] = std::make_unique<TabReadbackRequest>(
       rwhv, thumbnail_scale, std::move(readback_done_callback));
 }
@@ -309,7 +291,7 @@
   skbitmap.setImmutable();
 
   if (thumbnail_cache_->CheckAndUpdateThumbnailMetaData(tab_id, url))
-    PutThumbnailIntoCache(tab_id, thumbnail_scale, skbitmap);
+    OnTabReadback(tab_id, nullptr, true, thumbnail_scale, skbitmap);
 }
 
 void TabContentManager::InvalidateIfChanged(JNIEnv* env,
@@ -349,7 +331,7 @@
   NativeRemoveTabThumbnail(tab_id);
 }
 
-void TabContentManager::GetTabThumbnailWithCallback(
+void TabContentManager::GetEtc1TabThumbnail(
     JNIEnv* env,
     const base::android::JavaParamRef<jobject>& obj,
     jint tab_id,
@@ -357,7 +339,7 @@
   thumbnail_cache_->DecompressThumbnailFromFile(
       tab_id,
       base::BindRepeating(
-          &TabContentManager::TabThumbnailAvailable, weak_factory_.GetWeakPtr(),
+          &TabContentManager::SendThumbnailToJava, weak_factory_.GetWeakPtr(),
           base::android::ScopedJavaGlobalRef<jobject>(j_callback),
           /* need_downsampling */ true));
 }
@@ -372,22 +354,10 @@
       env, weak_java_tab_content_manager_.get(env), tab_id);
 }
 
-void TabContentManager::PutThumbnailIntoCache(int tab_id,
-                                              float thumbnail_scale,
-                                              const SkBitmap& bitmap) {
-  TabReadbackRequestMap::iterator readback_iter =
-      pending_tab_readbacks_.find(tab_id);
-
-  if (readback_iter != pending_tab_readbacks_.end())
-    pending_tab_readbacks_.erase(tab_id);
-
-  if (thumbnail_scale > 0 && !bitmap.empty())
-    thumbnail_cache_->Put(tab_id, bitmap, thumbnail_scale);
-}
-
-void TabContentManager::SendThumbnailToJava(
+void TabContentManager::OnTabReadback(
     int tab_id,
     base::android::ScopedJavaGlobalRef<jobject> j_callback,
+    bool write_to_cache,
     float thumbnail_scale,
     const SkBitmap& bitmap) {
   TabReadbackRequestMap::iterator readback_iter =
@@ -397,13 +367,14 @@
     pending_tab_readbacks_.erase(tab_id);
 
   if (j_callback) {
-    TabThumbnailAvailable(j_callback, /*need_downsampling */ false, true,
-                          bitmap);
-    return;
+    SendThumbnailToJava(j_callback, write_to_cache, true, bitmap);
   }
+
+  if (write_to_cache && thumbnail_scale > 0 && !bitmap.empty())
+    thumbnail_cache_->Put(tab_id, bitmap, thumbnail_scale);
 }
 
-void TabContentManager::TabThumbnailAvailable(
+void TabContentManager::SendThumbnailToJava(
     base::android::ScopedJavaGlobalRef<jobject> j_callback,
     bool need_downsampling,
     bool result,
diff --git a/chrome/browser/android/compositor/tab_content_manager.h b/chrome/browser/android/compositor/tab_content_manager.h
index 396699e..f6a8895 100644
--- a/chrome/browser/android/compositor/tab_content_manager.h
+++ b/chrome/browser/android/compositor/tab_content_manager.h
@@ -86,11 +86,8 @@
                         const base::android::JavaParamRef<jobject>& obj,
                         const base::android::JavaParamRef<jobject>& tab,
                         jfloat thumbnail_scale,
+                        jboolean write_to_cache,
                         const base::android::JavaParamRef<jobject>& j_callback);
-  void CacheTab(JNIEnv* env,
-                const base::android::JavaParamRef<jobject>& obj,
-                const base::android::JavaParamRef<jobject>& tab,
-                jfloat thumbnail_scale);
   void CacheTabWithBitmap(JNIEnv* env,
                           const base::android::JavaParamRef<jobject>& obj,
                           const base::android::JavaParamRef<jobject>& tab,
@@ -109,7 +106,7 @@
                           const base::android::JavaParamRef<jobject>& obj,
                           jint tab_id);
   void OnUIResourcesWereEvicted();
-  void GetTabThumbnailWithCallback(
+  void GetEtc1TabThumbnail(
       JNIEnv* env,
       const base::android::JavaParamRef<jobject>& obj,
       jint tab_id,
@@ -131,16 +128,13 @@
       JNIEnv* env,
       const base::android::JavaParamRef<jobject>& obj,
       const base::android::JavaParamRef<jobject>& tab);
-  void PutThumbnailIntoCache(int tab_id,
-                             float thumbnail_scale,
-                             const SkBitmap& bitmap);
-  void SendThumbnailToJava(
-      int tab_id,
-      base::android::ScopedJavaGlobalRef<jobject> j_callback,
-      float thumbnail_scale,
-      const SkBitmap& bitmap);
+  void OnTabReadback(int tab_id,
+                     base::android::ScopedJavaGlobalRef<jobject> j_callback,
+                     bool write_to_cache,
+                     float thumbnail_scale,
+                     const SkBitmap& bitmap);
 
-  void TabThumbnailAvailable(
+  void SendThumbnailToJava(
       base::android::ScopedJavaGlobalRef<jobject> j_callback,
       bool need_downsampling,
       bool result,