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