Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.db.BrowserDB; |
michael@0 | 9 | import org.mozilla.gecko.gfx.BitmapUtils; |
michael@0 | 10 | import org.mozilla.gecko.gfx.IntSize; |
michael@0 | 11 | import org.mozilla.gecko.mozglue.DirectBufferAllocator; |
michael@0 | 12 | import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; |
michael@0 | 13 | |
michael@0 | 14 | import android.graphics.Bitmap; |
michael@0 | 15 | import android.util.Log; |
michael@0 | 16 | import android.content.res.Resources; |
michael@0 | 17 | |
michael@0 | 18 | import java.nio.ByteBuffer; |
michael@0 | 19 | import java.util.LinkedList; |
michael@0 | 20 | import java.util.concurrent.atomic.AtomicInteger; |
michael@0 | 21 | |
michael@0 | 22 | /** |
michael@0 | 23 | * Helper class to generate thumbnails for tabs. |
michael@0 | 24 | * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails. |
michael@0 | 25 | * The head of the queue is the thumbnail that is currently being processed; upon |
michael@0 | 26 | * completion of the current thumbnail the next one is automatically processed. |
michael@0 | 27 | * Changes to the thumbnail width are stashed in mPendingWidth and the change is |
michael@0 | 28 | * applied between thumbnail processing. This allows a single thumbnail buffer to |
michael@0 | 29 | * be used for all thumbnails. |
michael@0 | 30 | */ |
michael@0 | 31 | public final class ThumbnailHelper { |
michael@0 | 32 | private static final String LOGTAG = "GeckoThumbnailHelper"; |
michael@0 | 33 | |
michael@0 | 34 | public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision) |
michael@0 | 35 | |
michael@0 | 36 | // static singleton stuff |
michael@0 | 37 | |
michael@0 | 38 | private static ThumbnailHelper sInstance; |
michael@0 | 39 | |
michael@0 | 40 | public static synchronized ThumbnailHelper getInstance() { |
michael@0 | 41 | if (sInstance == null) { |
michael@0 | 42 | sInstance = new ThumbnailHelper(); |
michael@0 | 43 | } |
michael@0 | 44 | return sInstance; |
michael@0 | 45 | } |
michael@0 | 46 | |
michael@0 | 47 | // instance stuff |
michael@0 | 48 | |
michael@0 | 49 | private final LinkedList<Tab> mPendingThumbnails; // synchronized access only |
michael@0 | 50 | private AtomicInteger mPendingWidth; |
michael@0 | 51 | private int mWidth; |
michael@0 | 52 | private int mHeight; |
michael@0 | 53 | private ByteBuffer mBuffer; |
michael@0 | 54 | |
michael@0 | 55 | private ThumbnailHelper() { |
michael@0 | 56 | mPendingThumbnails = new LinkedList<Tab>(); |
michael@0 | 57 | try { |
michael@0 | 58 | mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width)); |
michael@0 | 59 | } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); } |
michael@0 | 60 | mWidth = -1; |
michael@0 | 61 | mHeight = -1; |
michael@0 | 62 | } |
michael@0 | 63 | |
michael@0 | 64 | public void getAndProcessThumbnailFor(Tab tab) { |
michael@0 | 65 | if (AboutPages.isAboutHome(tab.getURL())) { |
michael@0 | 66 | tab.updateThumbnail(null); |
michael@0 | 67 | return; |
michael@0 | 68 | } |
michael@0 | 69 | |
michael@0 | 70 | if (tab.getState() == Tab.STATE_DELAYED) { |
michael@0 | 71 | String url = tab.getURL(); |
michael@0 | 72 | if (url != null) { |
michael@0 | 73 | byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url); |
michael@0 | 74 | if (thumbnail != null) { |
michael@0 | 75 | setTabThumbnail(tab, null, thumbnail); |
michael@0 | 76 | } |
michael@0 | 77 | } |
michael@0 | 78 | return; |
michael@0 | 79 | } |
michael@0 | 80 | |
michael@0 | 81 | synchronized (mPendingThumbnails) { |
michael@0 | 82 | if (mPendingThumbnails.lastIndexOf(tab) > 0) { |
michael@0 | 83 | // This tab is already in the queue, so don't add it again. |
michael@0 | 84 | // Note that if this tab is only at the *head* of the queue, |
michael@0 | 85 | // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do |
michael@0 | 86 | // add it again because it may have already been thumbnailed |
michael@0 | 87 | // and now we need to do it again. |
michael@0 | 88 | return; |
michael@0 | 89 | } |
michael@0 | 90 | |
michael@0 | 91 | mPendingThumbnails.add(tab); |
michael@0 | 92 | if (mPendingThumbnails.size() > 1) { |
michael@0 | 93 | // Some thumbnail was already being processed, so wait |
michael@0 | 94 | // for that to be done. |
michael@0 | 95 | return; |
michael@0 | 96 | } |
michael@0 | 97 | } |
michael@0 | 98 | requestThumbnailFor(tab); |
michael@0 | 99 | } |
michael@0 | 100 | |
michael@0 | 101 | public void setThumbnailWidth(int width) { |
michael@0 | 102 | // Check inverted for safety: Bug 803299 Comment 34. |
michael@0 | 103 | if (GeckoAppShell.getScreenDepth() == 24) { |
michael@0 | 104 | mPendingWidth.set(width); |
michael@0 | 105 | } else { |
michael@0 | 106 | // Bug 776906: on 16-bit screens we need to ensure an even width. |
michael@0 | 107 | mPendingWidth.set((width & 1) == 0 ? width : width + 1); |
michael@0 | 108 | } |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | private void updateThumbnailSize() { |
michael@0 | 112 | // Apply any pending width updates. |
michael@0 | 113 | mWidth = mPendingWidth.get(); |
michael@0 | 114 | |
michael@0 | 115 | mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO); |
michael@0 | 116 | |
michael@0 | 117 | int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2; |
michael@0 | 118 | int capacity = mWidth * mHeight * pixelSize; |
michael@0 | 119 | Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")"); |
michael@0 | 120 | if (mBuffer == null || mBuffer.capacity() != capacity) { |
michael@0 | 121 | if (mBuffer != null) { |
michael@0 | 122 | mBuffer = DirectBufferAllocator.free(mBuffer); |
michael@0 | 123 | } |
michael@0 | 124 | try { |
michael@0 | 125 | mBuffer = DirectBufferAllocator.allocate(capacity); |
michael@0 | 126 | } catch (IllegalArgumentException iae) { |
michael@0 | 127 | Log.w(LOGTAG, iae.toString()); |
michael@0 | 128 | } catch (OutOfMemoryError oom) { |
michael@0 | 129 | Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity); |
michael@0 | 130 | } |
michael@0 | 131 | // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state. |
michael@0 | 132 | } |
michael@0 | 133 | } |
michael@0 | 134 | |
michael@0 | 135 | private void requestThumbnailFor(Tab tab) { |
michael@0 | 136 | updateThumbnailSize(); |
michael@0 | 137 | |
michael@0 | 138 | if (mBuffer == null) { |
michael@0 | 139 | // Buffer allocation may have failed. In this case we can't send the |
michael@0 | 140 | // event requesting the screenshot which means we won't get back a response |
michael@0 | 141 | // and so our queue will grow unboundedly. Handle this scenario by clearing |
michael@0 | 142 | // the queue (no point trying more thumbnailing right now since we're likely |
michael@0 | 143 | // low on memory). We will try again normally on the next call to |
michael@0 | 144 | // getAndProcessThumbnailFor which will hopefully be when we have more free memory. |
michael@0 | 145 | synchronized (mPendingThumbnails) { |
michael@0 | 146 | mPendingThumbnails.clear(); |
michael@0 | 147 | } |
michael@0 | 148 | return; |
michael@0 | 149 | } |
michael@0 | 150 | |
michael@0 | 151 | Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight); |
michael@0 | 152 | GeckoEvent e = GeckoEvent.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer); |
michael@0 | 153 | GeckoAppShell.sendEventToGecko(e); |
michael@0 | 154 | } |
michael@0 | 155 | |
michael@0 | 156 | /* This method is invoked by JNI once the thumbnail data is ready. */ |
michael@0 | 157 | @WrapElementForJNI(stubName = "SendThumbnail") |
michael@0 | 158 | public static void notifyThumbnail(ByteBuffer data, int tabId, boolean success) { |
michael@0 | 159 | Tab tab = Tabs.getInstance().getTab(tabId); |
michael@0 | 160 | ThumbnailHelper helper = ThumbnailHelper.getInstance(); |
michael@0 | 161 | if (success && tab != null) { |
michael@0 | 162 | helper.handleThumbnailData(tab, data); |
michael@0 | 163 | } |
michael@0 | 164 | helper.processNextThumbnail(tab); |
michael@0 | 165 | } |
michael@0 | 166 | |
michael@0 | 167 | private void processNextThumbnail(Tab tab) { |
michael@0 | 168 | Tab nextTab = null; |
michael@0 | 169 | synchronized (mPendingThumbnails) { |
michael@0 | 170 | if (tab != null && tab != mPendingThumbnails.peek()) { |
michael@0 | 171 | Log.e(LOGTAG, "handleThumbnailData called with unexpected tab's data!"); |
michael@0 | 172 | // This should never happen, but recover gracefully by processing the |
michael@0 | 173 | // unexpected tab that we found in the queue |
michael@0 | 174 | } else { |
michael@0 | 175 | mPendingThumbnails.remove(); |
michael@0 | 176 | } |
michael@0 | 177 | nextTab = mPendingThumbnails.peek(); |
michael@0 | 178 | } |
michael@0 | 179 | if (nextTab != null) { |
michael@0 | 180 | requestThumbnailFor(nextTab); |
michael@0 | 181 | } |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | private void handleThumbnailData(Tab tab, ByteBuffer data) { |
michael@0 | 185 | Log.d(LOGTAG, "handleThumbnailData: " + data.capacity()); |
michael@0 | 186 | if (data != mBuffer) { |
michael@0 | 187 | // This should never happen, but log it and recover gracefully |
michael@0 | 188 | Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!"); |
michael@0 | 189 | } |
michael@0 | 190 | |
michael@0 | 191 | if (shouldUpdateThumbnail(tab)) { |
michael@0 | 192 | processThumbnailData(tab, data); |
michael@0 | 193 | } |
michael@0 | 194 | } |
michael@0 | 195 | |
michael@0 | 196 | private void processThumbnailData(Tab tab, ByteBuffer data) { |
michael@0 | 197 | Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight); |
michael@0 | 198 | data.position(0); |
michael@0 | 199 | b.copyPixelsFromBuffer(data); |
michael@0 | 200 | setTabThumbnail(tab, b, null); |
michael@0 | 201 | } |
michael@0 | 202 | |
michael@0 | 203 | private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed) { |
michael@0 | 204 | if (bitmap == null) { |
michael@0 | 205 | if (compressed == null) { |
michael@0 | 206 | Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!"); |
michael@0 | 207 | return; |
michael@0 | 208 | } |
michael@0 | 209 | bitmap = BitmapUtils.decodeByteArray(compressed); |
michael@0 | 210 | } |
michael@0 | 211 | tab.updateThumbnail(bitmap); |
michael@0 | 212 | } |
michael@0 | 213 | |
michael@0 | 214 | private boolean shouldUpdateThumbnail(Tab tab) { |
michael@0 | 215 | return (Tabs.getInstance().isSelectedTab(tab) || (GeckoAppShell.getGeckoInterface() != null && GeckoAppShell.getGeckoInterface().areTabsShown())); |
michael@0 | 216 | } |
michael@0 | 217 | } |