Bug 817067 - Introduce a ThumbnailHelper to deal with thumbnailing of pages. r=cpeterson, gcp

This commit is contained in:
Kartikaya Gupta 2012-12-03 14:27:34 -05:00
parent 04efb729ae
commit 7e8cc24ac5
7 changed files with 223 additions and 104 deletions

View File

@ -742,8 +742,8 @@ public class AboutHomeContent extends ScrollView
// Just using getWidth() will use incorrect values during onMeasure when rotating the device
// Instead we pass in the measuredWidth, which is correct
int w = getColumnWidth(measuredWidth);
Tabs.setThumbnailWidth(w);
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int)(w*Tabs.getThumbnailAspectRatio()*numRows) + getPaddingTop() + getPaddingBottom(),
ThumbnailHelper.getInstance().setThumbnailWidth(w);
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int)(w*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO*numRows) + getPaddingTop() + getPaddingBottom(),
MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@ -771,7 +771,7 @@ public class AboutHomeContent extends ScrollView
public void bindView(View view, Context context, Cursor cursor) {
super.bindView(view, context, cursor);
view.setLayoutParams(new AbsListView.LayoutParams(mTopSitesGrid.getColumnWidth(),
Math.round(mTopSitesGrid.getColumnWidth()*Tabs.getThumbnailAspectRatio())));
Math.round(mTopSitesGrid.getColumnWidth()*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO)));
}
}

View File

@ -43,7 +43,6 @@ import android.content.pm.ServiceInfo;
import android.content.pm.Signature;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Rect;
@ -102,7 +101,6 @@ import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -705,54 +703,6 @@ abstract public class GeckoApp
outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
}
void getAndProcessThumbnailForTab(final Tab tab) {
if ("about:home".equals(tab.getURL())) {
tab.updateThumbnail(null);
return;
}
if (tab.getState() == Tab.STATE_DELAYED) {
if (tab.getURL() != null) {
byte[] thumbnail = BrowserDB.getThumbnailForUrl(getContentResolver(), tab.getURL());
if (thumbnail != null)
processThumbnail(tab, null, thumbnail);
}
return;
}
int dw = Tabs.getThumbnailWidth();
int dh = Tabs.getThumbnailHeight();
GeckoAppShell.sendEventToGecko(GeckoEvent.createScreenshotEvent(tab.getId(), 0, 0, 0, 0, 0, 0, dw, dh, dw, dh, ScreenshotHandler.SCREENSHOT_THUMBNAIL, tab.getThumbnailBuffer()));
}
void handleThumbnailData(Tab tab, ByteBuffer data) {
if (shouldUpdateThumbnail(tab)) {
Bitmap b = tab.getThumbnailBitmap();
data.position(0);
b.copyPixelsFromBuffer(data);
processThumbnail(tab, b, null);
}
}
void processThumbnail(Tab thumbnailTab, Bitmap bitmap, byte[] compressed) {
try {
if (bitmap == null) {
if (compressed == null) {
Log.w(LOGTAG, "processThumbnail: one of bitmap or compressed must be non-null!");
return;
}
bitmap = BitmapFactory.decodeByteArray(compressed, 0, compressed.length);
}
thumbnailTab.updateThumbnail(bitmap);
} catch (OutOfMemoryError ome) {
Log.w(LOGTAG, "decoding byte array ran out of memory", ome);
}
}
private boolean shouldUpdateThumbnail(Tab tab) {
return (Tabs.getInstance().isSelectedTab(tab) || areTabsShown());
}
public void hideFormAssistPopup() {
if (mFormAssistPopup != null)
mFormAssistPopup.hide();
@ -1224,7 +1174,7 @@ abstract public class GeckoApp
if (!TextUtils.equals(oldURL, tab.getURL()))
return;
getAndProcessThumbnailForTab(tab);
ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
if (Tabs.getInstance().isSelectedTab(tab)) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createStartPaintListentingEvent(tab.getId()));
ScreenshotHandler.screenshotWholePage(tab);

View File

@ -137,6 +137,7 @@ FENNEC_JAVA_FILES = \
Telemetry.java \
TextSelection.java \
TextSelectionHandle.java \
ThumbnailHelper.java \
WebAppAllocator.java \
ZoomConstraints.java \
gfx/BitmapUtils.java \

View File

