1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
/* -*- 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.BitmapUtils;
import org.mozilla.gecko.gfx.IntSize;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import android.graphics.Bitmap;
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.
*/
public 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)GeckoAppShell.getContext().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(GeckoAppShell.getContext().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 (IllegalArgumentException iae) {
Log.w(LOGTAG, iae.toString());
} catch (OutOfMemoryError oom) {
Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
}
// If we hit an error above, 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.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer);
GeckoAppShell.sendEventToGecko(e);
}
/* This method is invoked by JNI once the thumbnail data is ready. */
public static void notifyThumbnail(ByteBuffer data, int tabId, boolean success) {
Tab tab = Tabs.getInstance().getTab(tabId);
ThumbnailHelper helper = ThumbnailHelper.getInstance();
if (success && tab != null) {
helper.handleThumbnailData(tab, data);
}
helper.processNextThumbnail(tab);
}
private void processNextThumbnail(Tab tab) {
Tab nextTab = null;
synchronized (mPendingThumbnails) {
if (tab != null && 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 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);
}
}
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) {
if (bitmap == null) {
if (compressed == null) {
Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
return;
}
bitmap = BitmapUtils.decodeByteArray(compressed);
}
tab.updateThumbnail(bitmap);
}
private boolean shouldUpdateThumbnail(Tab tab) {
return (Tabs.getInstance().isSelectedTab(tab) || (GeckoAppShell.getGeckoInterface() != null && GeckoAppShell.getGeckoInterface().areTabsShown()));
}
}
|