1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/ThumbnailHelper.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,217 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.db.BrowserDB; 1.12 +import org.mozilla.gecko.gfx.BitmapUtils; 1.13 +import org.mozilla.gecko.gfx.IntSize; 1.14 +import org.mozilla.gecko.mozglue.DirectBufferAllocator; 1.15 +import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; 1.16 + 1.17 +import android.graphics.Bitmap; 1.18 +import android.util.Log; 1.19 +import android.content.res.Resources; 1.20 + 1.21 +import java.nio.ByteBuffer; 1.22 +import java.util.LinkedList; 1.23 +import java.util.concurrent.atomic.AtomicInteger; 1.24 + 1.25 +/** 1.26 + * Helper class to generate thumbnails for tabs. 1.27 + * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails. 1.28 + * The head of the queue is the thumbnail that is currently being processed; upon 1.29 + * completion of the current thumbnail the next one is automatically processed. 1.30 + * Changes to the thumbnail width are stashed in mPendingWidth and the change is 1.31 + * applied between thumbnail processing. This allows a single thumbnail buffer to 1.32 + * be used for all thumbnails. 1.33 + */ 1.34 +public final class ThumbnailHelper { 1.35 + private static final String LOGTAG = "GeckoThumbnailHelper"; 1.36 + 1.37 + public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision) 1.38 + 1.39 + // static singleton stuff 1.40 + 1.41 + private static ThumbnailHelper sInstance; 1.42 + 1.43 + public static synchronized ThumbnailHelper getInstance() { 1.44 + if (sInstance == null) { 1.45 + sInstance = new ThumbnailHelper(); 1.46 + } 1.47 + return sInstance; 1.48 + } 1.49 + 1.50 + // instance stuff 1.51 + 1.52 + private final LinkedList<Tab> mPendingThumbnails; // synchronized access only 1.53 + private AtomicInteger mPendingWidth; 1.54 + private int mWidth; 1.55 + private int mHeight; 1.56 + private ByteBuffer mBuffer; 1.57 + 1.58 + private ThumbnailHelper() { 1.59 + mPendingThumbnails = new LinkedList<Tab>(); 1.60 + try { 1.61 + mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width)); 1.62 + } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); } 1.63 + mWidth = -1; 1.64 + mHeight = -1; 1.65 + } 1.66 + 1.67 + public void getAndProcessThumbnailFor(Tab tab) { 1.68 + if (AboutPages.isAboutHome(tab.getURL())) { 1.69 + tab.updateThumbnail(null); 1.70 + return; 1.71 + } 1.72 + 1.73 + if (tab.getState() == Tab.STATE_DELAYED) { 1.74 + String url = tab.getURL(); 1.75 + if (url != null) { 1.76 + byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url); 1.77 + if (thumbnail != null) { 1.78 + setTabThumbnail(tab, null, thumbnail); 1.79 + } 1.80 + } 1.81 + return; 1.82 + } 1.83 + 1.84 + synchronized (mPendingThumbnails) { 1.85 + if (mPendingThumbnails.lastIndexOf(tab) > 0) { 1.86 + // This tab is already in the queue, so don't add it again. 1.87 + // Note that if this tab is only at the *head* of the queue, 1.88 + // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do 1.89 + // add it again because it may have already been thumbnailed 1.90 + // and now we need to do it again. 1.91 + return; 1.92 + } 1.93 + 1.94 + mPendingThumbnails.add(tab); 1.95 + if (mPendingThumbnails.size() > 1) { 1.96 + // Some thumbnail was already being processed, so wait 1.97 + // for that to be done. 1.98 + return; 1.99 + } 1.100 + } 1.101 + requestThumbnailFor(tab); 1.102 + } 1.103 + 1.104 + public void setThumbnailWidth(int width) { 1.105 + // Check inverted for safety: Bug 803299 Comment 34. 1.106 + if (GeckoAppShell.getScreenDepth() == 24) { 1.107 + mPendingWidth.set(width); 1.108 + } else { 1.109 + // Bug 776906: on 16-bit screens we need to ensure an even width. 1.110 + mPendingWidth.set((width & 1) == 0 ? width : width + 1); 1.111 + } 1.112 + } 1.113 + 1.114 + private void updateThumbnailSize() { 1.115 + // Apply any pending width updates. 1.116 + mWidth = mPendingWidth.get(); 1.117 + 1.118 + mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); 1.119 + 1.120 + int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; 1.121 + int capacity = mWidth * mHeight * pixelSize; 1.122 + Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")"); 1.123 + if (mBuffer == null || mBuffer.capacity() != capacity) { 1.124 + if (mBuffer != null) { 1.125 + mBuffer = DirectBufferAllocator.free(mBuffer); 1.126 + } 1.127 + try { 1.128 + mBuffer = DirectBufferAllocator.allocate(capacity); 1.129 + } catch (IllegalArgumentException iae) { 1.130 + Log.w(LOGTAG, iae.toString()); 1.131 + } catch (OutOfMemoryError oom) { 1.132 + Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity); 1.133 + } 1.134 + // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state. 1.135 + } 1.136 + } 1.137 + 1.138 + private void requestThumbnailFor(Tab tab) { 1.139 + updateThumbnailSize(); 1.140 + 1.141 + if (mBuffer == null) { 1.142 + // Buffer allocation may have failed. In this case we can't send the 1.143 + // event requesting the screenshot which means we won't get back a response 1.144 + // and so our queue will grow unboundedly. Handle this scenario by clearing 1.145 + // the queue (no point trying more thumbnailing right now since we're likely 1.146 + // low on memory). We will try again normally on the next call to 1.147 + // getAndProcessThumbnailFor which will hopefully be when we have more free memory. 1.148 + synchronized (mPendingThumbnails) { 1.149 + mPendingThumbnails.clear(); 1.150 + } 1.151 + return; 1.152 + } 1.153 + 1.154 + Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight); 1.155 + GeckoEvent e = GeckoEvent.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer); 1.156 + GeckoAppShell.sendEventToGecko(e); 1.157 + } 1.158 + 1.159 + /* This method is invoked by JNI once the thumbnail data is ready. */ 1.160 + @WrapElementForJNI(stubName = "SendThumbnail") 1.161 + public static void notifyThumbnail(ByteBuffer data, int tabId, boolean success) { 1.162 + Tab tab = Tabs.getInstance().getTab(tabId); 1.163 + ThumbnailHelper helper = ThumbnailHelper.getInstance(); 1.164 + if (success && tab != null) { 1.165 + helper.handleThumbnailData(tab, data); 1.166 + } 1.167 + helper.processNextThumbnail(tab); 1.168 + } 1.169 + 1.170 + private void processNextThumbnail(Tab tab) { 1.171 + Tab nextTab = null; 1.172 + synchronized (mPendingThumbnails) { 1.173 + if (tab != null && tab != mPendingThumbnails.peek()) { 1.174 + Log.e(LOGTAG, "handleThumbnailData called with unexpected tab's data!"); 1.175 + // This should never happen, but recover gracefully by processing the 1.176 + // unexpected tab that we found in the queue 1.177 + } else { 1.178 + mPendingThumbnails.remove(); 1.179 + } 1.180 + nextTab = mPendingThumbnails.peek(); 1.181 + } 1.182 + if (nextTab != null) { 1.183 + requestThumbnailFor(nextTab); 1.184 + } 1.185 + } 1.186 + 1.187 + private void handleThumbnailData(Tab tab, ByteBuffer data) { 1.188 + Log.d(LOGTAG, "handleThumbnailData: " + data.capacity()); 1.189 + if (data != mBuffer) { 1.190 + // This should never happen, but log it and recover gracefully 1.191 + Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!"); 1.192 + } 1.193 + 1.194 + if (shouldUpdateThumbnail(tab)) { 1.195 + processThumbnailData(tab, data); 1.196 + } 1.197 + } 1.198 + 1.199 + private void processThumbnailData(Tab tab, ByteBuffer data) { 1.200 + Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight); 1.201 + data.position(0); 1.202 + b.copyPixelsFromBuffer(data); 1.203 + setTabThumbnail(tab, b, null); 1.204 + } 1.205 + 1.206 + private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed) { 1.207 + if (bitmap == null) { 1.208 + if (compressed == null) { 1.209 + Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!"); 1.210 + return; 1.211 + } 1.212 + bitmap = BitmapUtils.decodeByteArray(compressed); 1.213 + } 1.214 + tab.updateThumbnail(bitmap); 1.215 + } 1.216 + 1.217 + private boolean shouldUpdateThumbnail(Tab tab) { 1.218 + return (Tabs.getInstance().isSelectedTab(tab) || (GeckoAppShell.getGeckoInterface() != null && GeckoAppShell.getGeckoInterface().areTabsShown())); 1.219 + } 1.220 +}