@ -367,7 +367,7 @@ public final class ScreenshotHandler implements Runnable {
{
Tab tab = Tabs.getInstance().getTab(tabId);
if (tab != null) {
GeckoApp.mAppContext.handleThumbnailData(tab, data);
ThumbnailHelper.getInstance().handleThumbnailData(tab, data);
}
break;
}

View File

@ -7,7 +7,6 @@ package org.mozilla.gecko;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.Layer;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import org.mozilla.gecko.util.GeckoAsyncTask;
import org.json.JSONException;
@ -25,7 +24,6 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@ -64,7 +62,6 @@ public class Tab {
private ContentObserver mContentObserver;
private int mCheckerboardColor = Color.WHITE;
private int mState;
private ByteBuffer mThumbnailBuffer;
private Bitmap mThumbnailBitmap;
private boolean mDesktopMode;
private boolean mEnteringReaderMode;
@ -155,33 +152,25 @@ public class Tab {
return mThumbnail;
}
synchronized public ByteBuffer getThumbnailBuffer() {
int capacity = Tabs.getThumbnailWidth() * Tabs.getThumbnailHeight() * 2 /* 16 bpp */;
if (mThumbnailBuffer != null && mThumbnailBuffer.capacity() == capacity)
return mThumbnailBuffer;
freeBuffer();
mThumbnailBitmap = null;
mThumbnailBuffer = DirectBufferAllocator.allocate(capacity);
return mThumbnailBuffer;
}
synchronized public Bitmap getThumbnailBitmap() {
// Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
// reuse the bitmap there.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2) {
if (mThumbnailBitmap != null)
return mThumbnailBitmap;
} else {
if (mThumbnailBitmap != null)
public Bitmap getThumbnailBitmap(int width, int height) {
if (mThumbnailBitmap != null) {
// Bug 787318 - Honeycomb has a bug with bitmap caching, we can't
// reuse the bitmap there.
boolean honeycomb = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2);
boolean sizeChange = mThumbnailBitmap.getWidth() != width
|| mThumbnailBitmap.getHeight() != height;
if (honeycomb || sizeChange) {
mThumbnailBitmap.recycle();
mThumbnailBitmap = null;
}
}
return mThumbnailBitmap = Bitmap.createBitmap(Tabs.getThumbnailWidth(), Tabs.getThumbnailHeight(), Bitmap.Config.RGB_565);
}
synchronized void freeBuffer() {
DirectBufferAllocator.free(mThumbnailBuffer);
mThumbnailBuffer = null;
if (mThumbnailBitmap == null) {
mThumbnailBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
}
return mThumbnailBitmap;
}
public void updateThumbnail(final Bitmap b) {

View File

@ -51,8 +51,6 @@ public class Tabs implements GeckoEventListener {
private GeckoApp mActivity;
static private int sThumbnailWidth = -1;
private Tabs() {
registerEventListener("SessionHistory:New");
registerEventListener("SessionHistory:Back");
@ -69,24 +67,6 @@ public class Tabs implements GeckoEventListener {
registerEventListener("Reader:Share");
}
static public void setThumbnailWidth(int val) {
// Round this to the next highest power of two
sThumbnailWidth = (int)(Math.pow( 2, Math.ceil(Math.log(val)/Math.log(2) )));
}
static public int getThumbnailWidth() {
if (sThumbnailWidth < 0) {
sThumbnailWidth = (int) (GeckoApp.mAppContext.getResources().getDimension(R.dimen.tab_thumbnail_width));
}
return sThumbnailWidth & ~0x1;
}
static public int getThumbnailHeight() {
return Math.round(getThumbnailWidth() * getThumbnailAspectRatio()) & ~0x1;
}
static public float getThumbnailAspectRatio() { return 0.714f; }
public void attachToActivity(GeckoApp activity) {
mActivity = activity;
}
@ -114,7 +94,6 @@ public class Tabs implements GeckoEventListener {
Tab tab = getTab(id);
mOrder.remove(tab);
mTabs.remove(id);
tab.freeBuffer();
}
}
@ -330,12 +309,13 @@ public class Tabs implements GeckoEventListener {
}
public void refreshThumbnails() {
final ThumbnailHelper helper = ThumbnailHelper.getInstance();
Iterator<Tab> iterator = mTabs.values().iterator();
while (iterator.hasNext()) {
final Tab tab = iterator.next();
GeckoAppShell.getHandler().post(new Runnable() {
public void run() {
mActivity.getAndProcessThumbnailForTab(tab);
helper.getAndProcessThumbnailFor(tab);
}
});
}

View File

@ -0,0 +1,199 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.IntSize;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Helper class to generate thumbnails for tabs.
* Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
* The head of the queue is the thumbnail that is currently being processed; upon
* completion of the current thumbnail the next one is automatically processed.
* Changes to the thumbnail width are stashed in mPendingWidth and the change is
* applied between thumbnail processing. This allows a single thumbnail buffer to
* be used for all thumbnails.
*/
final class ThumbnailHelper {
private static final String LOGTAG = "GeckoThumbnailHelper";
public static final float THUMBNAIL_ASPECT_RATIO = 0.714f; // this is a 5:7 ratio (as per UX decision)
// static singleton stuff
private static ThumbnailHelper sInstance;
public static synchronized ThumbnailHelper getInstance() {
if (sInstance == null) {
sInstance = new ThumbnailHelper();
}
return sInstance;
}
// instance stuff
private final LinkedList<Tab> mPendingThumbnails; // synchronized access only
private AtomicInteger mPendingWidth;
private int mWidth;
private int mHeight;
private ByteBuffer mBuffer;
private ThumbnailHelper() {
mPendingThumbnails = new LinkedList<Tab>();
mPendingWidth = new AtomicInteger((int)GeckoApp.mAppContext.getResources().getDimension(R.dimen.tab_thumbnail_width));
mWidth = -1;
mHeight = -1;
}
public void getAndProcessThumbnailFor(Tab tab) {
if ("about:home".equals(tab.getURL())) {
tab.updateThumbnail(null);
return;
}
if (tab.getState() == Tab.STATE_DELAYED) {
String url = tab.getURL();
if (url != null) {
byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoApp.mAppContext.getContentResolver(), url);
if (thumbnail != null) {
setTabThumbnail(tab, null, thumbnail);
}
}
return;
}
synchronized (mPendingThumbnails) {
if (mPendingThumbnails.lastIndexOf(tab) > 0) {
// This tab is already in the queue, so don't add it again.
// Note that if this tab is only at the *head* of the queue,
// (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
// add it again because it may have already been thumbnailed
// and now we need to do it again.
return;
}
mPendingThumbnails.add(tab);
if (mPendingThumbnails.size() > 1) {
// Some thumbnail was already being processed, so wait
// for that to be done.
return;
}
}
requestThumbnailFor(tab);
}
public void setThumbnailWidth(int width) {
mPendingWidth.set(IntSize.nextPowerOfTwo(width));
}
private void updateThumbnailSize() {
// Apply any pending width updates
mWidth = mPendingWidth.get();
mWidth &= ~0x1; // Ensure the width is always an even number (bug 776906)
mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
int capacity = mWidth * mHeight * 2; // Multiply by 2 for 16bpp
if (mBuffer == null || mBuffer.capacity() != capacity) {
if (mBuffer != null) {
mBuffer = DirectBufferAllocator.free(mBuffer);
}
try {
mBuffer = DirectBufferAllocator.allocate(capacity);
} catch (OutOfMemoryError oom) {
Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
// At this point mBuffer will be pointing to null, so we are in a sane state.
}
}
}
private void requestThumbnailFor(Tab tab) {
updateThumbnailSize();
if (mBuffer == null) {
// Buffer allocation may have failed. In this case we can't send the
// event requesting the screenshot which means we won't get back a response
// and so our queue will grow unboundedly. Handle this scenario by clearing
// the queue (no point trying more thumbnailing right now since we're likely
// low on memory). We will try again normally on the next call to
// getAndProcessThumbnailFor which will hopefully be when we have more free memory.
synchronized (mPendingThumbnails) {
mPendingThumbnails.clear();
}
return;
}
GeckoEvent e = GeckoEvent.createScreenshotEvent(
tab.getId(),
0, 0, 0, 0, // sx, sy, sw, sh
0, 0, mWidth, mHeight, // dx, dy, dw, dh
mWidth, mHeight, // bw, bh
ScreenshotHandler.SCREENSHOT_THUMBNAIL,
mBuffer);
GeckoAppShell.sendEventToGecko(e);
}
/* This method is invoked by Gecko once the thumbnail data is ready. */
void handleThumbnailData(Tab tab, ByteBuffer data) {
if (data != mBuffer) {
// This should never happen, but log it and recover gracefully
Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
}
if (shouldUpdateThumbnail(tab)) {
processThumbnailData(tab, data);
}
Tab nextTab = null;
synchronized (mPendingThumbnails) {
if (tab != mPendingThumbnails.peek()) {
Log.e(LOGTAG, "handleThumbnailData called with unexpected tab's data!");
// This should never happen, but recover gracefully by processing the
// unexpected tab that we found in the queue
} else {
mPendingThumbnails.remove();
}
nextTab = mPendingThumbnails.peek();
}
if (nextTab != null) {
requestThumbnailFor(nextTab);
}
}
private void processThumbnailData(Tab tab, ByteBuffer data) {
Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
data.position(0);
b.copyPixelsFromBuffer(data);
setTabThumbnail(tab, b, null);
}
private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed) {
try {
if (bitmap == null) {
if (compressed == null) {
Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
return;
}
bitmap = BitmapFactory.decodeByteArray(compressed, 0, compressed.length);
}
tab.updateThumbnail(bitmap);
} catch (OutOfMemoryError ome) {
Log.w(LOGTAG, "setTabThumbnail: decoding byte array of length " + compressed.length + " ran out of memory");
}
}
private boolean shouldUpdateThumbnail(Tab tab) {
return (Tabs.getInstance().isSelectedTab(tab) || GeckoApp.mAppContext.areTabsShown());
}
}