author | Chris Kitching <chriskitching@linux.com> |
Sat, 18 Jan 2014 03:24:28 +0000 | |
changeset 164171 | 1c402a47da513701a2959972bf8852eab8809b11 |
parent 164170 | b551eec92a42773c315edfc178e39037d9ea7fda |
child 164172 | 3da20e4c20ec517d07a004fa23879b2726e1757a |
push id | 26027 |
push user | ttaubert@mozilla.com |
push date | Sun, 19 Jan 2014 09:43:10 +0000 |
treeherder | mozilla-central@f038d28c0349 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rnewman |
bugs | 748100 |
milestone | 29.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -7,16 +7,17 @@ package org.mozilla.gecko; import org.mozilla.gecko.animation.PropertyAnimator; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.favicons.OnFaviconLoadedListener; import org.mozilla.gecko.favicons.LoadFaviconTask; +import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.GeckoLayerClient; import org.mozilla.gecko.gfx.ImmutableViewportMetrics; import org.mozilla.gecko.gfx.LayerMarginsAnimator; import org.mozilla.gecko.health.BrowserHealthRecorder; import org.mozilla.gecko.health.BrowserHealthReporter; import org.mozilla.gecko.home.BrowserSearch; import org.mozilla.gecko.home.HomePager; @@ -590,16 +591,19 @@ abstract public class BrowserApp extends @Override public boolean isObserver() { // We want to be notified of changes to be able to switch mode // without restarting. return true; } }); + + // Set the maximum bits-per-pixel the favicon system cares about. + IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth()); } @Override public void onBackPressed() { if (getSupportFragmentManager().getBackStackEntryCount() > 0) { super.onBackPressed(); return; } @@ -783,20 +787,20 @@ abstract public class BrowserApp extends final String url = tab.getURL(); final String title = tab.getDisplayTitle(); if (url == null || title == null) { return true; } final OnFaviconLoadedListener listener = new GeckoAppShell.CreateShortcutFaviconLoadedListener(url, title); Favicons.getSizedFavicon(url, - tab.getFaviconURL(), - Integer.MAX_VALUE, - LoadFaviconTask.FLAG_PERSIST, - listener); + tab.getFaviconURL(), + Integer.MAX_VALUE, + LoadFaviconTask.FLAG_PERSIST, + listener); return true; } return false; } @Override public void setAccessibilityEnabled(boolean enabled) {
--- a/mobile/android/base/db/BrowserDB.java +++ b/mobile/android/base/db/BrowserDB.java @@ -2,16 +2,17 @@ * 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.db; import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.ExpirePriority; +import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; import org.mozilla.gecko.mozglue.RobocopTarget; import android.content.ContentResolver; import android.database.ContentObserver; import android.database.Cursor; import android.database.CursorWrapper; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -89,21 +90,21 @@ public class BrowserDB { @RobocopTarget public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword); public void addReadingListItem(ContentResolver cr, String title, String uri); public void removeReadingListItemWithURL(ContentResolver cr, String uri); - public Bitmap getFaviconForUrl(ContentResolver cr, String uri); + public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String uri); public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url); - public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri); + public void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri); public void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail); @RobocopTarget public byte[] getThumbnailForUrl(ContentResolver cr, String uri); public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls); @@ -252,26 +253,26 @@ public class BrowserDB { public static void addReadingListItem(ContentResolver cr, String title, String uri) { sDb.addReadingListItem(cr, title, uri); } public static void removeReadingListItemWithURL(ContentResolver cr, String uri) { sDb.removeReadingListItemWithURL(cr, uri); } - public static Bitmap getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) { + public static LoadFaviconResult getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) { return sDb.getFaviconForUrl(cr, faviconURL); } public static String getFaviconUrlForHistoryUrl(ContentResolver cr, String url) { return sDb.getFaviconUrlForHistoryUrl(cr, url); } - public static void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri) { - sDb.updateFaviconForUrl(cr, pageUri, favicon, faviconUri); + public static void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri) { + sDb.updateFaviconForUrl(cr, pageUri, encodedFavicon, faviconUri); } public static void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail) { sDb.updateThumbnailForUrl(cr, uri, thumbnail); } @RobocopTarget public static byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
--- a/mobile/android/base/db/LocalBrowserDB.java +++ b/mobile/android/base/db/LocalBrowserDB.java @@ -10,16 +10,18 @@ import org.mozilla.gecko.db.BrowserContr import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserContract.ExpirePriority; import org.mozilla.gecko.db.BrowserContract.FaviconColumns; import org.mozilla.gecko.db.BrowserContract.Favicons; import org.mozilla.gecko.db.BrowserContract.History; import org.mozilla.gecko.db.BrowserContract.SyncColumns; import org.mozilla.gecko.db.BrowserContract.Thumbnails; import org.mozilla.gecko.db.BrowserContract.URLColumns; +import org.mozilla.gecko.favicons.decoders.FaviconDecoder; +import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; import org.mozilla.gecko.gfx.BitmapUtils; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.database.ContentObserver; import android.database.Cursor; import android.database.CursorWrapper; @@ -703,17 +705,17 @@ public class LocalBrowserDB implements B /** * Get the favicon from the database, if any, associated with the given favicon URL. (That is, * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.) * @param cr The ContentResolver to use. * @param faviconURL The URL of the favicon to fetch from the database. * @return The decoded Bitmap from the database, if any. null if none is stored. */ @Override - public Bitmap getFaviconForUrl(ContentResolver cr, String faviconURL) { + public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) { Cursor c = null; byte[] b = null; try { c = cr.query(mFaviconsUriWithProfile, new String[] { Favicons.DATA }, Favicons.URL + " = ?", new String[] { faviconURL }, @@ -730,17 +732,17 @@ public class LocalBrowserDB implements B c.close(); } } if (b == null) { return null; } - return BitmapUtils.decodeByteArray(b); + return FaviconDecoder.decodeFavicon(b); } @Override public String getFaviconUrlForHistoryUrl(ContentResolver cr, String uri) { Cursor c = null; try { c = cr.query(mHistoryUriWithProfile, @@ -756,29 +758,21 @@ public class LocalBrowserDB implements B c.close(); } return null; } @Override public void updateFaviconForUrl(ContentResolver cr, String pageUri, - Bitmap favicon, String faviconUri) { + byte[] encodedFavicon, String faviconUri) { ContentValues values = new ContentValues(); values.put(Favicons.URL, faviconUri); values.put(Favicons.PAGE_URL, pageUri); - - byte[] data = null; - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) { - data = stream.toByteArray(); - } else { - Log.w(LOGTAG, "Favicon compression failed."); - } - values.put(Favicons.DATA, data); + values.put(Favicons.DATA, encodedFavicon); // Update or insert Uri faviconsUri = getAllFaviconsUri().buildUpon(). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); cr.update(faviconsUri, values, Favicons.URL + " = ?",
--- a/mobile/android/base/favicons/Favicons.java +++ b/mobile/android/base/favicons/Favicons.java @@ -54,16 +54,19 @@ public class Favicons { protected static Context sContext; // The default Favicon to show if no other can be found. public static Bitmap sDefaultFavicon; // The density-adjusted default Favicon dimensions. public static int sDefaultFaviconSize; + // The density-adjusted maximum Favicon dimensions. + public static int sLargestFaviconSize; + private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>()); // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when // doing so is not necessary. private static final NonEvictingLruCache<String, String> sPageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE); public static String getFaviconURLForPageURLFromCache(String pageURL) { return sPageURLMappings.get(pageURL); @@ -285,20 +288,32 @@ public class Favicons { return taskId; } public static void putFaviconInMemCache(String pageUrl, Bitmap image) { sFaviconsCache.putSingleFavicon(pageUrl, image); } + /** + * Adds the bitmaps given by the specified iterator to the cache associated with the url given. + * Future requests for images will be able to select the least larger image than the target + * size from this new set of images. + * + * @param pageUrl The URL to associate the new favicons with. + * @param images An iterator over the new favicons to put in the cache. + */ public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) { sFaviconsCache.putFavicons(pageUrl, images, permanently); } + public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) { + putFaviconsInMemCache(pageUrl, images, false); + } + public static void clearMemCache() { sFaviconsCache.evictAll(); sPageURLMappings.evictAll(); } public static void putFaviconInFailedCache(String faviconURL) { sFaviconsCache.putFailed(faviconURL); } @@ -361,17 +376,21 @@ public class Favicons { // Decode the default Favicon ready for use. sDefaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon); if (sDefaultFavicon == null) { throw new Exception("Null default favicon was returned from the resources system!"); } sDefaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg); - sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, res.getDimensionPixelSize(R.dimen.favicon_largest_interesting_size)); + + // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are + // downscaled to this size or discarded. + sLargestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size); + sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, sLargestFaviconSize); // Initialize page mappings for each of our special pages. for (String url : AboutPages.getDefaultIconPages()) { sPageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL); } // Load and cache the built-in favicon in each of its sizes. // TODO: don't open the zip twice!
--- a/mobile/android/base/favicons/LoadFaviconTask.java +++ b/mobile/android/base/favicons/LoadFaviconTask.java @@ -10,34 +10,33 @@ import android.graphics.Bitmap; import android.net.http.AndroidHttpClient; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.BufferedHttpEntity; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.favicons.decoders.FaviconDecoder; +import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; import org.mozilla.gecko.util.GeckoJarReader; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; import static org.mozilla.gecko.favicons.Favicons.sContext; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; /** * Class representing the asynchronous task to load a Favicon which is not currently in the in-memory * cache. * The implementation initially tries to get the Favicon from the database. Upon failure, the icon * is loaded from the internet. */ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> { @@ -45,16 +44,19 @@ public class LoadFaviconTask extends UiA // Access to this map needs to be synchronized prevent multiple jobs loading the same favicon // from executing concurrently. private static final HashMap<String, LoadFaviconTask> loadsInFlight = new HashMap<String, LoadFaviconTask>(); public static final int FLAG_PERSIST = 1; public static final int FLAG_SCALE = 2; 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 + // by the server. + private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000; private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0); private int mId; private String mPageUrl; private String mFaviconUrl; private OnFaviconLoadedListener mListener; private int mFlags; @@ -83,29 +85,29 @@ public class LoadFaviconTask extends UiA mFaviconUrl = faviconUrl; mListener = aListener; mFlags = flags; mTargetWidth = targetSize; mOnlyFromLocal = fromLocal; } // Runs in background thread - private Bitmap loadFaviconFromDB() { + private LoadFaviconResult loadFaviconFromDb() { ContentResolver resolver = sContext.getContentResolver(); return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl); } // Runs in background thread - private void saveFaviconToDb(final Bitmap favicon) { + private void saveFaviconToDb(final byte[] encodedFavicon) { if ((mFlags & FLAG_PERSIST) == 0) { return; } ContentResolver resolver = sContext.getContentResolver(); - BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl); + BrowserDB.updateFaviconForUrl(resolver, mPageUrl, encodedFavicon, mFaviconUrl); } /** * Helper method for trying the download request to grab a Favicon. * @param faviconURI URL of Favicon to try and download * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise. */ private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException { @@ -177,59 +179,106 @@ public class LoadFaviconTask extends UiA return null; } } return null; } // Runs in background thread. // Does not attempt to fetch from JARs. - private Bitmap downloadFavicon(URI targetFaviconURI) { + private LoadFaviconResult downloadFavicon(URI targetFaviconURI) { if (targetFaviconURI == null) { return null; } // Only get favicons for HTTP/HTTPS. String scheme = targetFaviconURI.getScheme(); if (!"http".equals(scheme) && !"https".equals(scheme)) { return null; } - Bitmap image = null; - - // skia decoder sometimes returns null; workaround is to use BufferedHttpEntity - // http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot - try { - // Try the URL we were given. - HttpResponse response = tryDownload(targetFaviconURI); - if (response == null) { - return null; - } + LoadFaviconResult result = null; - HttpEntity entity = response.getEntity(); - if (entity == null) { - return null; - } - - BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity); - InputStream contentStream = null; - try { - contentStream = bufferedEntity.getContent(); - image = BitmapUtils.decodeStream(contentStream); - contentStream.close(); - } finally { - if (contentStream != null) { - contentStream.close(); - } - } + try { + result = downloadAndDecodeImage(targetFaviconURI); } catch (Exception e) { Log.e(LOGTAG, "Error reading favicon", e); } - return image; + return result; + } + + /** + * Download the Favicon from the given URL and pass it to the decoder function. + * + * @param targetFaviconURL URL of the favicon to download. + * @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or + * null if no or corrupt data ware received. + * @throws IOException If attempts to fully read the stream result in such an exception, such as + * in the event of a transient connection failure. + * @throws URISyntaxException If the underlying call to tryDownload retries and raises such an + * exception trying a fallback URL. + */ + private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException { + // Try the URL we were given. + HttpResponse response = tryDownload(targetFaviconURL); + if (response == null) { + return null; + } + + HttpEntity entity = response.getEntity(); + if (entity == null) { + return null; + } + + // This may not be provided, but if it is, it's useful. + final long entityReportedLength = entity.getContentLength(); + int bufferSize; + if (entityReportedLength > 0) { + // The size was reported and sane, so let's use that. + // Integer overflow should not be a problem for Favicon sizes... + bufferSize = (int) entityReportedLength + 1; + } else { + // No declared size, so guess and reallocate later if it turns out to be too small. + bufferSize = DEFAULT_FAVICON_BUFFER_SIZE; + } + + // Allocate a buffer to hold the raw favicon 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; + 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. + return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1); } @Override protected Bitmap doInBackground(Void... unused) { if (isCancelled()) { return null; } @@ -294,94 +343,113 @@ public class LoadFaviconTask extends UiA // chain onto the same parent task. loadsInFlight.put(mFaviconUrl, this); } if (isCancelled()) { return null; } - image = loadFaviconFromDB(); - if (imageIsValid(image)) { - return image; + // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null. + LoadFaviconResult loadedBitmaps = loadFaviconFromDb(); + if (loadedBitmaps != null) { + return pushToCacheAndGetResult(loadedBitmaps); } if (mOnlyFromLocal || isCancelled()) { return null; } // Let's see if it's in a JAR. image = fetchJARFavicon(mFaviconUrl); - if (image != null) { + if (imageIsValid(image)) { // We don't want to put this into the DB. + Favicons.putFaviconInMemCache(mFaviconUrl, image); return image; } try { - image = downloadFavicon(new URI(mFaviconUrl)); + loadedBitmaps = downloadFavicon(new URI(mFaviconUrl)); } catch (URISyntaxException e) { Log.e(LOGTAG, "The provided favicon URL is not valid"); return null; } catch (Exception e) { Log.e(LOGTAG, "Couldn't download favicon.", e); } - if (imageIsValid(image)) { - saveFaviconToDb(image); - return image; + if (loadedBitmaps != null) { + saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); + return pushToCacheAndGetResult(loadedBitmaps); } if (isUsingDefaultURL) { Favicons.putFaviconInFailedCache(mFaviconUrl); return null; } + if (isCancelled()) { + return null; + } + // If we're not already trying the default URL, try it now. final String guessed = Favicons.guessDefaultFaviconURL(mPageUrl); if (guessed == null) { Favicons.putFaviconInFailedCache(mFaviconUrl); return null; } image = fetchJARFavicon(guessed); if (imageIsValid(image)) { // We don't want to put this into the DB. + Favicons.putFaviconInMemCache(mFaviconUrl, image); return image; } try { - image = downloadFavicon(new URI(guessed)); + loadedBitmaps = downloadFavicon(new URI(guessed)); } catch (Exception e) { // Not interesting. It was an educated guess, anyway. return null; } - if (imageIsValid(image)) { - saveFaviconToDb(image); - return image; + if (loadedBitmaps != null) { + saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage()); + return pushToCacheAndGetResult(loadedBitmaps); } return null; } + /** + * Helper method to put the result of a favicon load into the memory cache and then query the + * cache for the particular bitmap we want for this request. + * This call is certain to succeed, provided there was enough memory to decode this favicon. + * + * @param loadedBitmaps LoadFaviconResult to store. + * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if + * we are under extreme memory pressure and find ourselves dropping the cache immediately. + */ + private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) { + Favicons.putFaviconsInMemCache(mFaviconUrl, loadedBitmaps.getBitmaps()); + Bitmap result = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth); + return result; + } + private static boolean imageIsValid(final Bitmap image) { return image != null && image.getWidth() > 0 && image.getHeight() > 0; } @Override protected void onPostExecute(Bitmap image) { if (mIsChaining) { return; } - // Put what we got in the memcache. - Favicons.putFaviconInMemCache(mFaviconUrl, image); - // Process the result, scale for the listener, etc. processResult(image); synchronized (loadsInFlight) { // Prevent any other tasks from chaining on this one. loadsInFlight.remove(mFaviconUrl); } @@ -392,16 +460,18 @@ public class LoadFaviconTask extends UiA // As such, I believe we're safe to do the following without holding the lock. // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely // actually happens outside of the strange situations unit tests create. // Share the result with all chained tasks. if (mChainees != null) { for (LoadFaviconTask t : mChainees) { + // In the case that we just decoded multiple favicons, either we're passing the right + // image now, or the call into the cache in processResult will fetch the right one. t.processResult(image); } } } private void processResult(Bitmap image) { Favicons.removeLoadTask(mId);
--- a/mobile/android/base/favicons/cache/FaviconsForURL.java +++ b/mobile/android/base/favicons/cache/FaviconsForURL.java @@ -105,18 +105,21 @@ public class FaviconsForURL { final int numIcons = mFavicons.size(); int searchIndex = fromIndex; while (searchIndex < numIcons) { FaviconCacheElement element = mFavicons.get(searchIndex); if (element.mIsPrimary) { if (element.mInvalidated) { - // TODO: Replace with `return null` when ICO decoder is introduced. - break; + // We return null here, despite the possible existence of other primaries, + // because we know the most suitable primary for this request exists, but is + // no longer in the cache. By returning null, we cause the caller to load the + // missing primary from the database and call again. + return null; } return element; } searchIndex++; } // No larger primary available. Let's look for smaller ones... searchIndex = fromIndex - 1;
new file mode 100644 --- /dev/null +++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java @@ -0,0 +1,158 @@ +/* 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.favicons.decoders; + +import android.graphics.Bitmap; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.gfx.BitmapUtils; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Class providing static utility methods for decoding favicons. + */ +public class FaviconDecoder { + static enum ImageMagicNumbers { + // It is irritating that Java bytes are signed... + PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}), + GIF(new byte[] {0x47, 0x49, 0x46, 0x38}), + JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}), + BMP(new byte[] {0x42, 0x4d}), + WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a}); + + public byte[] value; + + private ImageMagicNumbers(byte[] value) { + this.value = value; + } + } + + /** + * Check for image format magic numbers of formats supported by Android. + * @param buffer Byte buffer to check for magic numbers + * @param offset Offset at which to look for magic numbers. + * @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence + * starting with the magic numbers thereof). false otherwise. + */ + private static boolean isDecodableByAndroid(byte[] buffer, int offset) { + for (ImageMagicNumbers m : ImageMagicNumbers.values()) { + if (bufferStartsWith(buffer, m.value, offset)) { + return true; + } + } + + return false; + } + + /** + * Utility function to check for the existence of a test byte sequence at a given offset in a + * buffer. + * + * @param buffer Byte buffer to search. + * @param test Byte sequence to search for. + * @param bufferOffset Index in input buffer to expect test sequence. + * @return true if buffer contains the byte sequence given in test at offset bufferOffset, false + * otherwise. + */ + static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) { + if (buffer.length < test.length) { + return false; + } + + for (int i = 0; i < test.length; ++i) { + if (buffer[bufferOffset + i] != test[i]) { + return false; + } + } + return true; + } + + /** + * Decode the favicon present in the region of the provided byte[] starting at offset and + * proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the + * given range does not contain a bitmap we know how to decode. + * + * @param buffer Byte array containing the favicon to decode. + * @param offset The index of the first byte in the array of the region of interest. + * @param length The length of the region in the array to decode. + * @return The decoded version of the bitmap in the described region, or null if none can be + * decoded. + */ + public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) { + LoadFaviconResult result; + if (isDecodableByAndroid(buffer, offset)) { + result = new LoadFaviconResult(); + result.mOffset = offset; + result.mLength = length; + result.mHasMultipleBitmaps = false; + + // We assume here that decodeByteArray doesn't hold on to the entire supplied + // buffer -- worst case, each of our buffers will be twice the necessary size. + result.mBitmapsDecoded = new SingleBitmapIterator(BitmapUtils.decodeByteArray(buffer, offset, length)); + result.mFaviconBytes = buffer; + + return result; + } + + // If it's not decodable by Android, it might be an ICO. Let's try. + ICODecoder decoder = new ICODecoder(buffer, offset, length); + + result = decoder.decode(); + + if (result == null) { + return null; + } + + return result; + } + + public static LoadFaviconResult decodeFavicon(byte[] buffer) { + return decodeFavicon(buffer, 0, buffer.length); + } + + /** + * Iterator to hold a single bitmap. + */ + static class SingleBitmapIterator implements Iterator<Bitmap> { + private Bitmap mBitmap; + + public SingleBitmapIterator(Bitmap b) { + mBitmap = b; + } + + /** + * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure + * places where the runtime type of the Iterator under consideration is known and + * destruction of it is discouraged. + * + * @return The bitmap carried by this SingleBitmapIterator. + */ + public Bitmap peek() { + return mBitmap; + } + + @Override + public boolean hasNext() { + return mBitmap != null; + } + + @Override + public Bitmap next() { + if (mBitmap == null) { + throw new NoSuchElementException("Element already returned from SingleBitmapIterator."); + } + + Bitmap ret = mBitmap; + mBitmap = null; + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator."); + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/favicons/decoders/ICODecoder.java @@ -0,0 +1,380 @@ +/* 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.favicons.decoders; + +import android.graphics.Bitmap; +import org.mozilla.gecko.favicons.Favicons; +import org.mozilla.gecko.gfx.BitmapUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Utility class for determining the region of a provided array which contains the largest bitmap, + * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning + * unwanted entries from ICO files, if desired. + * + * An ICO file is a container format that may hold up to 255 images in either BMP or PNG format. + * A mixture of image types may not exist. + * + * The format consists of a header specifying the number, n, of images, followed by the Icon Directory. + * + * The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for + * the corresponding image, the dimensions, colour information, payload size, and location in the file. + * + * All numerical fields follow a little-endian byte ordering. + * + * Header format: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Image count (n) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * The type field is expected to always be 1. CUR format images should not be used for Favicons. + * + * + * Icon Directory Entry format: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Image width | Image height | Palette size | Reserved (0) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Colour plane count | Bits per pixel | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Size of image data, in bytes | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Start of image data, as an offset from start of file | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Image dimensions of zero are to be interpreted as image dimensions of 256. + * + * The palette size field records the number of colours in the stored BMP, if a palette is used. Zero + * if the payload is a PNG or no palette is in use. + * + * The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be + * interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field. + * (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.) + * + * + * The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps. + * + * This class is not thread safe. + */ +public class ICODecoder implements Iterable<Bitmap> { + // The number of bytes that compacting will save for us to bother doing it. + public static final int COMPACT_THRESHOLD = 4000; + + // Some geometry of an ICO file. + public static final int ICO_HEADER_LENGTH_BYTES = 6; + public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16; + + // The buffer containing bytes to attempt to decode. + private byte[] mDecodand; + + // The region of the decodand to decode. + private int mOffset; + private int mLen; + + private IconDirectoryEntry[] mIconDirectory; + private boolean mIsValid; + private boolean mHasDecoded; + + public ICODecoder(byte[] buffer, int offset, int len) { + mDecodand = buffer; + mOffset = offset; + mLen = len; + } + + /** + * Decode the Icon Directory for this ICO and store the result in mIconDirectory. + * + * @return true if ICO decoding was considered to probably be a success, false if it certainly + * was a failure. + */ + private boolean decodeIconDirectoryAndPossiblyPrune() { + mHasDecoded = true; + + // Fail if the end of the described range is out of bounds. + if (mOffset + mLen > mDecodand.length) { + return false; + } + + // Fail if we don't have enough space for the header. + if (mLen < ICO_HEADER_LENGTH_BYTES) { + return false; + } + + // Check that the reserved fields in the header are indeed zero, and that the type field + // specifies ICO. If not, we've probably been given something that isn't really an ICO. + if (mDecodand[mOffset] != 0 || + mDecodand[mOffset + 1] != 0 || + mDecodand[mOffset + 2] != 1 || + mDecodand[mOffset + 3] != 0) { + return false; + } + + // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java + // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned + // interpretation of the byte of interest, we do this. + int numEncodedImages = (mDecodand[mOffset + 4] & 0xFF) | + (mDecodand[mOffset + 5] & 0xFF) << 8; + + + // Fail if there are no images or the field is corrupt. + if (numEncodedImages <= 0) { + return false; + } + + final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES); + + // Fail if there is not enough space in the buffer for the stated number of icondir entries, + // let alone the data. + if (mLen < headerAndDirectorySize) { + return false; + } + + // Put the pointer on the first byte of the first Icon Directory Entry. + int bufferIndex = mOffset + ICO_HEADER_LENGTH_BYTES; + + // We now iterate over the Icon Directory, decoding each entry as we go. We also need to + // discard all entries except one >= the maximum interesting size. + + // Size of the smallest image larger than the limit encountered. + int minimumMaximum = Integer.MAX_VALUE; + + // Used to track the best entry for each size. The entries we want to keep. + HashMap<Integer, IconDirectoryEntry> preferenceMap = new HashMap<Integer, IconDirectoryEntry>(); + + for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) { + // Decode the Icon Directory Entry at this offset. + IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(mDecodand, mOffset, mLen, bufferIndex); + newEntry.mIndex = i; + + if (newEntry.mIsErroneous) { + continue; + } + + if (newEntry.mWidth > Favicons.sLargestFaviconSize) { + // If we already have a smaller image larger than the maximum size of interest, we + // don't care about the new one which is larger than the smallest image larger than + // the maximum size. + if (newEntry.mWidth >= minimumMaximum) { + continue; + } + + // Remove the previous minimum-maximum. + if (preferenceMap.containsKey(minimumMaximum)) { + preferenceMap.remove(minimumMaximum); + } + + minimumMaximum = newEntry.mWidth; + } + + IconDirectoryEntry oldEntry = preferenceMap.get(newEntry.mWidth); + if (oldEntry == null) { + preferenceMap.put(newEntry.mWidth, newEntry); + continue; + } + + if (oldEntry.compareTo(newEntry) < 0) { + preferenceMap.put(newEntry.mWidth, newEntry); + } + } + + Collection<IconDirectoryEntry> entriesRetained = preferenceMap.values(); + + // Abort if no entries are desired (Perhaps all are corrupt?) + if (entriesRetained.isEmpty()) { + return false; + } + + // Allocate space for the icon directory entries in the decoded directory. + mIconDirectory = new IconDirectoryEntry[entriesRetained.size()]; + + // The size of the data in the buffer that we find useful. + int retainedSpace = ICO_HEADER_LENGTH_BYTES; + + int dirInd = 0; + for (IconDirectoryEntry e : entriesRetained) { + retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.mPayloadSize; + mIconDirectory[dirInd] = e; + dirInd++; + } + + mIsValid = true; + + // Set the number of images field in the buffer to reflect the number of retained entries. + mDecodand[mOffset + 4] = (byte) mIconDirectory.length; + mDecodand[mOffset + 5] = (byte) (mIconDirectory.length >>> 8); + + if ((mLen - retainedSpace) > COMPACT_THRESHOLD) { + compactingCopy(retainedSpace); + } + + return true; + } + + /** + * Copy the buffer into a new array of exactly the required size, omitting any unwanted data. + */ + private void compactingCopy(int spaceRetained) { + byte[] buf = new byte[spaceRetained]; + + // Copy the header. + System.arraycopy(mDecodand, mOffset, buf, 0, ICO_HEADER_LENGTH_BYTES); + + int headerPtr = ICO_HEADER_LENGTH_BYTES; + + int payloadPtr = ICO_HEADER_LENGTH_BYTES + (mIconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES); + + int ind = 0; + for (IconDirectoryEntry entry : mIconDirectory) { + // Copy this entry. + System.arraycopy(mDecodand, mOffset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES); + + // Copy its payload. + System.arraycopy(mDecodand, mOffset + entry.mPayloadOffset, buf, payloadPtr, entry.mPayloadSize); + + // Update the offset field. + buf[headerPtr + 12] = (byte) payloadPtr; + buf[headerPtr + 13] = (byte) (payloadPtr >>> 8); + buf[headerPtr + 14] = (byte) (payloadPtr >>> 16); + buf[headerPtr + 15] = (byte) (payloadPtr >>> 24); + + entry.mPayloadOffset = payloadPtr; + entry.mIndex = ind; + + payloadPtr += entry.mPayloadSize; + headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES; + ind++; + } + + mDecodand = buf; + mOffset = 0; + mLen = spaceRetained; + } + + /** + * Decode and return the bitmap represented by the given index in the Icon Directory, if valid. + * + * @param index The index into the Icon Directory of the image of interest. + * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding + * fails. + */ + public Bitmap decodeBitmapAtIndex(int index) { + final IconDirectoryEntry iconDirEntry = mIconDirectory[index]; + + if (iconDirEntry.mPayloadIsPNG) { + // PNG payload. Simply extract it and decode it. + return BitmapUtils.decodeByteArray(mDecodand, mOffset + iconDirEntry.mPayloadOffset, iconDirEntry.mPayloadSize); + } + + // The payload is a BMP, so we need to do some magic to get the decoder to do what we want. + // We construct an ICO containing just the image we want, and let Android do the rest. + byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.mPayloadSize]; + + // Set the type field in the ICO header. + decodeTarget[2] = 1; + + // Set the num-images field in the header to 1. + decodeTarget[4] = 1; + + // Copy the ICONDIRENTRY we need into the new buffer. + System.arraycopy(mDecodand, mOffset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES); + + // Copy the payload into the new buffer. + final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES; + System.arraycopy(mDecodand, mOffset + iconDirEntry.mPayloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.mPayloadSize); + + // Update the offset field of the ICONDIRENTRY to make the new ICO valid. + decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset; + decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8); + decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16); + decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24); + + // Decode the newly-constructed singleton-ICO. + return BitmapUtils.decodeByteArray(decodeTarget); + } + + /** + * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid. + * + * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails. + */ + @Override + public ICOIterator iterator() { + // If a previous call to decode concluded this ICO is invalid, abort. + if (mHasDecoded && !mIsValid) { + return null; + } + + // If we've not been decoded before, but now fail to make any sense of the ICO, abort. + if (!mHasDecoded) { + if (!decodeIconDirectoryAndPossiblyPrune()) { + return null; + } + } + + // If decoding was a success, return an iterator over the images in this ICO. + return new ICOIterator(); + } + + /** + * Decode this ICO and return the result as a LoadFaviconResult. + * @return A LoadFaviconResult representing the decoded ICO. + */ + public LoadFaviconResult decode() { + // The call to iterator returns null if decoding fails. + Iterator<Bitmap> bitmaps = iterator(); + if (bitmaps == null) { + return null; + } + + LoadFaviconResult result = new LoadFaviconResult(); + + result.mBitmapsDecoded = bitmaps; + result.mFaviconBytes = mDecodand; + result.mOffset = mOffset; + result.mLength = mLen; + result.mHasMultipleBitmaps = mIconDirectory.length > 1; + + return result; + } + + /** + * Inner class to iterate over the elements in the ICO represented by the enclosing instance. + */ + private class ICOIterator implements Iterator<Bitmap> { + private int mIndex = 0; + + @Override + public boolean hasNext() { + return mIndex < mIconDirectory.length; + } + + @Override + public Bitmap next() { + if (mIndex > mIconDirectory.length) { + throw new NoSuchElementException("No more elements in this ICO."); + } + return decodeBitmapAtIndex(mIndex++); + } + + @Override + public void remove() { + if (mIconDirectory[mIndex] == null) { + throw new IllegalStateException("Remove already called for element " + mIndex); + } + mIconDirectory[mIndex] = null; + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/favicons/decoders/IconDirectoryEntry.java @@ -0,0 +1,201 @@ +/* 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.favicons.decoders; + +/** + * Representation of an ICO file ICONDIRENTRY structure. + */ +public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> { + + public static int sMaxBPP; + + int mWidth; + int mHeight; + int mPaletteSize; + int mBitsPerPixel; + int mPayloadSize; + int mPayloadOffset; + boolean mPayloadIsPNG; + + // Tracks the index in the Icon Directory of this entry. Useful only for pruning. + int mIndex; + boolean mIsErroneous; + + public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) { + mWidth = width; + mHeight = height; + mPaletteSize = paletteSize; + mBitsPerPixel = bitsPerPixel; + mPayloadSize = payloadSize; + mPayloadOffset = payloadOffset; + mPayloadIsPNG = payloadIsPNG; + } + + /** + * Method to get a dummy Icon Directory Entry with the Erroneous bit set. + * + * @return An erroneous placeholder Icon Directory Entry. + */ + public static IconDirectoryEntry getErroneousEntry() { + IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false); + ret.mIsErroneous = true; + + return ret; + } + + /** + * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given + * offset as an IconDirectoryEntry and returns the result. + * + * @param buffer Byte array containing the icon directory entry to decode. + * @param regionOffset Offset into the byte array of the valid region of the buffer. + * @param regionLength Length of the valid region in the buffer. + * @param entryOffset Offset of the icon directory entry to decode within the buffer. + * @return An IconDirectoryEntry object representing the entry specified, or null if the entry + * is obviously invalid. + */ + public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) { + // Verify that the reserved field is really zero. + if (buffer[entryOffset + 3] != 0) { + return getErroneousEntry(); + } + + // Verify that the entry points to a region that actually exists in the buffer, else bin it. + int fieldPtr = entryOffset + 8; + int entryLength = (buffer[fieldPtr] & 0xFF) | + (buffer[fieldPtr + 1] & 0xFF) << 8 | + (buffer[fieldPtr + 2] & 0xFF) << 16 | + (buffer[fieldPtr + 3] & 0xFF) << 24; + + // Advance to the offset field. + fieldPtr += 4; + + int payloadOffset = (buffer[fieldPtr] & 0xFF) | + (buffer[fieldPtr + 1] & 0xFF) << 8 | + (buffer[fieldPtr + 2] & 0xFF) << 16 | + (buffer[fieldPtr + 3] & 0xFF) << 24; + + // Fail if the entry describes a region outside the buffer. + if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) { + return getErroneousEntry(); + } + + // Extract the image dimensions. + int imageWidth = buffer[entryOffset] & 0xFF; + int imageHeight = buffer[entryOffset+1] & 0xFF; + + // Because Microsoft, a size value of zero represents an image size of 256. + if (imageWidth == 0) { + imageWidth = 256; + } + + if (imageHeight == 0) { + imageHeight = 256; + } + + // If the image uses a colour palette, this is the number of colours, otherwise this is zero. + int paletteSize = buffer[entryOffset + 2] & 0xFF; + + // The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel. + int colorPlanes = buffer[entryOffset + 4] & 0xFF; + + int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) | + (buffer[entryOffset + 7] & 0xFF) << 8; + + if (colorPlanes > 1) { + bitsPerPixel *= colorPlanes; + } + + // Look for PNG magic numbers at the start of the payload. + boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset); + + return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG); + } + + /** + * Get the number of bytes from the start of the ICO file to the beginning of this entry. + */ + public int getOffset() { + return ICODecoder.ICO_HEADER_LENGTH_BYTES + (mIndex * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES); + } + + @Override + public int compareTo(IconDirectoryEntry another) { + if (mWidth > another.mWidth) { + return 1; + } + + if (mWidth < another.mWidth) { + return -1; + } + + // Where both images exceed the max BPP, take the smaller of the two BPP values. + if (mBitsPerPixel >= sMaxBPP && another.mBitsPerPixel >= sMaxBPP) { + if (mBitsPerPixel < another.mBitsPerPixel) { + return 1; + } + + if (mBitsPerPixel > another.mBitsPerPixel) { + return -1; + } + } + + // Otherwise, take the larger of the BPP values. + if (mBitsPerPixel > another.mBitsPerPixel) { + return 1; + } + + if (mBitsPerPixel < another.mBitsPerPixel) { + return -1; + } + + // Prefer large palettes. + if (mPaletteSize > another.mPaletteSize) { + return 1; + } + + if (mPaletteSize < another.mPaletteSize) { + return -1; + } + + // Prefer smaller payloads. + if (mPayloadSize < another.mPayloadSize) { + return 1; + } + + if (mPayloadSize > another.mPayloadSize) { + return -1; + } + + // If all else fails, prefer PNGs over BMPs. They tend to be smaller. + if (mPayloadIsPNG && !another.mPayloadIsPNG) { + return 1; + } + + if (!mPayloadIsPNG && another.mPayloadIsPNG) { + return -1; + } + + return 0; + } + + public static void setMaxBPP(int maxBPP) { + sMaxBPP = maxBPP; + } + + @Override + public String toString() { + return "IconDirectoryEntry{" + + "\nmWidth=" + mWidth + + ", \nmHeight=" + mHeight + + ", \nmPaletteSize=" + mPaletteSize + + ", \nmBitsPerPixel=" + mBitsPerPixel + + ", \nmPayloadSize=" + mPayloadSize + + ", \nmPayloadOffset=" + mPayloadOffset + + ", \nmPayloadIsPNG=" + mPayloadIsPNG + + ", \nmIndex=" + mIndex + + '}'; + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/favicons/decoders/LoadFaviconResult.java @@ -0,0 +1,74 @@ +/* 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.favicons.decoders; + +import android.graphics.Bitmap; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.util.Iterator; + +/** + * Class representing the result of loading a favicon. + * This operation will produce either a collection of favicons, a single favicon, or no favicon. + * It is necessary to model single favicons differently to a collection of one favicon (An entity + * that may not exist with this scheme) since the in-database representation of these things differ. + * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are + * stored as decoded bitmap blobs.) + */ +public class LoadFaviconResult { + private static final String LOGTAG = "LoadFaviconResult"; + + byte[] mFaviconBytes; + int mOffset; + int mLength; + + boolean mHasMultipleBitmaps; + Iterator<Bitmap> mBitmapsDecoded; + + public Iterator<Bitmap> getBitmaps() { + return mBitmapsDecoded; + } + + /** + * Return a representation of this result suitable for storing in the database. + * For + * + * @return A byte array containing the bytes from which this result was decoded. + */ + public byte[] getBytesForDatabaseStorage() { + // Begin by normalising the buffer. + if (mOffset != 0 || mLength != mFaviconBytes.length) { + final byte[] normalised = new byte[mLength]; + System.arraycopy(mFaviconBytes, mOffset, normalised, 0, mLength); + mOffset = 0; + mFaviconBytes = normalised; + } + + // For results containing a single image, we re-encode the result as a PNG in an effort to + // save space. + if (!mHasMultipleBitmaps) { + Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) mBitmapsDecoded).peek(); + byte[] data = null; + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) { + data = stream.toByteArray(); + } else { + Log.w(LOGTAG, "Favicon compression failed."); + } + + return data; + } + + // For results containing multiple images, we store the result verbatim. (But cutting the + // buffer to size first). + // We may instead want to consider re-encoding the entire ICO as a collection of efficiently + // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image + // favicons may also not be worth the time/space tradeoff.). + return mFaviconBytes; + } + +}
--- a/mobile/android/base/gfx/BitmapUtils.java +++ b/mobile/android/base/gfx/BitmapUtils.java @@ -113,24 +113,32 @@ public final class BitmapUtils { loader.onBitmapFound(null); } public static Bitmap decodeByteArray(byte[] bytes) { return decodeByteArray(bytes, null); } public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) { + return decodeByteArray(bytes, 0, bytes.length, options); + } + + public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) { + return decodeByteArray(bytes, offset, length, null); + } + + public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) { if (bytes.length <= 0) { throw new IllegalArgumentException("bytes.length " + bytes.length + " must be a positive number"); } Bitmap bitmap = null; try { - bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options); } catch (OutOfMemoryError e) { Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length + ", options= " + options + ") OOM!", e); return null; } if (bitmap == null) { Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -123,16 +123,20 @@ gbjar.sources += [ 'db/SQLiteBridgeContentProvider.java', 'db/TabsProvider.java', 'Distribution.java', 'DoorHangerPopup.java', 'EditBookmarkDialog.java', 'favicons/cache/FaviconCache.java', 'favicons/cache/FaviconCacheElement.java', 'favicons/cache/FaviconsForURL.java', + 'favicons/decoders/FaviconDecoder.java', + 'favicons/decoders/ICODecoder.java', + 'favicons/decoders/IconDirectoryEntry.java', + 'favicons/decoders/LoadFaviconResult.java', 'favicons/Favicons.java', 'favicons/LoadFaviconTask.java', 'favicons/OnFaviconLoadedListener.java', 'FilePickerResultHandler.java', 'FilePickerResultHandlerSync.java', 'FindInPageBar.java', 'FirefoxAccountsHelper.java', 'FormAssistPopup.java',