michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.gfx.IntSize; michael@0: import org.mozilla.gecko.mozglue.DirectBufferAllocator; michael@0: import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; michael@0: michael@0: import android.graphics.Bitmap; michael@0: import android.util.Log; michael@0: import android.content.res.Resources; michael@0: michael@0: import java.nio.ByteBuffer; michael@0: import java.util.LinkedList; michael@0: import java.util.concurrent.atomic.AtomicInteger; michael@0: michael@0: /** michael@0: * Helper class to generate thumbnails for tabs. michael@0: * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails. michael@0: * The head of the queue is the thumbnail that is currently being processed; upon michael@0: * completion of the current thumbnail the next one is automatically processed. michael@0: * Changes to the thumbnail width are stashed in mPendingWidth and the change is michael@0: * applied between thumbnail processing. This allows a single thumbnail buffer to michael@0: * be used for all thumbnails. michael@0: */ michael@0: public final class ThumbnailHelper { michael@0: private static final String LOGTAG = "GeckoThumbnailHelper"; michael@0: michael@0: public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision) michael@0: michael@0: // static singleton stuff michael@0: michael@0: private static ThumbnailHelper sInstance; michael@0: michael@0: public static synchronized ThumbnailHelper getInstance() { michael@0: if (sInstance == null) { michael@0: sInstance = new ThumbnailHelper(); michael@0: } michael@0: return sInstance; michael@0: } michael@0: michael@0: // instance stuff michael@0: michael@0: private final LinkedList mPendingThumbnails; // synchronized access only michael@0: private AtomicInteger mPendingWidth; michael@0: private int mWidth; michael@0: private int mHeight; michael@0: private ByteBuffer mBuffer; michael@0: michael@0: private ThumbnailHelper() { michael@0: mPendingThumbnails = new LinkedList(); michael@0: try { michael@0: mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width)); michael@0: } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); } michael@0: mWidth = -1; michael@0: mHeight = -1; michael@0: } michael@0: michael@0: public void getAndProcessThumbnailFor(Tab tab) { michael@0: if (AboutPages.isAboutHome(tab.getURL())) { michael@0: tab.updateThumbnail(null); michael@0: return; michael@0: } michael@0: michael@0: if (tab.getState() == Tab.STATE_DELAYED) { michael@0: String url = tab.getURL(); michael@0: if (url != null) { michael@0: byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url); michael@0: if (thumbnail != null) { michael@0: setTabThumbnail(tab, null, thumbnail); michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: michael@0: synchronized (mPendingThumbnails) { michael@0: if (mPendingThumbnails.lastIndexOf(tab) > 0) { michael@0: // This tab is already in the queue, so don't add it again. michael@0: // Note that if this tab is only at the *head* of the queue, michael@0: // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do michael@0: // add it again because it may have already been thumbnailed michael@0: // and now we need to do it again. michael@0: return; michael@0: } michael@0: michael@0: mPendingThumbnails.add(tab); michael@0: if (mPendingThumbnails.size() > 1) { michael@0: // Some thumbnail was already being processed, so wait michael@0: // for that to be done. michael@0: return; michael@0: } michael@0: } michael@0: requestThumbnailFor(tab); michael@0: } michael@0: michael@0: public void setThumbnailWidth(int width) { michael@0: // Check inverted for safety: Bug 803299 Comment 34. michael@0: if (GeckoAppShell.getScreenDepth() == 24) { michael@0: mPendingWidth.set(width); michael@0: } else { michael@0: // Bug 776906: on 16-bit screens we need to ensure an even width. michael@0: mPendingWidth.set((width & 1) == 0 ? width : width + 1); michael@0: } michael@0: } michael@0: michael@0: private void updateThumbnailSize() { michael@0: // Apply any pending width updates. michael@0: mWidth = mPendingWidth.get(); michael@0: michael@0: mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); michael@0: michael@0: int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; michael@0: int capacity = mWidth * mHeight * pixelSize; michael@0: Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")"); michael@0: if (mBuffer == null || mBuffer.capacity() != capacity) { michael@0: if (mBuffer != null) { michael@0: mBuffer = DirectBufferAllocator.free(mBuffer); michael@0: } michael@0: try { michael@0: mBuffer = DirectBufferAllocator.allocate(capacity); michael@0: } catch (IllegalArgumentException iae) { michael@0: Log.w(LOGTAG, iae.toString()); michael@0: } catch (OutOfMemoryError oom) { michael@0: Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity); michael@0: } michael@0: // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state. michael@0: } michael@0: } michael@0: michael@0: private void requestThumbnailFor(Tab tab) { michael@0: updateThumbnailSize(); michael@0: michael@0: if (mBuffer == null) { michael@0: // Buffer allocation may have failed. In this case we can't send the michael@0: // event requesting the screenshot which means we won't get back a response michael@0: // and so our queue will grow unboundedly. Handle this scenario by clearing michael@0: // the queue (no point trying more thumbnailing right now since we're likely michael@0: // low on memory). We will try again normally on the next call to michael@0: // getAndProcessThumbnailFor which will hopefully be when we have more free memory. michael@0: synchronized (mPendingThumbnails) { michael@0: mPendingThumbnails.clear(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight); michael@0: GeckoEvent e = GeckoEvent.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer); michael@0: GeckoAppShell.sendEventToGecko(e); michael@0: } michael@0: michael@0: /* This method is invoked by JNI once the thumbnail data is ready. */ michael@0: @WrapElementForJNI(stubName = "SendThumbnail") michael@0: public static void notifyThumbnail(ByteBuffer data, int tabId, boolean success) { michael@0: Tab tab = Tabs.getInstance().getTab(tabId); michael@0: ThumbnailHelper helper = ThumbnailHelper.getInstance(); michael@0: if (success && tab != null) { michael@0: helper.handleThumbnailData(tab, data); michael@0: } michael@0: helper.processNextThumbnail(tab); michael@0: } michael@0: michael@0: private void processNextThumbnail(Tab tab) { michael@0: Tab nextTab = null; michael@0: synchronized (mPendingThumbnails) { michael@0: if (tab != null && tab != mPendingThumbnails.peek()) { michael@0: Log.e(LOGTAG, "handleThumbnailData called with unexpected tab's data!"); michael@0: // This should never happen, but recover gracefully by processing the michael@0: // unexpected tab that we found in the queue michael@0: } else { michael@0: mPendingThumbnails.remove(); michael@0: } michael@0: nextTab = mPendingThumbnails.peek(); michael@0: } michael@0: if (nextTab != null) { michael@0: requestThumbnailFor(nextTab); michael@0: } michael@0: } michael@0: michael@0: private void handleThumbnailData(Tab tab, ByteBuffer data) { michael@0: Log.d(LOGTAG, "handleThumbnailData: " + data.capacity()); michael@0: if (data != mBuffer) { michael@0: // This should never happen, but log it and recover gracefully michael@0: Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!"); michael@0: } michael@0: michael@0: if (shouldUpdateThumbnail(tab)) { michael@0: processThumbnailData(tab, data); michael@0: } michael@0: } michael@0: michael@0: private void processThumbnailData(Tab tab, ByteBuffer data) { michael@0: Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight); michael@0: data.position(0); michael@0: b.copyPixelsFromBuffer(data); michael@0: setTabThumbnail(tab, b, null); michael@0: } michael@0: michael@0: private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed) { michael@0: if (bitmap == null) { michael@0: if (compressed == null) { michael@0: Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!"); michael@0: return; michael@0: } michael@0: bitmap = BitmapUtils.decodeByteArray(compressed); michael@0: } michael@0: tab.updateThumbnail(bitmap); michael@0: } michael@0: michael@0: private boolean shouldUpdateThumbnail(Tab tab) { michael@0: return (Tabs.getInstance().isSelectedTab(tab) || (GeckoAppShell.getGeckoInterface() != null && GeckoAppShell.getGeckoInterface().areTabsShown())); michael@0: } michael@0: }