diff -r 000000000000 -r 6474c204b198 mobile/android/base/ThumbnailHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/ThumbnailHelper.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,217 @@ +/* -*- 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 org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; + +import android.graphics.Bitmap; +import android.util.Log; +import android.content.res.Resources; + +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.571f; // this is a 4: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 mPendingThumbnails; // synchronized access only + private AtomicInteger mPendingWidth; + private int mWidth; + private int mHeight; + private ByteBuffer mBuffer; + + private ThumbnailHelper() { + mPendingThumbnails = new LinkedList(); + try { + mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width)); + } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); } + mWidth = -1; + mHeight = -1; + } + + public void getAndProcessThumbnailFor(Tab tab) { + if (AboutPages.isAboutHome(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) { + // Check inverted for safety: Bug 803299 Comment 34. + if (GeckoAppShell.getScreenDepth() == 24) { + mPendingWidth.set(width); + } else { + // Bug 776906: on 16-bit screens we need to ensure an even width. + mPendingWidth.set((width & 1) == 0 ? width : width + 1); + } + } + + private void updateThumbnailSize() { + // Apply any pending width updates. + mWidth = mPendingWidth.get(); + + mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); + + int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; + int capacity = mWidth * mHeight * pixelSize; + Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")"); + 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; + } + + Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight); + GeckoEvent e = GeckoEvent.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer); + GeckoAppShell.sendEventToGecko(e); + } + + /* This method is invoked by JNI once the thumbnail data is ready. */ + @WrapElementForJNI(stubName = "SendThumbnail") + 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) { + Log.d(LOGTAG, "handleThumbnailData: " + data.capacity()); + 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())); + } +}