michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.lang.reflect.Field; michael@0: import java.net.MalformedURLException; michael@0: import java.net.URL; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.util.GeckoJarReader; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: import org.mozilla.gecko.Tab; michael@0: import org.mozilla.gecko.Tabs; michael@0: import org.mozilla.gecko.ThumbnailHelper; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.BitmapFactory; michael@0: import android.graphics.Canvas; michael@0: import android.graphics.Color; michael@0: import android.graphics.drawable.BitmapDrawable; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.net.Uri; michael@0: import android.text.TextUtils; michael@0: import android.util.Base64; michael@0: import android.util.Log; michael@0: michael@0: public final class BitmapUtils { michael@0: private static final String LOGTAG = "GeckoBitmapUtils"; michael@0: michael@0: private BitmapUtils() {} michael@0: michael@0: public interface BitmapLoader { michael@0: public void onBitmapFound(Drawable d); michael@0: } michael@0: michael@0: private static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) { michael@0: if (ThreadUtils.isOnUiThread()) { michael@0: loader.onBitmapFound(d); michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: loader.onBitmapFound(d); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Attempts to find a drawable associated with a given string, using its URI scheme to determine michael@0: * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and michael@0: * will be called with `null` if no drawable is found. michael@0: * michael@0: * The BitmapLoader `onBitmapFound` method always runs on the UI thread. michael@0: */ michael@0: public static void getDrawable(final Context context, final String data, final BitmapLoader loader) { michael@0: if (TextUtils.isEmpty(data)) { michael@0: runOnBitmapFoundOnUiThread(loader, null); michael@0: return; michael@0: } michael@0: michael@0: if (data.startsWith("data")) { michael@0: final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data)); michael@0: runOnBitmapFoundOnUiThread(loader, d); michael@0: return; michael@0: } michael@0: michael@0: if (data.startsWith("thumbnail:")) { michael@0: getThumbnailDrawable(context, data, loader); michael@0: return; michael@0: } michael@0: michael@0: if (data.startsWith("jar:") || data.startsWith("file://")) { michael@0: (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: public Drawable doInBackground(Void... params) { michael@0: try { michael@0: if (data.startsWith("jar:jar")) { michael@0: return GeckoJarReader.getBitmapDrawable(context.getResources(), data); michael@0: } michael@0: michael@0: // Don't attempt to validate the JAR signature when loading an add-on icon michael@0: if (data.startsWith("jar:file")) { michael@0: return GeckoJarReader.getBitmapDrawable(context.getResources(), Uri.decode(data)); michael@0: } michael@0: michael@0: final URL url = new URL(data); michael@0: final InputStream is = (InputStream) url.getContent(); michael@0: try { michael@0: return Drawable.createFromStream(is, "src"); michael@0: } finally { michael@0: is.close(); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.w(LOGTAG, "Unable to set icon", e); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: @Override michael@0: public void onPostExecute(Drawable drawable) { michael@0: loader.onBitmapFound(drawable); michael@0: } michael@0: }).execute(); michael@0: return; michael@0: } michael@0: michael@0: if (data.startsWith("-moz-icon://")) { michael@0: final Uri imageUri = Uri.parse(data); michael@0: final String ssp = imageUri.getSchemeSpecificPart(); michael@0: final String resource = ssp.substring(ssp.lastIndexOf('/') + 1); michael@0: michael@0: try { michael@0: final Drawable d = context.getPackageManager().getApplicationIcon(resource); michael@0: runOnBitmapFoundOnUiThread(loader, d); michael@0: } catch(Exception ex) { } michael@0: michael@0: return; michael@0: } michael@0: michael@0: if (data.startsWith("drawable://")) { michael@0: final Uri imageUri = Uri.parse(data); michael@0: final int id = getResource(imageUri, R.drawable.ic_status_logo); michael@0: final Drawable d = context.getResources().getDrawable(id); michael@0: michael@0: runOnBitmapFoundOnUiThread(loader, d); michael@0: return; michael@0: } michael@0: michael@0: runOnBitmapFoundOnUiThread(loader, null); michael@0: } michael@0: michael@0: public static void getThumbnailDrawable(final Context context, final String data, final BitmapLoader loader) { michael@0: int id = Integer.parseInt(data.substring(10), 10); michael@0: final Tab tab = Tabs.getInstance().getTab(id); michael@0: runOnBitmapFoundOnUiThread(loader, tab.getThumbnail()); michael@0: Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() { michael@0: public void onTabChanged(Tab t, Tabs.TabEvents msg, Object data) { michael@0: if (tab == t && msg == Tabs.TabEvents.THUMBNAIL) { michael@0: Tabs.unregisterOnTabsChangedListener(this); michael@0: runOnBitmapFoundOnUiThread(loader, t.getThumbnail()); michael@0: } michael@0: } michael@0: }); michael@0: ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab); michael@0: } michael@0: michael@0: public static Bitmap decodeByteArray(byte[] bytes) { michael@0: return decodeByteArray(bytes, null); michael@0: } michael@0: michael@0: public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) { michael@0: return decodeByteArray(bytes, 0, bytes.length, options); michael@0: } michael@0: michael@0: public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) { michael@0: return decodeByteArray(bytes, offset, length, null); michael@0: } michael@0: michael@0: public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) { michael@0: if (bytes.length <= 0) { michael@0: throw new IllegalArgumentException("bytes.length " + bytes.length michael@0: + " must be a positive number"); michael@0: } michael@0: michael@0: Bitmap bitmap = null; michael@0: try { michael@0: bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options); michael@0: } catch (OutOfMemoryError e) { michael@0: Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length michael@0: + ", options= " + options + ") OOM!", e); michael@0: return null; michael@0: } michael@0: michael@0: if (bitmap == null) { michael@0: Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null"); michael@0: return null; michael@0: } michael@0: michael@0: if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { michael@0: Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned " michael@0: + "a bitmap with dimensions " + bitmap.getWidth() michael@0: + "x" + bitmap.getHeight()); michael@0: return null; michael@0: } michael@0: michael@0: return bitmap; michael@0: } michael@0: michael@0: public static Bitmap decodeStream(InputStream inputStream) { michael@0: try { michael@0: return BitmapFactory.decodeStream(inputStream); michael@0: } catch (OutOfMemoryError e) { michael@0: Log.e(LOGTAG, "decodeStream() OOM!", e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public static Bitmap decodeUrl(Uri uri) { michael@0: return decodeUrl(uri.toString()); michael@0: } michael@0: michael@0: public static Bitmap decodeUrl(String urlString) { michael@0: URL url; michael@0: michael@0: try { michael@0: url = new URL(urlString); michael@0: } catch(MalformedURLException e) { michael@0: Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString); michael@0: return null; michael@0: } michael@0: michael@0: return decodeUrl(url); michael@0: } michael@0: michael@0: public static Bitmap decodeUrl(URL url) { michael@0: InputStream stream = null; michael@0: michael@0: try { michael@0: stream = url.openStream(); michael@0: } catch(IOException e) { michael@0: Log.w(LOGTAG, "decodeUrl: IOException downloading " + url); michael@0: return null; michael@0: } michael@0: michael@0: if (stream == null) { michael@0: Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url); michael@0: return null; michael@0: } michael@0: michael@0: Bitmap bitmap = decodeStream(stream); michael@0: michael@0: try { michael@0: stream.close(); michael@0: } catch(IOException e) { michael@0: Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e); michael@0: } michael@0: michael@0: return bitmap; michael@0: } michael@0: michael@0: public static Bitmap decodeResource(Context context, int id) { michael@0: return decodeResource(context, id, null); michael@0: } michael@0: michael@0: public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) { michael@0: Resources resources = context.getResources(); michael@0: try { michael@0: return BitmapFactory.decodeResource(resources, id, options); michael@0: } catch (OutOfMemoryError e) { michael@0: Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public static int getDominantColor(Bitmap source) { michael@0: return getDominantColor(source, true); michael@0: } michael@0: michael@0: public static int getDominantColor(Bitmap source, boolean applyThreshold) { michael@0: if (source == null) michael@0: return Color.argb(255,255,255,255); michael@0: michael@0: // Keep track of how many times a hue in a given bin appears in the image. michael@0: // Hue values range [0 .. 360), so dividing by 10, we get 36 bins. michael@0: int[] colorBins = new int[36]; michael@0: michael@0: // The bin with the most colors. Initialize to -1 to prevent accidentally michael@0: // thinking the first bin holds the dominant color. michael@0: int maxBin = -1; michael@0: michael@0: // Keep track of sum hue/saturation/value per hue bin, which we'll use to michael@0: // compute an average to for the dominant color. michael@0: float[] sumHue = new float[36]; michael@0: float[] sumSat = new float[36]; michael@0: float[] sumVal = new float[36]; michael@0: float[] hsv = new float[3]; michael@0: michael@0: int height = source.getHeight(); michael@0: int width = source.getWidth(); michael@0: int[] pixels = new int[width * height]; michael@0: source.getPixels(pixels, 0, width, 0, 0, width, height); michael@0: for (int row = 0; row < height; row++) { michael@0: for (int col = 0; col < width; col++) { michael@0: int c = pixels[col + row * width]; michael@0: // Ignore pixels with a certain transparency. michael@0: if (Color.alpha(c) < 128) michael@0: continue; michael@0: michael@0: Color.colorToHSV(c, hsv); michael@0: michael@0: // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black". michael@0: if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f)) michael@0: continue; michael@0: michael@0: // We compute the dominant color by putting colors in bins based on their hue. michael@0: int bin = (int) Math.floor(hsv[0] / 10.0f); michael@0: michael@0: // Update the sum hue/saturation/value for this bin. michael@0: sumHue[bin] = sumHue[bin] + hsv[0]; michael@0: sumSat[bin] = sumSat[bin] + hsv[1]; michael@0: sumVal[bin] = sumVal[bin] + hsv[2]; michael@0: michael@0: // Increment the number of colors in this bin. michael@0: colorBins[bin]++; michael@0: michael@0: // Keep track of the bin that holds the most colors. michael@0: if (maxBin < 0 || colorBins[bin] > colorBins[maxBin]) michael@0: maxBin = bin; michael@0: } michael@0: } michael@0: michael@0: // maxBin may never get updated if the image holds only transparent and/or black/white pixels. michael@0: if (maxBin < 0) michael@0: return Color.argb(255,255,255,255); michael@0: michael@0: // Return a color with the average hue/saturation/value of the bin with the most colors. michael@0: hsv[0] = sumHue[maxBin]/colorBins[maxBin]; michael@0: hsv[1] = sumSat[maxBin]/colorBins[maxBin]; michael@0: hsv[2] = sumVal[maxBin]/colorBins[maxBin]; michael@0: return Color.HSVToColor(hsv); michael@0: } michael@0: michael@0: /** michael@0: * Decodes a bitmap from a Base64 data URI. michael@0: * michael@0: * @param dataURI a Base64-encoded data URI string michael@0: * @return the decoded bitmap, or null if the data URI is invalid michael@0: */ michael@0: public static Bitmap getBitmapFromDataURI(String dataURI) { michael@0: String base64 = dataURI.substring(dataURI.indexOf(',') + 1); michael@0: try { michael@0: byte[] raw = Base64.decode(base64, Base64.DEFAULT); michael@0: return BitmapUtils.decodeByteArray(raw); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: public static Bitmap getBitmapFromDrawable(Drawable drawable) { michael@0: if (drawable instanceof BitmapDrawable) { michael@0: return ((BitmapDrawable) drawable).getBitmap(); michael@0: } michael@0: michael@0: int width = drawable.getIntrinsicWidth(); michael@0: width = width > 0 ? width : 1; michael@0: int height = drawable.getIntrinsicHeight(); michael@0: height = height > 0 ? height : 1; michael@0: michael@0: Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); michael@0: Canvas canvas = new Canvas(bitmap); michael@0: drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); michael@0: drawable.draw(canvas); michael@0: michael@0: return bitmap; michael@0: } michael@0: michael@0: public static int getResource(Uri resourceUrl, int defaultIcon) { michael@0: int icon = defaultIcon; michael@0: michael@0: final String scheme = resourceUrl.getScheme(); michael@0: if ("drawable".equals(scheme)) { michael@0: String resource = resourceUrl.getSchemeSpecificPart(); michael@0: resource = resource.substring(resource.lastIndexOf('/') + 1); michael@0: michael@0: try { michael@0: return Integer.parseInt(resource); michael@0: } catch(NumberFormatException ex) { michael@0: // This isn't a resource id, try looking for a string michael@0: } michael@0: michael@0: try { michael@0: final Class drawableClass = R.drawable.class; michael@0: final Field f = drawableClass.getField(resource); michael@0: icon = f.getInt(null); michael@0: } catch (final NoSuchFieldException e1) { michael@0: michael@0: // just means the resource doesn't exist for fennec. Check in Android resources michael@0: try { michael@0: final Class drawableClass = android.R.drawable.class; michael@0: final Field f = drawableClass.getField(resource); michael@0: icon = f.getInt(null); michael@0: } catch (final NoSuchFieldException e2) { michael@0: // This drawable doesn't seem to exist... michael@0: } catch(Exception e3) { michael@0: Log.i(LOGTAG, "Exception getting drawable", e3); michael@0: } michael@0: michael@0: } catch (Exception e4) { michael@0: Log.i(LOGTAG, "Exception getting drawable", e4); michael@0: } michael@0: michael@0: resourceUrl = null; michael@0: } michael@0: return icon; michael@0: } michael@0: } michael@0: