Bug 1065485: Don't decode-to-encode default favicons. r=rnewman

--HG--
rename : mobile/android/base/resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png => mobile/android/base/resources/raw/bookmarkdefaults_favicon_addons.png
rename : mobile/android/base/resources/drawable-mdpi/bookmarkdefaults_favicon_marketplace.png => mobile/android/base/resources/raw/bookmarkdefaults_favicon_marketplace.png
rename : mobile/android/base/resources/drawable-mdpi/bookmarkdefaults_favicon_support.png => mobile/android/base/resources/raw/bookmarkdefaults_favicon_support.png
This commit is contained in:
Chris Kitching 2014-09-10 16:10:34 -07:00
parent d7c79fa1a7
commit a1ac9b703d
8 changed files with 214 additions and 95 deletions

View File

@ -7,6 +7,9 @@ package org.mozilla.gecko.db;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.lang.IllegalAccessException;
import java.lang.NoSuchFieldException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -53,12 +56,18 @@ import android.net.Uri;
import android.provider.Browser; import android.provider.Browser;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.mozilla.gecko.util.IOUtils;
import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
import static org.mozilla.gecko.favicons.LoadFaviconTask.DEFAULT_FAVICON_BUFFER_SIZE;
public class LocalBrowserDB { public class LocalBrowserDB {
// Calculate these once, at initialization. isLoggable is too expensive to // Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call. // have in-line in each log call.
private static final String LOGTAG = "GeckoLocalBrowserDB"; private static final String LOGTAG = "GeckoLocalBrowserDB";
private static final Integer FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
// Sentinel value used to indicate a failure to locate an ID for a default favicon.
private static final int FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
protected static void debug(String message) { protected static void debug(String message) {
@ -185,11 +194,20 @@ public class LocalBrowserDB {
final ContentValues bookmarkValue = createBookmark(now, title, url, pos++, folderID); final ContentValues bookmarkValue = createBookmark(now, title, url, pos++, folderID);
bookmarkValues.add(bookmarkValue); bookmarkValues.add(bookmarkValue);
Bitmap icon = getDefaultFaviconFromPath(context, name); ConsumedInputStream faviconStream = getDefaultFaviconFromDrawable(context, name);
if (icon == null) { if (faviconStream == null) {
icon = getDefaultFaviconFromDrawable(context, name); faviconStream = getDefaultFaviconFromPath(context, name);
} }
if (icon == null) {
if (faviconStream == null) {
continue;
}
// In the event that truncating the buffer fails, give up and move on.
byte[] icon;
try {
icon = faviconStream.getTruncatedData();
} catch (OutOfMemoryError e) {
continue; continue;
} }
@ -204,11 +222,7 @@ public class LocalBrowserDB {
bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID); bookmarkValue.put(Bookmarks.FAVICON_ID, faviconID);
faviconValues.add(iconValue); faviconValues.add(iconValue);
} }
} catch (IllegalAccessException e) { } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException e) {
Log.wtf(LOGTAG, "Reflection failure.", e);
} catch (IllegalArgumentException e) {
Log.wtf(LOGTAG, "Reflection failure.", e);
} catch (NoSuchFieldException e) {
Log.wtf(LOGTAG, "Reflection failure.", e); Log.wtf(LOGTAG, "Reflection failure.", e);
} }
} }
@ -292,13 +306,13 @@ public class LocalBrowserDB {
try { try {
final String iconData = bookmark.getString("icon"); final String iconData = bookmark.getString("icon");
final Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconData);
byte[] icon = BitmapUtils.getBytesFromDataURI(iconData);
if (icon == null) { if (icon == null) {
continue; continue;
} }
final ContentValues iconValue = createFavicon(url, icon); final ContentValues iconValue = createFavicon(url, icon);
if (iconValue == null) { if (iconValue == null) {
continue; continue;
} }
@ -361,21 +375,11 @@ public class LocalBrowserDB {
return v; return v;
} }
private static ContentValues createFavicon(final String url, final Bitmap icon) { private static ContentValues createFavicon(final String url, final byte[] icon) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ContentValues iconValues = new ContentValues(); ContentValues iconValues = new ContentValues();
iconValues.put(Favicons.PAGE_URL, url); iconValues.put(Favicons.PAGE_URL, url);
iconValues.put(Favicons.DATA, icon);
byte[] data = null;
if (icon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
data = stream.toByteArray();
} else {
Log.w(LOGTAG, "Favicon compression failed.");
return null;
}
iconValues.put(Favicons.DATA, data);
return iconValues; return iconValues;
} }
@ -404,45 +408,53 @@ public class LocalBrowserDB {
return bookmark.getString(property); return bookmark.getString(property);
} }
private static Bitmap getDefaultFaviconFromPath(Context context, String name) { private static int getFaviconId(String name) {
Class<?> stringClass = R.string.class;
try { try {
// Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_* Class<?> drawablesClass = R.raw.class;
Field faviconField = stringClass.getField(name.replace("_title_", "_favicon_"));
if (faviconField == null) { // Look for a favicon with the id R.raw.bookmarkdefaults_favicon_*.
Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
faviconField.setAccessible(true);
return faviconField.getInt(null);
} catch (IllegalAccessException | NoSuchFieldException ex) {
Log.wtf(LOGTAG, "Reflection error fetching favicon: " + name, ex);
}
Log.e(LOGTAG, "Failed to find favicon resource ID for " + name);
return FAVICON_ID_NOT_FOUND;
}
/**
* Load a favicon from the omnijar.
* @return A ConsumedInputStream containing the bytes loaded from omnijar. This must be a format
* compatible with the favicon decoder (most probably a PNG or ICO file).
*/
private static ConsumedInputStream getDefaultFaviconFromPath(Context context, String name) {
int faviconId = getFaviconId(name);
if (faviconId == FAVICON_ID_NOT_FOUND) {
return null; return null;
} }
int faviconId = faviconField.getInt(null);
String path = context.getString(faviconId); String path = context.getString(faviconId);
String apkPath = context.getPackageResourcePath(); String apkPath = context.getPackageResourcePath();
File apkFile = new File(apkPath); File apkFile = new File(apkPath);
String bitmapPath = "jar:jar:" + apkFile.toURI() + "!/" + AppConstants.OMNIJAR_NAME + "!/" + path; String bitmapPath = "jar:jar:" + apkFile.toURI() + "!/" + AppConstants.OMNIJAR_NAME + "!/" + path;
return GeckoJarReader.getBitmap(context.getResources(), bitmapPath);
} catch (java.lang.IllegalAccessException ex) { InputStream iStream = GeckoJarReader.getStream(bitmapPath);
Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
} catch (java.lang.NoSuchFieldException ex) { return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE);
// If the field does not exist, that means we intend to load via a drawable.
} }
private static ConsumedInputStream getDefaultFaviconFromDrawable(Context context, String name) {
int faviconId = getFaviconId(name);
if (faviconId == FAVICON_ID_NOT_FOUND) {
return null; return null;
} }
private static Bitmap getDefaultFaviconFromDrawable(Context context, String name) { InputStream iStream = context.getResources().openRawResource(faviconId);
Class<?> drawablesClass = R.drawable.class; return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE);
try {
// Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
if (faviconField == null) {
return null;
}
int faviconId = faviconField.getInt(null);
return BitmapUtils.decodeResource(context, faviconId);
} catch (java.lang.IllegalAccessException ex) {
Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
} catch (java.lang.NoSuchFieldException ex) {
Log.wtf(LOGTAG, "No field, and presumably no drawable, for " + name);
}
return null;
} }
// Invalidate cached data // Invalidate cached data

