|
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.gfx; |
|
7 |
|
8 import java.io.IOException; |
|
9 import java.io.InputStream; |
|
10 import java.lang.reflect.Field; |
|
11 import java.net.MalformedURLException; |
|
12 import java.net.URL; |
|
13 |
|
14 import org.mozilla.gecko.R; |
|
15 import org.mozilla.gecko.util.GeckoJarReader; |
|
16 import org.mozilla.gecko.util.ThreadUtils; |
|
17 import org.mozilla.gecko.util.UiAsyncTask; |
|
18 import org.mozilla.gecko.Tab; |
|
19 import org.mozilla.gecko.Tabs; |
|
20 import org.mozilla.gecko.ThumbnailHelper; |
|
21 |
|
22 import android.content.Context; |
|
23 import android.content.res.Resources; |
|
24 import android.graphics.Bitmap; |
|
25 import android.graphics.BitmapFactory; |
|
26 import android.graphics.Canvas; |
|
27 import android.graphics.Color; |
|
28 import android.graphics.drawable.BitmapDrawable; |
|
29 import android.graphics.drawable.Drawable; |
|
30 import android.net.Uri; |
|
31 import android.text.TextUtils; |
|
32 import android.util.Base64; |
|
33 import android.util.Log; |
|
34 |
|
35 public final class BitmapUtils { |
|
36 private static final String LOGTAG = "GeckoBitmapUtils"; |
|
37 |
|
38 private BitmapUtils() {} |
|
39 |
|
40 public interface BitmapLoader { |
|
41 public void onBitmapFound(Drawable d); |
|
42 } |
|
43 |
|
44 private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) { |
|
45 if (ThreadUtils.isOnUiThread()) { |
|
46 loader.onBitmapFound(d); |
|
47 return; |
|
48 } |
|
49 |
|
50 ThreadUtils.postToUiThread(new Runnable() { |
|
51 @Override |
|
52 public void run() { |
|
53 loader.onBitmapFound(d); |
|
54 } |
|
55 }); |
|
56 } |
|
57 |
|
58 /** |
|
59 * Attempts to find a drawable associated with a given string, using its URI scheme to determine |
|
60 * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and |
|
61 * will be called with `null` if no drawable is found. |
|
62 * |
|
63 * The BitmapLoader `onBitmapFound` method always runs on the UI thread. |
|
64 */ |
|
65 public static void getDrawable(final Context context, final String data, final BitmapLoader loader) { |
|
66 if (TextUtils.isEmpty(data)) { |
|
67 runOnBitmapFoundOnUiThread(loader, null); |
|
68 return; |
|
69 } |
|
70 |
|
71 if (data.startsWith("data")) { |
|
72 final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data)); |
|
73 runOnBitmapFoundOnUiThread(loader, d); |
|
74 return; |
|
75 } |
|
76 |
|
77 if (data.startsWith("thumbnail:")) { |
|
78 getThumbnailDrawable(context, data, loader); |
|
79 return; |
|
80 } |
|
81 |
|
82 if (data.startsWith("jar:") || data.startsWith("file://")) { |
|
83 (new UiAsyncTask<Void, Void, Drawable>(ThreadUtils.getBackgroundHandler()) { |
|
84 @Override |
|
85 public Drawable doInBackground(Void... params) { |
|
86 try { |
|
87 if (data.startsWith("jar:jar")) { |
|
88 return GeckoJarReader.getBitmapDrawable(context.getResources(), data); |
|
89 } |
|
90 |
|
91 // Don't attempt to validate the JAR signature when loading an add-on icon |
|
92 if (data.startsWith("jar:file")) { |
|
93 return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data)); |
|
94 } |
|
95 |
|
96 final URL url = new URL(data); |
|
97 final InputStream is = (InputStream) url.getContent(); |
|
98 try { |
|
99 return Drawable.createFromStream(is, "src"); |
|
100 } finally { |
|
101 is.close(); |
|
102 } |
|
103 } catch (Exception e) { |
|
104 Log.w(LOGTAG, "Unable to set icon", e); |
|
105 } |
|
106 return null; |
|
107 } |
|
108 |
|
109 @Override |
|
110 public void onPostExecute(Drawable drawable) { |
|
111 loader.onBitmapFound(drawable); |
|
112 } |
|
113 }).execute(); |
|
114 return; |
|
115 } |
|
116 |
|
117 if (data.startsWith("-moz-icon://")) { |
|
118 final Uri imageUri = Uri.parse(data); |
|
119 final String ssp = imageUri.getSchemeSpecificPart(); |
|
120 final String resource = ssp.substring(ssp.lastIndexOf('/') + 1); |
|
121 |
|
122 try { |
|
123 final Drawable d = context.getPackageManager().getApplicationIcon(resource); |
|
124 runOnBitmapFoundOnUiThread(loader, d); |
|
125 } catch(Exception ex) { } |
|
126 |
|
127 return; |
|
128 } |
|
129 |
|
130 if (data.startsWith("drawable://")) { |
|
131 final Uri imageUri = Uri.parse(data); |
|
132 final int id = getResource(imageUri, R.drawable.ic_status_logo); |
|
133 final Drawable d = context.getResources().getDrawable(id); |
|
134 |
|
135 runOnBitmapFoundOnUiThread(loader, d); |
|
136 return; |
|
137 } |
|
138 |
|
139 runOnBitmapFoundOnUiThread(loader, null); |
|
140 } |
|
141 |
|
142 public static void getThumbnailDrawable(final Context context, final String data, final BitmapLoader loader) { |
|
143 int id = Integer.parseInt(data.substring(10), 10); |
|
144 final Tab tab = Tabs.getInstance().getTab(id); |
|
145 runOnBitmapFoundOnUiThread(loader, tab.getThumbnail()); |
|
146 Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() { |
|
147 public void onTabChanged(Tab t, Tabs.TabEvents msg, Object data) { |
|
148 if (tab == t && msg == Tabs.TabEvents.THUMBNAIL) { |
|
149 Tabs.unregisterOnTabsChangedListener(this); |
|
150 runOnBitmapFoundOnUiThread(loader, t.getThumbnail()); |
|
151 } |
|
152 } |
|
153 }); |
|
154 ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab); |
|
155 } |
|
156 |
|
157 public static Bitmap decodeByteArray(byte[] bytes) { |
|
158 return decodeByteArray(bytes, null); |
|
159 } |
|
160 |
|
161 public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) { |
|
162 return decodeByteArray(bytes, 0, bytes.length, options); |
|
163 } |
|
164 |
|
165 public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) { |
|
166 return decodeByteArray(bytes, offset, length, null); |
|
167 } |
|
168 |
|
169 public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) { |
|
170 if (bytes.length <= 0) { |
|
171 throw new IllegalArgumentException("bytes.length " + bytes.length |
|
172 + " must be a positive number"); |
|
173 } |
|
174 |
|
175 Bitmap bitmap = null; |
|
176 try { |
|
177 bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options); |
|
178 } catch (OutOfMemoryError e) { |
|
179 Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length |
|
180 + ", options= " + options + ") OOM!", e); |
|
181 return null; |
|
182 } |
|
183 |
|
184 if (bitmap == null) { |
|
185 Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null"); |
|
186 return null; |
|
187 } |
|
188 |
|
189 if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { |
|
190 Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned " |
|
191 + "a bitmap with dimensions " + bitmap.getWidth() |
|
192 + "x" + bitmap.getHeight()); |
|
193 return null; |
|
194 } |
|
195 |
|
196 return bitmap; |
|
197 } |
|
198 |
|
199 public static Bitmap decodeStream(InputStream inputStream) { |
|
200 try { |
|
201 return BitmapFactory.decodeStream(inputStream); |
|
202 } catch (OutOfMemoryError e) { |
|
203 Log.e(LOGTAG, "decodeStream() OOM!", e); |
|
204 return null; |
|
205 } |
|
206 } |
|
207 |
|
208 public static Bitmap decodeUrl(Uri uri) { |
|
209 return decodeUrl(uri.toString()); |
|
210 } |
|
211 |
|
212 public static Bitmap decodeUrl(String urlString) { |
|
213 URL url; |
|
214 |
|
215 try { |
|
216 url = new URL(urlString); |
|
217 } catch(MalformedURLException e) { |
|
218 Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString); |
|
219 return null; |
|
220 } |
|
221 |
|
222 return decodeUrl(url); |
|
223 } |
|
224 |
|
225 public static Bitmap decodeUrl(URL url) { |
|
226 InputStream stream = null; |
|
227 |
|
228 try { |
|
229 stream = url.openStream(); |
|
230 } catch(IOException e) { |
|
231 Log.w(LOGTAG, "decodeUrl: IOException downloading " + url); |
|
232 return null; |
|
233 } |
|
234 |
|
235 if (stream == null) { |
|
236 Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url); |
|
237 return null; |
|
238 } |
|
239 |
|
240 Bitmap bitmap = decodeStream(stream); |
|
241 |
|
242 try { |
|
243 stream.close(); |
|
244 } catch(IOException e) { |
|
245 Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e); |
|
246 } |
|
247 |
|
248 return bitmap; |
|
249 } |
|
250 |
|
251 public static Bitmap decodeResource(Context context, int id) { |
|
252 return decodeResource(context, id, null); |
|
253 } |
|
254 |
|
255 public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) { |
|
256 Resources resources = context.getResources(); |
|
257 try { |
|
258 return BitmapFactory.decodeResource(resources, id, options); |
|
259 } catch (OutOfMemoryError e) { |
|
260 Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e); |
|
261 return null; |
|
262 } |
|
263 } |
|
264 |
|
265 public static int getDominantColor(Bitmap source) { |
|
266 return getDominantColor(source, true); |
|
267 } |
|
268 |
|
269 public static int getDominantColor(Bitmap source, boolean applyThreshold) { |
|
270 if (source == null) |
|
271 return Color.argb(255,255,255,255); |
|
272 |
|
273 // Keep track of how many times a hue in a given bin appears in the image. |
|
274 // Hue values range [0 .. 360), so dividing by 10, we get 36 bins. |
|
275 int[] colorBins = new int[36]; |
|
276 |
|
277 // The bin with the most colors. Initialize to -1 to prevent accidentally |
|
278 // thinking the first bin holds the dominant color. |
|
279 int maxBin = -1; |
|
280 |
|
281 // Keep track of sum hue/saturation/value per hue bin, which we'll use to |
|
282 // compute an average to for the dominant color. |
|
283 float[] sumHue = new float[36]; |
|
284 float[] sumSat = new float[36]; |
|
285 float[] sumVal = new float[36]; |
|
286 float[] hsv = new float[3]; |
|
287 |
|
288 int height = source.getHeight(); |
|
289 int width = source.getWidth(); |
|
290 int[] pixels = new int[width * height]; |
|
291 source.getPixels(pixels, 0, width, 0, 0, width, height); |
|
292 for (int row = 0; row < height; row++) { |
|
293 for (int col = 0; col < width; col++) { |
|
294 int c = pixels[col + row * width]; |
|
295 // Ignore pixels with a certain transparency. |
|
296 if (Color.alpha(c) < 128) |
|
297 continue; |
|
298 |
|
299 Color.colorToHSV(c, hsv); |
|
300 |
|
301 // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black". |
|
302 if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f)) |
|
303 continue; |
|
304 |
|
305 // We compute the dominant color by putting colors in bins based on their hue. |
|
306 int bin = (int) Math.floor(hsv[0] / 10.0f); |
|
307 |
|
308 // Update the sum hue/saturation/value for this bin. |
|
309 sumHue[bin] = sumHue[bin] + hsv[0]; |
|
310 sumSat[bin] = sumSat[bin] + hsv[1]; |
|
311 sumVal[bin] = sumVal[bin] + hsv[2]; |
|
312 |
|
313 // Increment the number of colors in this bin. |
|
314 colorBins[bin]++; |
|
315 |
|
316 // Keep track of the bin that holds the most colors. |
|
317 if (maxBin < 0 || colorBins[bin] > colorBins[maxBin]) |
|
318 maxBin = bin; |
|
319 } |
|
320 } |
|
321 |
|
322 // maxBin may never get updated if the image holds only transparent and/or black/white pixels. |
|
323 if (maxBin < 0) |
|
324 return Color.argb(255,255,255,255); |
|
325 |
|
326 // Return a color with the average hue/saturation/value of the bin with the most colors. |
|
327 hsv[0] = sumHue[maxBin]/colorBins[maxBin]; |
|
328 hsv[1] = sumSat[maxBin]/colorBins[maxBin]; |
|
329 hsv[2] = sumVal[maxBin]/colorBins[maxBin]; |
|
330 return Color.HSVToColor(hsv); |
|
331 } |
|
332 |
|
333 /** |
|
334 * Decodes a bitmap from a Base64 data URI. |
|
335 * |
|
336 * @param dataURI a Base64-encoded data URI string |
|
337 * @return the decoded bitmap, or null if the data URI is invalid |
|
338 */ |
|
339 public static Bitmap getBitmapFromDataURI(String dataURI) { |
|
340 String base64 = dataURI.substring(dataURI.indexOf(',') + 1); |
|
341 try { |
|
342 byte[] raw = Base64.decode(base64, Base64.DEFAULT); |
|
343 return BitmapUtils.decodeByteArray(raw); |
|
344 } catch (Exception e) { |
|
345 Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e); |
|
346 } |
|
347 return null; |
|
348 } |
|
349 |
|
350 public static Bitmap getBitmapFromDrawable(Drawable drawable) { |
|
351 if (drawable instanceof BitmapDrawable) { |
|
352 return ((BitmapDrawable) drawable).getBitmap(); |
|
353 } |
|
354 |
|
355 int width = drawable.getIntrinsicWidth(); |
|
356 width = width > 0 ? width : 1; |
|
357 int height = drawable.getIntrinsicHeight(); |
|
358 height = height > 0 ? height : 1; |
|
359 |
|
360 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); |
|
361 Canvas canvas = new Canvas(bitmap); |
|
362 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); |
|
363 drawable.draw(canvas); |
|
364 |
|
365 return bitmap; |
|
366 } |
|
367 |
|
368 public static int getResource(Uri resourceUrl, int defaultIcon) { |
|
369 int icon = defaultIcon; |
|
370 |
|
371 final String scheme = resourceUrl.getScheme(); |
|
372 if ("drawable".equals(scheme)) { |
|
373 String resource = resourceUrl.getSchemeSpecificPart(); |
|
374 resource = resource.substring(resource.lastIndexOf('/') + 1); |
|
375 |
|
376 try { |
|
377 return Integer.parseInt(resource); |
|
378 } catch(NumberFormatException ex) { |
|
379 // This isn't a resource id, try looking for a string |
|
380 } |
|
381 |
|
382 try { |
|
383 final Class<R.drawable> drawableClass = R.drawable.class; |
|
384 final Field f = drawableClass.getField(resource); |
|
385 icon = f.getInt(null); |
|
386 } catch (final NoSuchFieldException e1) { |
|
387 |
|
388 // just means the resource doesn't exist for fennec. Check in Android resources |
|
389 try { |
|
390 final Class<android.R.drawable> drawableClass = android.R.drawable.class; |
|
391 final Field f = drawableClass.getField(resource); |
|
392 icon = f.getInt(null); |
|
393 } catch (final NoSuchFieldException e2) { |
|
394 // This drawable doesn't seem to exist... |
|
395 } catch(Exception e3) { |
|
396 Log.i(LOGTAG, "Exception getting drawable", e3); |
|
397 } |
|
398 |
|
399 } catch (Exception e4) { |
|
400 Log.i(LOGTAG, "Exception getting drawable", e4); |
|
401 } |
|
402 |
|
403 resourceUrl = null; |
|
404 } |
|
405 return icon; |
|
406 } |
|
407 } |
|
408 |