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