OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.tab; |
| 6 |
| 7 import android.content.res.Resources; |
| 8 import android.graphics.Bitmap; |
| 9 import android.graphics.Bitmap.Config; |
| 10 import android.graphics.Canvas; |
| 11 import android.graphics.Paint; |
| 12 import android.graphics.Rect; |
| 13 import android.os.Handler; |
| 14 import android.text.TextUtils; |
| 15 import android.util.Log; |
| 16 |
| 17 import com.google.android.apps.chrome.R; |
| 18 |
| 19 import org.chromium.chrome.browser.ChromeActivity; |
| 20 import org.chromium.chrome.browser.EmptyTabObserver; |
| 21 import org.chromium.chrome.browser.Tab; |
| 22 import org.chromium.chrome.browser.TabObserver; |
| 23 import org.chromium.chrome.browser.UrlConstants; |
| 24 import org.chromium.chrome.browser.profiles.Profile; |
| 25 import org.chromium.chrome.browser.util.MathUtils; |
| 26 import org.chromium.content.browser.ContentReadbackHandler; |
| 27 import org.chromium.content.browser.ContentViewCore; |
| 28 import org.chromium.content_public.browser.GestureStateListener; |
| 29 import org.chromium.content_public.browser.WebContents; |
| 30 import org.chromium.ui.base.WindowAndroid; |
| 31 |
| 32 /** |
| 33 * Handles capturing most visited thumbnails for a tab. |
| 34 */ |
| 35 public class ThumbnailTabHelper { |
| 36 |
| 37 private static final String TAG = "ThumbnailTabHelper"; |
| 38 |
| 39 /** The general motivation for this value is giving the scrollbar fadeout |
| 40 * animation sufficient time to finish before the capture executes. */ |
| 41 private static final int THUMBNAIL_CAPTURE_DELAY_MS = 350; |
| 42 |
| 43 private final Tab mTab; |
| 44 private final Handler mHandler; |
| 45 |
| 46 private final int mThumbnailWidth; |
| 47 private final int mThumbnailHeight; |
| 48 |
| 49 private ContentViewCore mContentViewCore; |
| 50 private boolean mThumbnailCapturedForLoad; |
| 51 private boolean mIsRenderViewHostReady; |
| 52 private boolean mWasRenderViewHostReady; |
| 53 |
| 54 private final Runnable mThumbnailRunnable = new Runnable() { |
| 55 @Override |
| 56 public void run() { |
| 57 // http://crbug.com/461506 : Do not get thumbnail unless render view
host is ready. |
| 58 if (!mIsRenderViewHostReady) return; |
| 59 |
| 60 if (mThumbnailCapturedForLoad) return; |
| 61 // Prevent redundant thumbnail capture attempts. |
| 62 mThumbnailCapturedForLoad = true; |
| 63 if (!canUpdateHistoryThumbnail()) { |
| 64 // Allow a hidden tab to re-attempt capture in the future via |s
how()|. |
| 65 mThumbnailCapturedForLoad = !mTab.isHidden(); |
| 66 return; |
| 67 } |
| 68 if (!shouldUpdateThumbnail()) return; |
| 69 |
| 70 // Scale the image so we're not copying more than we need to (from |
| 71 // the GPU). |
| 72 int[] dim = new int[] { |
| 73 mTab.getWidth(), mTab.getHeight() |
| 74 }; |
| 75 MathUtils.scaleToFitTargetSize(dim, mThumbnailWidth, mThumbnailHeigh
t); |
| 76 |
| 77 ContentReadbackHandler readbackHandler = getActivity().getContentRea
dbackHandler(); |
| 78 if (readbackHandler == null || mTab.getContentViewCore() == null) re
turn; |
| 79 final String requestedUrl = mTab.getUrl(); |
| 80 ContentReadbackHandler.GetBitmapCallback bitmapCallback = |
| 81 new ContentReadbackHandler.GetBitmapCallback() { |
| 82 @Override |
| 83 public void onFinishGetBitmap(Bitmap bitmap, int respons
e) { |
| 84 // Ensure that the URLs match for the requested page
, and ensure |
| 85 // that the page is still valid for thumbnail captur
ing (i.e. |
| 86 // not showing an error page). |
| 87 if (bitmap == null |
| 88 || !TextUtils.equals(requestedUrl, mTab.getU
rl()) |
| 89 || !mThumbnailCapturedForLoad |
| 90 || !canUpdateHistoryThumbnail()) { |
| 91 return; |
| 92 } |
| 93 updateHistoryThumbnail(bitmap); |
| 94 bitmap.recycle(); |
| 95 } |
| 96 }; |
| 97 readbackHandler.getContentBitmapAsync(1, new Rect(0, 0, dim[0], dim[
1]), |
| 98 mTab.getContentViewCore(), Bitmap.Config.ARGB_8888, bitmapCa
llback); |
| 99 } |
| 100 }; |
| 101 |
| 102 private final TabObserver mTabObserver = new EmptyTabObserver() { |
| 103 @Override |
| 104 public void onContentChanged(Tab tab) { |
| 105 ThumbnailTabHelper.this.onContentChanged(); |
| 106 } |
| 107 |
| 108 @Override |
| 109 public void onCrash(Tab tab, boolean sadTabShown) { |
| 110 cancelThumbnailCapture(); |
| 111 } |
| 112 |
| 113 @Override |
| 114 public void onPageLoadStarted(Tab tab) { |
| 115 cancelThumbnailCapture(); |
| 116 mThumbnailCapturedForLoad = false; |
| 117 } |
| 118 |
| 119 @Override |
| 120 public void onPageLoadFinished(Tab tab) { |
| 121 rescheduleThumbnailCapture(); |
| 122 } |
| 123 |
| 124 @Override |
| 125 public void onPageLoadFailed(Tab tab, int errorCode) { |
| 126 cancelThumbnailCapture(); |
| 127 } |
| 128 |
| 129 @Override |
| 130 public void onShown(Tab tab) { |
| 131 // For tabs opened in the background, they may finish loading prior
to becoming visible |
| 132 // and the thumbnail capture triggered as part of load finish will b
e skipped as the |
| 133 // tab has nothing rendered. To handle this case, we also attempt t
humbnail capture |
| 134 // when showing the tab to give it a better chance to have valid con
tent. |
| 135 rescheduleThumbnailCapture(); |
| 136 } |
| 137 |
| 138 @Override |
| 139 public void onClosingStateChanged(Tab tab, boolean closing) { |
| 140 if (closing) cancelThumbnailCapture(); |
| 141 } |
| 142 |
| 143 @Override |
| 144 public void onDestroyed(Tab tab) { |
| 145 mTab.removeObserver(mTabObserver); |
| 146 if (mContentViewCore != null) { |
| 147 mContentViewCore.removeGestureStateListener(mGestureListener); |
| 148 mContentViewCore = null; |
| 149 } |
| 150 } |
| 151 |
| 152 @Override |
| 153 public void onDidStartProvisionalLoadForFrame( |
| 154 Tab tab, long frameId, long parentFrameId, boolean isMainFrame,
String validatedUrl, |
| 155 boolean isErrorPage, boolean isIframeSrcdoc) { |
| 156 if (isMainFrame) { |
| 157 mWasRenderViewHostReady = mIsRenderViewHostReady; |
| 158 mIsRenderViewHostReady = false; |
| 159 } |
| 160 } |
| 161 |
| 162 @Override |
| 163 public void onDidFailLoad( |
| 164 Tab tab, boolean isProvisionalLoad, boolean isMainFrame, int err
orCode, |
| 165 String description, String failingUrl) { |
| 166 // For a case that URL overriding happens, we should recover |mIsRen
derViewHostReady| to |
| 167 // its old value to enable capturing thumbnail of the current page. |
| 168 // If this failure shows an error page, capturing thumbnail will be
denied anyway in |
| 169 // canUpdateHistoryThumbnail(). |
| 170 if (isProvisionalLoad && isMainFrame) mIsRenderViewHostReady = mWasR
enderViewHostReady; |
| 171 } |
| 172 |
| 173 @Override |
| 174 public void onDidCommitProvisionalLoadForFrame( |
| 175 Tab tab, long frameId, boolean isMainFrame, String url, int tran
sitionType) { |
| 176 if (isMainFrame) mIsRenderViewHostReady = true; |
| 177 } |
| 178 }; |
| 179 |
| 180 private GestureStateListener mGestureListener = new GestureStateListener() { |
| 181 @Override |
| 182 public void onFlingStartGesture(int vx, int vy, int scrollOffsetY, int s
crollExtentY) { |
| 183 cancelThumbnailCapture(); |
| 184 } |
| 185 |
| 186 @Override |
| 187 public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) { |
| 188 rescheduleThumbnailCapture(); |
| 189 } |
| 190 |
| 191 @Override |
| 192 public void onScrollStarted(int scrollOffsetY, int scrollExtentY) { |
| 193 cancelThumbnailCapture(); |
| 194 } |
| 195 |
| 196 @Override |
| 197 public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { |
| 198 rescheduleThumbnailCapture(); |
| 199 } |
| 200 }; |
| 201 |
| 202 /** |
| 203 * Creates a thumbnail tab helper for the given tab. |
| 204 * @param tab The Tab whose thumbnails will be generated by this helper. |
| 205 */ |
| 206 public static void createForTab(Tab tab) { |
| 207 new ThumbnailTabHelper(tab); |
| 208 } |
| 209 |
| 210 /** |
| 211 * Constructs the thumbnail tab helper for a given Tab. |
| 212 * @param tab The Tab whose thumbnails will be generated by this helper. |
| 213 */ |
| 214 private ThumbnailTabHelper(Tab tab) { |
| 215 mTab = tab; |
| 216 mTab.addObserver(mTabObserver); |
| 217 |
| 218 mHandler = new Handler(); |
| 219 |
| 220 Resources res = tab.getWindowAndroid().getApplicationContext().getResour
ces(); |
| 221 mThumbnailWidth = res.getDimensionPixelSize(R.dimen.most_visited_thumbna
il_width); |
| 222 mThumbnailHeight = res.getDimensionPixelSize(R.dimen.most_visited_thumbn
ail_height); |
| 223 |
| 224 onContentChanged(); |
| 225 } |
| 226 |
| 227 private void onContentChanged() { |
| 228 if (mContentViewCore != null) { |
| 229 mContentViewCore.removeGestureStateListener(mGestureListener); |
| 230 } |
| 231 |
| 232 mContentViewCore = mTab.getContentViewCore(); |
| 233 if (mContentViewCore != null) { |
| 234 mContentViewCore.addGestureStateListener(mGestureListener); |
| 235 nativeInitThumbnailHelper(mContentViewCore.getWebContents()); |
| 236 } |
| 237 } |
| 238 |
| 239 private ChromeActivity getActivity() { |
| 240 WindowAndroid window = mTab.getWindowAndroid(); |
| 241 return (ChromeActivity) window.getActivity().get(); |
| 242 } |
| 243 |
| 244 private void cancelThumbnailCapture() { |
| 245 mHandler.removeCallbacks(mThumbnailRunnable); |
| 246 } |
| 247 |
| 248 private void rescheduleThumbnailCapture() { |
| 249 if (mThumbnailCapturedForLoad) return; |
| 250 cancelThumbnailCapture(); |
| 251 // Capture will be rescheduled when the GestureStateListener receives a |
| 252 // scroll or fling end notification. |
| 253 if (mTab.getContentViewCore() != null |
| 254 && mTab.getContentViewCore().isScrollInProgress()) { |
| 255 return; |
| 256 } |
| 257 mHandler.postDelayed(mThumbnailRunnable, THUMBNAIL_CAPTURE_DELAY_MS); |
| 258 } |
| 259 |
| 260 private boolean shouldUpdateThumbnail() { |
| 261 return nativeShouldUpdateThumbnail(mTab.getProfile(), mTab.getUrl()); |
| 262 } |
| 263 |
| 264 private void updateThumbnail(Bitmap bitmap) { |
| 265 if (mTab.getContentViewCore() != null) { |
| 266 final boolean atTop = mTab.getContentViewCore().computeVerticalScrol
lOffset() == 0; |
| 267 nativeUpdateThumbnail(mTab.getWebContents(), bitmap, atTop); |
| 268 } |
| 269 } |
| 270 |
| 271 private boolean canUpdateHistoryThumbnail() { |
| 272 String url = mTab.getUrl(); |
| 273 if (url.startsWith(UrlConstants.CHROME_SCHEME) |
| 274 || url.startsWith(UrlConstants.CHROME_NATIVE_SCHEME)) { |
| 275 return false; |
| 276 } |
| 277 return mTab.isReady() |
| 278 && !mTab.isShowingErrorPage() |
| 279 && !mTab.isHidden() |
| 280 && !mTab.isShowingSadTab() |
| 281 && !mTab.isShowingInterstitialPage() |
| 282 && mTab.getProgress() == 100 |
| 283 && mTab.getWidth() > 0 |
| 284 && mTab.getHeight() > 0; |
| 285 } |
| 286 |
| 287 private void updateHistoryThumbnail(Bitmap bitmap) { |
| 288 if (mTab.isIncognito()) return; |
| 289 |
| 290 // TODO(yusufo): It will probably be faster and more efficient on resour
ces to do this on |
| 291 // the native side, but the thumbnail_generator code has to be refactore
d a bit to allow |
| 292 // creating a downsized version of a bitmap progressively. |
| 293 if (bitmap.getWidth() != mThumbnailWidth |
| 294 || bitmap.getHeight() != mThumbnailHeight |
| 295 || bitmap.getConfig() != Config.ARGB_8888) { |
| 296 try { |
| 297 int[] dim = new int[] { |
| 298 bitmap.getWidth(), bitmap.getHeight() |
| 299 }; |
| 300 // If the thumbnail size is small compared to the bitmap size do
wnsize in |
| 301 // two stages. This makes the final quality better. |
| 302 float scale = Math.max( |
| 303 (float) mThumbnailWidth / dim[0], |
| 304 (float) mThumbnailHeight / dim[1]); |
| 305 int adjustedWidth = (scale < 1) |
| 306 ? mThumbnailWidth * (int) (1 / Math.sqrt(scale)) : mThum
bnailWidth; |
| 307 int adjustedHeight = (scale < 1) |
| 308 ? mThumbnailHeight * (int) (1 / Math.sqrt(scale)) : mThu
mbnailHeight; |
| 309 scale = MathUtils.scaleToFitTargetSize(dim, adjustedWidth, adjus
tedHeight); |
| 310 // Horizontally center the source bitmap in the final result. |
| 311 float leftOffset = (adjustedWidth - dim[0]) / 2.0f / scale; |
| 312 Bitmap tmpBitmap = Bitmap.createBitmap(adjustedWidth, |
| 313 adjustedHeight, Config.ARGB_8888); |
| 314 Canvas c = new Canvas(tmpBitmap); |
| 315 c.scale(scale, scale); |
| 316 c.drawBitmap(bitmap, leftOffset, 0, new Paint(Paint.FILTER_BITMA
P_FLAG)); |
| 317 if (scale < 1) { |
| 318 tmpBitmap = Bitmap.createScaledBitmap(tmpBitmap, |
| 319 mThumbnailWidth, mThumbnailHeight, true); |
| 320 } |
| 321 updateThumbnail(tmpBitmap); |
| 322 tmpBitmap.recycle(); |
| 323 } catch (OutOfMemoryError ex) { |
| 324 Log.w(TAG, "OutOfMemoryError while updating the history thumbnai
l."); |
| 325 } |
| 326 } else { |
| 327 updateThumbnail(bitmap); |
| 328 } |
| 329 } |
| 330 |
| 331 private static native void nativeInitThumbnailHelper(WebContents webContents
); |
| 332 private static native void nativeUpdateThumbnail( |
| 333 WebContents webContents, Bitmap bitmap, boolean atTop); |
| 334 private static native boolean nativeShouldUpdateThumbnail(Profile profile, S
tring url); |
| 335 } |
OLD | NEW |