View File

@ -20,6 +20,7 @@ import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.decoders.FaviconDecoder; import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.GeckoJarReader; import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.ThreadUtils;
import java.io.IOException; import java.io.IOException;
@ -32,6 +33,8 @@ import java.util.LinkedList;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
/** /**
* Class representing the asynchronous task to load a Favicon which is not currently in the in-memory * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
* cache. * cache.
@ -49,7 +52,7 @@ public class LoadFaviconTask {
private static final int MAX_REDIRECTS_TO_FOLLOW = 5; private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
// The default size of the buffer to use for downloading Favicons in the event no size is given // The default size of the buffer to use for downloading Favicons in the event no size is given
// by the server. // by the server.
private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000; public static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0); private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
private final Context context; private final Context context;
@ -281,42 +284,17 @@ public class LoadFaviconTask {
bufferSize = DEFAULT_FAVICON_BUFFER_SIZE; bufferSize = DEFAULT_FAVICON_BUFFER_SIZE;
} }
// Allocate a buffer to hold the raw favicon data downloaded. // Read the InputStream into a byte[].
byte[] buffer = new byte[bufferSize]; ConsumedInputStream result = IOUtils.readFully(entity.getContent(), bufferSize);
if (result == null) {
// The offset of the start of the buffer's free space. return null;
int bPointer = 0;
// The quantity of bytes the last call to read yielded.
int lastRead = 0;
InputStream contentStream = entity.getContent();
try {
// Fully read the entity into the buffer - decoding of streams is not supported
// (and questionably pointful - what would one do with a half-decoded Favicon?)
while (lastRead != -1) {
// Read as many bytes as are currently available into the buffer.
lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer);
bPointer += lastRead;
// If buffer has overflowed, double its size and carry on.
if (bPointer == buffer.length) {
bufferSize *= 2;
byte[] newBuffer = new byte[bufferSize];
// Copy the contents of the old buffer into the new buffer.
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
buffer = newBuffer;
}
}
} finally {
contentStream.close();
} }
// Having downloaded the image, decode it. // Having downloaded the image, decode it.
return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1); return FaviconDecoder.decodeFavicon(result.getData(), 0, result.consumedLength);
} }
// LoadFavicon tasks are performed on a unique background executor thread // LoadFaviconTasks are performed on a unique background executor thread
// to avoid network blocking. // to avoid network blocking.
public final void execute() { public final void execute() {
try { try {

View File

@ -341,16 +341,33 @@ public final class BitmapUtils {
return null; return null;
} }
final String base64 = dataURI.substring(dataURI.indexOf(',') + 1); byte[] raw = getBytesFromDataURI(dataURI);
try { if (raw == null || raw.length == 0) {
byte[] raw = Base64.decode(base64, Base64.DEFAULT);
return BitmapUtils.decodeByteArray(raw);
} catch (Exception e) {
Log.e(LOGTAG, "exception decoding bitmap from data URI: " + dataURI, e);
}
return null; return null;
} }
return decodeByteArray(raw);
}
/**
* Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
* base64 string.
*/
public static byte[] getBytesFromBase64(String base64) {
try {
return Base64.decode(base64, Base64.DEFAULT);
} catch (Exception e) {
Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
}
return null;
}
public static byte[] getBytesFromDataURI(String dataURI) {
final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
return getBytesFromBase64(base64);
}
public static Bitmap getBitmapFromDrawable(Drawable drawable) { public static Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable instanceof BitmapDrawable) { if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap(); return ((BitmapDrawable) drawable).getBitmap();

View File

@ -60,6 +60,7 @@ gujar.sources += [
'util/HardwareUtils.java', 'util/HardwareUtils.java',
'util/INIParser.java', 'util/INIParser.java',
'util/INISection.java', 'util/INISection.java',
'util/IOUtils.java',
'util/JSONUtils.java', 'util/JSONUtils.java',
'util/MenuUtils.java', 'util/MenuUtils.java',
'util/NativeEventListener.java', 'util/NativeEventListener.java',

View File

@ -0,0 +1,111 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.util;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* Static helper class containing useful methods for manipulating IO objects.
*/
public class IOUtils {
private static final String LOGTAG = "GeckoIOUtils";
/**
* Represents the result of consuming an input stream, holding the returned data as well
* as the length of the data returned.
* The byte[] is not guaranteed to be trimmed to the size of the data acquired from the stream:
* hence the need for the length field. This strategy avoids the need to copy the data into a
* trimmed buffer after consumption.
*/
public static class ConsumedInputStream {
public final int consumedLength;
// Only reassigned in getTruncatedData.
private byte[] consumedData;
public ConsumedInputStream(int consumedLength, byte[] consumedData) {
this.consumedLength = consumedLength;
this.consumedData = consumedData;
}
/**
* Get the data trimmed to the length of the actual payload read, caching the result.
*/
public byte[] getTruncatedData() {
if (consumedData.length == consumedLength) {
return consumedData;
}
consumedData = truncateBytes(consumedData, consumedLength);
return consumedData;
}
public byte[] getData() {
return consumedData;
}
}
/**
* Fully read an InputStream into a byte array.
* @param iStream the InputStream to consume.
* @param bufferSize The initial size of the buffer to allocate. It will be grown as
* needed, but if the caller knows something about the InputStream then
* passing a good value here can improve performance.
*/
public static ConsumedInputStream readFully(InputStream iStream, int bufferSize) {
// Allocate a buffer to hold the raw data downloaded.
byte[] buffer = new byte[bufferSize];
// The offset of the start of the buffer's free space.
int bPointer = 0;
// The quantity of bytes the last call to read yielded.
int lastRead = 0;
try {
// Fully read the data into the buffer.
while (lastRead != -1) {
// Read as many bytes as are currently available into the buffer.
lastRead = iStream.read(buffer, bPointer, buffer.length - bPointer);
bPointer += lastRead;
// If buffer has overflowed, double its size and carry on.
if (bPointer == buffer.length) {
bufferSize *= 2;
byte[] newBuffer = new byte[bufferSize];
// Copy the contents of the old buffer into the new buffer.
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
buffer = newBuffer;
}
}
return new ConsumedInputStream(bPointer + 1, buffer);
} catch (IOException e) {
Log.e(LOGTAG, "Error consuming input stream.", e);
} finally {
try {
iStream.close();
} catch (IOException e) {
Log.e(LOGTAG, "Error closing input stream.", e);
}
}
return null;
}
/**
* Truncate a given byte[] to a given length. Returns a new byte[] with the first length many
* bytes of the input.
*/
public static byte[] truncateBytes(byte[] bytes, int length) {
byte[] newBytes = new byte[length];
System.arraycopy(bytes, 0, newBytes, 0, length);
return newBytes;
}
}