Index: chrome/android/java_staging/src/org/chromium/chrome/browser/tab/ThumbnailTabHelper.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/ThumbnailTabHelper.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/ThumbnailTabHelper.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e483b1f67aeda4fa79980f0dcfa584ed32c1ef3e |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/ThumbnailTabHelper.java |
@@ -0,0 +1,335 @@ |
+// Copyright 2015 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.tab; |
+ |
+import android.content.res.Resources; |
+import android.graphics.Bitmap; |
+import android.graphics.Bitmap.Config; |
+import android.graphics.Canvas; |
+import android.graphics.Paint; |
+import android.graphics.Rect; |
+import android.os.Handler; |
+import android.text.TextUtils; |
+import android.util.Log; |
+ |
+import com.google.android.apps.chrome.R; |
+ |
+import org.chromium.chrome.browser.ChromeActivity; |
+import org.chromium.chrome.browser.EmptyTabObserver; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.chrome.browser.TabObserver; |
+import org.chromium.chrome.browser.UrlConstants; |
+import org.chromium.chrome.browser.profiles.Profile; |
+import org.chromium.chrome.browser.util.MathUtils; |
+import org.chromium.content.browser.ContentReadbackHandler; |
+import org.chromium.content.browser.ContentViewCore; |
+import org.chromium.content_public.browser.GestureStateListener; |
+import org.chromium.content_public.browser.WebContents; |
+import org.chromium.ui.base.WindowAndroid; |
+ |
+/** |
+ * Handles capturing most visited thumbnails for a tab. |
+ */ |
+public class ThumbnailTabHelper { |
+ |
+ private static final String TAG = "ThumbnailTabHelper"; |
+ |
+ /** The general motivation for this value is giving the scrollbar fadeout |
+ * animation sufficient time to finish before the capture executes. */ |
+ private static final int THUMBNAIL_CAPTURE_DELAY_MS = 350; |
+ |
+ private final Tab mTab; |
+ private final Handler mHandler; |
+ |
+ private final int mThumbnailWidth; |
+ private final int mThumbnailHeight; |
+ |
+ private ContentViewCore mContentViewCore; |
+ private boolean mThumbnailCapturedForLoad; |
+ private boolean mIsRenderViewHostReady; |
+ private boolean mWasRenderViewHostReady; |
+ |
+ private final Runnable mThumbnailRunnable = new Runnable() { |
+ @Override |
+ public void run() { |
+ // http://crbug.com/461506 : Do not get thumbnail unless render view host is ready. |
+ if (!mIsRenderViewHostReady) return; |
+ |
+ if (mThumbnailCapturedForLoad) return; |
+ // Prevent redundant thumbnail capture attempts. |
+ mThumbnailCapturedForLoad = true; |
+ if (!canUpdateHistoryThumbnail()) { |
+ // Allow a hidden tab to re-attempt capture in the future via |show()|. |
+ mThumbnailCapturedForLoad = !mTab.isHidden(); |
+ return; |
+ } |
+ if (!shouldUpdateThumbnail()) return; |
+ |
+ // Scale the image so we're not copying more than we need to (from |
+ // the GPU). |
+ int[] dim = new int[] { |
+ mTab.getWidth(), mTab.getHeight() |
+ }; |
+ MathUtils.scaleToFitTargetSize(dim, mThumbnailWidth, mThumbnailHeight); |
+ |
+ ContentReadbackHandler readbackHandler = getActivity().getContentReadbackHandler(); |
+ if (readbackHandler == null || mTab.getContentViewCore() == null) return; |
+ final String requestedUrl = mTab.getUrl(); |
+ ContentReadbackHandler.GetBitmapCallback bitmapCallback = |
+ new ContentReadbackHandler.GetBitmapCallback() { |
+ @Override |
+ public void onFinishGetBitmap(Bitmap bitmap, int response) { |
+ // Ensure that the URLs match for the requested page, and ensure |
+ // that the page is still valid for thumbnail capturing (i.e. |
+ // not showing an error page). |
+ if (bitmap == null |
+ || !TextUtils.equals(requestedUrl, mTab.getUrl()) |
+ || !mThumbnailCapturedForLoad |
+ || !canUpdateHistoryThumbnail()) { |
+ return; |
+ } |
+ updateHistoryThumbnail(bitmap); |
+ bitmap.recycle(); |
+ } |
+ }; |
+ readbackHandler.getContentBitmapAsync(1, new Rect(0, 0, dim[0], dim[1]), |
+ mTab.getContentViewCore(), Bitmap.Config.ARGB_8888, bitmapCallback); |
+ } |
+ }; |
+ |
+ private final TabObserver mTabObserver = new EmptyTabObserver() { |
+ @Override |
+ public void onContentChanged(Tab tab) { |
+ ThumbnailTabHelper.this.onContentChanged(); |
+ } |
+ |
+ @Override |
+ public void onCrash(Tab tab, boolean sadTabShown) { |
+ cancelThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onPageLoadStarted(Tab tab) { |
+ cancelThumbnailCapture(); |
+ mThumbnailCapturedForLoad = false; |
+ } |
+ |
+ @Override |
+ public void onPageLoadFinished(Tab tab) { |
+ rescheduleThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onPageLoadFailed(Tab tab, int errorCode) { |
+ cancelThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onShown(Tab tab) { |
+ // For tabs opened in the background, they may finish loading prior to becoming visible |
+ // and the thumbnail capture triggered as part of load finish will be skipped as the |
+ // tab has nothing rendered. To handle this case, we also attempt thumbnail capture |
+ // when showing the tab to give it a better chance to have valid content. |
+ rescheduleThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onClosingStateChanged(Tab tab, boolean closing) { |
+ if (closing) cancelThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onDestroyed(Tab tab) { |
+ mTab.removeObserver(mTabObserver); |
+ if (mContentViewCore != null) { |
+ mContentViewCore.removeGestureStateListener(mGestureListener); |
+ mContentViewCore = null; |
+ } |
+ } |
+ |
+ @Override |
+ public void onDidStartProvisionalLoadForFrame( |
+ Tab tab, long frameId, long parentFrameId, boolean isMainFrame, String validatedUrl, |
+ boolean isErrorPage, boolean isIframeSrcdoc) { |
+ if (isMainFrame) { |
+ mWasRenderViewHostReady = mIsRenderViewHostReady; |
+ mIsRenderViewHostReady = false; |
+ } |
+ } |
+ |
+ @Override |
+ public void onDidFailLoad( |
+ Tab tab, boolean isProvisionalLoad, boolean isMainFrame, int errorCode, |
+ String description, String failingUrl) { |
+ // For a case that URL overriding happens, we should recover |mIsRenderViewHostReady| to |
+ // its old value to enable capturing thumbnail of the current page. |
+ // If this failure shows an error page, capturing thumbnail will be denied anyway in |
+ // canUpdateHistoryThumbnail(). |
+ if (isProvisionalLoad && isMainFrame) mIsRenderViewHostReady = mWasRenderViewHostReady; |
+ } |
+ |
+ @Override |
+ public void onDidCommitProvisionalLoadForFrame( |
+ Tab tab, long frameId, boolean isMainFrame, String url, int transitionType) { |
+ if (isMainFrame) mIsRenderViewHostReady = true; |
+ } |
+ }; |
+ |
+ private GestureStateListener mGestureListener = new GestureStateListener() { |
+ @Override |
+ public void onFlingStartGesture(int vx, int vy, int scrollOffsetY, int scrollExtentY) { |
+ cancelThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) { |
+ rescheduleThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
+ cancelThumbnailCapture(); |
+ } |
+ |
+ @Override |
+ public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { |
+ rescheduleThumbnailCapture(); |
+ } |
+ }; |
+ |
+ /** |
+ * Creates a thumbnail tab helper for the given tab. |
+ * @param tab The Tab whose thumbnails will be generated by this helper. |
+ */ |
+ public static void createForTab(Tab tab) { |
+ new ThumbnailTabHelper(tab); |
+ } |
+ |
+ /** |
+ * Constructs the thumbnail tab helper for a given Tab. |
+ * @param tab The Tab whose thumbnails will be generated by this helper. |
+ */ |
+ private ThumbnailTabHelper(Tab tab) { |
+ mTab = tab; |
+ mTab.addObserver(mTabObserver); |
+ |
+ mHandler = new Handler(); |
+ |
+ Resources res = tab.getWindowAndroid().getApplicationContext().getResources(); |
+ mThumbnailWidth = res.getDimensionPixelSize(R.dimen.most_visited_thumbnail_width); |
+ mThumbnailHeight = res.getDimensionPixelSize(R.dimen.most_visited_thumbnail_height); |
+ |
+ onContentChanged(); |
+ } |
+ |
+ private void onContentChanged() { |
+ if (mContentViewCore != null) { |
+ mContentViewCore.removeGestureStateListener(mGestureListener); |
+ } |
+ |
+ mContentViewCore = mTab.getContentViewCore(); |
+ if (mContentViewCore != null) { |
+ mContentViewCore.addGestureStateListener(mGestureListener); |
+ nativeInitThumbnailHelper(mContentViewCore.getWebContents()); |
+ } |
+ } |
+ |
+ private ChromeActivity getActivity() { |
+ WindowAndroid window = mTab.getWindowAndroid(); |
+ return (ChromeActivity) window.getActivity().get(); |
+ } |
+ |
+ private void cancelThumbnailCapture() { |
+ mHandler.removeCallbacks(mThumbnailRunnable); |
+ } |
+ |
+ private void rescheduleThumbnailCapture() { |
+ if (mThumbnailCapturedForLoad) return; |
+ cancelThumbnailCapture(); |
+ // Capture will be rescheduled when the GestureStateListener receives a |
+ // scroll or fling end notification. |
+ if (mTab.getContentViewCore() != null |
+ && mTab.getContentViewCore().isScrollInProgress()) { |
+ return; |
+ } |
+ mHandler.postDelayed(mThumbnailRunnable, THUMBNAIL_CAPTURE_DELAY_MS); |
+ } |
+ |
+ private boolean shouldUpdateThumbnail() { |
+ return nativeShouldUpdateThumbnail(mTab.getProfile(), mTab.getUrl()); |
+ } |
+ |
+ private void updateThumbnail(Bitmap bitmap) { |
+ if (mTab.getContentViewCore() != null) { |
+ final boolean atTop = mTab.getContentViewCore().computeVerticalScrollOffset() == 0; |
+ nativeUpdateThumbnail(mTab.getWebContents(), bitmap, atTop); |
+ } |
+ } |
+ |
+ private boolean canUpdateHistoryThumbnail() { |
+ String url = mTab.getUrl(); |
+ if (url.startsWith(UrlConstants.CHROME_SCHEME) |
+ || url.startsWith(UrlConstants.CHROME_NATIVE_SCHEME)) { |
+ return false; |
+ } |
+ return mTab.isReady() |
+ && !mTab.isShowingErrorPage() |
+ && !mTab.isHidden() |
+ && !mTab.isShowingSadTab() |
+ && !mTab.isShowingInterstitialPage() |
+ && mTab.getProgress() == 100 |
+ && mTab.getWidth() > 0 |
+ && mTab.getHeight() > 0; |
+ } |
+ |
+ private void updateHistoryThumbnail(Bitmap bitmap) { |
+ if (mTab.isIncognito()) return; |
+ |
+ // TODO(yusufo): It will probably be faster and more efficient on resources to do this on |
+ // the native side, but the thumbnail_generator code has to be refactored a bit to allow |
+ // creating a downsized version of a bitmap progressively. |
+ if (bitmap.getWidth() != mThumbnailWidth |
+ || bitmap.getHeight() != mThumbnailHeight |
+ || bitmap.getConfig() != Config.ARGB_8888) { |
+ try { |
+ int[] dim = new int[] { |
+ bitmap.getWidth(), bitmap.getHeight() |
+ }; |
+ // If the thumbnail size is small compared to the bitmap size downsize in |
+ // two stages. This makes the final quality better. |
+ float scale = Math.max( |
+ (float) mThumbnailWidth / dim[0], |
+ (float) mThumbnailHeight / dim[1]); |
+ int adjustedWidth = (scale < 1) |
+ ? mThumbnailWidth * (int) (1 / Math.sqrt(scale)) : mThumbnailWidth; |
+ int adjustedHeight = (scale < 1) |
+ ? mThumbnailHeight * (int) (1 / Math.sqrt(scale)) : mThumbnailHeight; |
+ scale = MathUtils.scaleToFitTargetSize(dim, adjustedWidth, adjustedHeight); |
+ // Horizontally center the source bitmap in the final result. |
+ float leftOffset = (adjustedWidth - dim[0]) / 2.0f / scale; |
+ Bitmap tmpBitmap = Bitmap.createBitmap(adjustedWidth, |
+ adjustedHeight, Config.ARGB_8888); |
+ Canvas c = new Canvas(tmpBitmap); |
+ c.scale(scale, scale); |
+ c.drawBitmap(bitmap, leftOffset, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); |
+ if (scale < 1) { |
+ tmpBitmap = Bitmap.createScaledBitmap(tmpBitmap, |
+ mThumbnailWidth, mThumbnailHeight, true); |
+ } |
+ updateThumbnail(tmpBitmap); |
+ tmpBitmap.recycle(); |
+ } catch (OutOfMemoryError ex) { |
+ Log.w(TAG, "OutOfMemoryError while updating the history thumbnail."); |
+ } |
+ } else { |
+ updateThumbnail(bitmap); |
+ } |
+ } |
+ |
+ private static native void nativeInitThumbnailHelper(WebContents webContents); |
+ private static native void nativeUpdateThumbnail( |
+ WebContents webContents, Bitmap bitmap, boolean atTop); |
+ private static native boolean nativeShouldUpdateThumbnail(Profile profile, String url); |
+} |