pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

View file

@ -0,0 +1,13 @@
/* -*- 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.icons;
/**
* Interface for a callback that will be executed once an icon has been loaded successfully.
*/
public interface IconCallback {
void onIconResponse(IconResponse response);
}

View file

@ -0,0 +1,96 @@
/* -*- 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.icons;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
/**
* A class describing the location and properties of an icon that can be loaded.
*/
public class IconDescriptor {
@IntDef({ TYPE_GENERIC, TYPE_FAVICON, TYPE_TOUCHICON, TYPE_LOOKUP })
@interface IconType {}
// The type values are used for ranking icons (higher values = try to load first).
@VisibleForTesting static final int TYPE_GENERIC = 0;
@VisibleForTesting static final int TYPE_LOOKUP = 1;
@VisibleForTesting static final int TYPE_FAVICON = 5;
@VisibleForTesting static final int TYPE_TOUCHICON = 10;
private final String url;
private final int size;
private final String mimeType;
private final int type;
/**
* Create a generic icon located at the given URL. No MIME type or size is known.
*/
public static IconDescriptor createGenericIcon(String url) {
return new IconDescriptor(TYPE_GENERIC, url, 0, null);
}
/**
* Create a favicon located at the given URL and with a known size and MIME type.
*/
public static IconDescriptor createFavicon(String url, int size, String mimeType) {
return new IconDescriptor(TYPE_FAVICON, url, size, mimeType);
}
/**
* Create a touch icon located at the given URL and with a known MIME type and size.
*/
public static IconDescriptor createTouchicon(String url, int size, String mimeType) {
return new IconDescriptor(TYPE_TOUCHICON, url, size, mimeType);
}
/**
* Create an icon located at an URL that has been returned from a disk or memory storage. This
* is an icon with an URL we loaded an icon from previously. Therefore we give it a little higher
* ranking than a generic icon - even though we do not know the MIME type or size of the icon.
*/
public static IconDescriptor createLookupIcon(String url) {
return new IconDescriptor(TYPE_LOOKUP, url, 0, null);
}
private IconDescriptor(@IconType int type, String url, int size, String mimeType) {
this.type = type;
this.url = url;
this.size = size;
this.mimeType = mimeType;
}
/**
* Get the URL of the icon.
*/
public String getUrl() {
return url;
}
/**
* Get the (assumed) size of the icon. Returns 0 if no size is known.
*/
public int getSize() {
return size;
}
/**
* Get the type of the icon (favicon, touch icon, generic, lookup).
*/
@IconType
public int getType() {
return type;
}
/**
* Get the (assumed) MIME type of the icon. Returns null if no MIME type is known.
*/
@Nullable
public String getMimeType() {
return mimeType;
}
}

View file

@ -0,0 +1,67 @@
/* -*- 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.icons;
import java.util.Comparator;
/**
* This comparator implementation compares IconDescriptor objects in order to determine which icon
* to load first.
*
* In general this comparator will try touch icons before favicons (they usually have a higher resolution)
* and prefers larger icons over smaller ones.
*/
/* package-private */ class IconDescriptorComparator implements Comparator<IconDescriptor> {
@Override
public int compare(final IconDescriptor lhs, final IconDescriptor rhs) {
if (lhs.getUrl().equals(rhs.getUrl())) {
// Two descriptors pointing to the same URL are always referencing the same icon. So treat
// them as equal.
return 0;
}
// First compare the types. We prefer touch icons because they tend to have a higher resolution
// than ordinary favicons.
if (lhs.getType() != rhs.getType()) {
return compareType(lhs, rhs);
}
// If one of them is larger than pick the larger icon.
if (lhs.getSize() != rhs.getSize()) {
return compareSizes(lhs, rhs);
}
// If there's no other way to choose, we prefer container types. They *might* contain
// an image larger than the size given in the <link> tag.
final boolean lhsContainer = IconsHelper.isContainerType(lhs.getMimeType());
final boolean rhsContainer = IconsHelper.isContainerType(rhs.getMimeType());
if (lhsContainer != rhsContainer) {
return lhsContainer ? -1 : 1;
}
// There's no way to know which icon might be better. However we need to pick a consistent
// one to avoid breaking the TreeSet implementation (See Bug 1331808). Therefore we are
// picking one by just comparing the URLs.
return lhs.getUrl().compareTo(rhs.getUrl());
}
private int compareType(IconDescriptor lhs, IconDescriptor rhs) {
if (lhs.getType() > rhs.getType()) {
return -1;
} else {
return 1;
}
}
private int compareSizes(IconDescriptor lhs, IconDescriptor rhs) {
if (lhs.getSize() > rhs.getSize()) {
return -1;
} else {
return 1;
}
}
}

View file

@ -0,0 +1,181 @@
/* -*- 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.icons;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import org.mozilla.gecko.R;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.concurrent.Future;
/**
* A class describing a request to load an icon for a website.
*/
public class IconRequest {
private Context context;
// Those values are written by the IconRequestBuilder class.
/* package-private */ String pageUrl;
/* package-private */ boolean privileged;
/* package-private */ TreeSet<IconDescriptor> icons;
/* package-private */ boolean skipNetwork;
/* package-private */ boolean backgroundThread;
/* package-private */ boolean skipDisk;
/* package-private */ boolean skipMemory;
/* package-private */ int targetSize;
/* package-private */ boolean prepareOnly;
private IconCallback callback;
/* package-private */ IconRequest(Context context) {
this.context = context.getApplicationContext();
this.icons = new TreeSet<>(new IconDescriptorComparator());
// Setting some sensible defaults.
this.privileged = false;
this.skipMemory = false;
this.skipDisk = false;
this.skipNetwork = false;
this.targetSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
this.prepareOnly = false;
}
/**
* Execute this request and try to load an icon. Once an icon has been loaded successfully the
* callback will be executed.
*
* The returned Future can be used to cancel the job.
*/
public Future<IconResponse> execute(IconCallback callback) {
setCallback(callback);
return IconRequestExecutor.submit(this);
}
@VisibleForTesting void setCallback(IconCallback callback) {
this.callback = callback;
}
/**
* Get the (application) context associated with this request.
*/
public Context getContext() {
return context;
}
/**
* Get the descriptor for the potentially best icon. This is the icon that should be loaded if
* possible.
*/
public IconDescriptor getBestIcon() {
return icons.first();
}
/**
* Get the URL of the page for which an icon should be loaded.
*/
public String getPageUrl() {
return pageUrl;
}
/**
* Is this request allowed to load icons from internal data sources like the omni.ja?
*/
public boolean isPrivileged() {
return privileged;
}
/**
* Get the number of icon descriptors associated with this request.
*/
public int getIconCount() {
return icons.size();
}
/**
* Get the required target size of the icon.
*/
public int getTargetSize() {
return targetSize;
}
/**
* Should a loader access the network to load this icon?
*/
public boolean shouldSkipNetwork() {
return skipNetwork;
}
/**
* Should a loader access the disk to load this icon?
*/
public boolean shouldSkipDisk() {
return skipDisk;
}
/**
* Should a loader access the memory cache to load this icon?
*/
public boolean shouldSkipMemory() {
return skipMemory;
}
/**
* Get an iterator to iterate over all icon descriptors associated with this request.
*/
public Iterator<IconDescriptor> getIconIterator() {
return icons.iterator();
}
/**
* Create a builder to modify this request.
*
* Calling methods on the builder will modify this object and not create a copy.
*/
public IconRequestBuilder modify() {
return new IconRequestBuilder(this);
}
/**
* Should the callback be executed on a background thread? By default a callback is always
* executed on the UI thread because an icon is usually loaded in order to display it somewhere
* in the UI.
*/
/* package-private */ boolean shouldRunOnBackgroundThread() {
return backgroundThread;
}
/* package-private */ IconCallback getCallback() {
return callback;
}
/* package-private */ boolean hasIconDescriptors() {
return !icons.isEmpty();
}
/**
* Move to the next icon. This method is called after all loaders for the current best icon
* have failed. After calling this method getBestIcon() will return the next icon to try.
* hasIconDescriptors() should be called before requesting the next icon.
*/
/* package-private */ void moveToNextIcon() {
if (!icons.remove(getBestIcon())) {
// Calling this method when there's no next icon is an error (use hasIconDescriptors()).
// Theoretically this method can fail even if there's a next icon (like it did in bug 1331808).
// In this case crashing to see and fix the issue is desired.
throw new IllegalStateException("Moving to next icon failed. Could not remove first icon from set.");
}
}
/**
* Should this request be prepared but not actually load an icon?
*/
/* package-private */ boolean shouldPrepareOnly() {
return prepareOnly;
}
}

View file

@ -0,0 +1,143 @@
/* -*- 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.icons;
import android.content.Context;
import android.support.annotation.CheckResult;
import org.mozilla.gecko.GeckoAppShell;
import ch.boye.httpclientandroidlib.util.TextUtils;
/**
* Builder for creating a request to load an icon.
*/
public class IconRequestBuilder {
private final IconRequest request;
/* package-private */ IconRequestBuilder(Context context) {
this(new IconRequest(context));
}
/* package-private */ IconRequestBuilder(IconRequest request) {
this.request = request;
}
/**
* Set the URL of the page for which the icon should be loaded.
*/
@CheckResult
public IconRequestBuilder pageUrl(String pageUrl) {
request.pageUrl = pageUrl;
return this;
}
/**
* Set whether this request is allowed to load icons from non http(s) URLs (e.g. the omni.ja).
*
* For example web content referencing internal URLs should not lead to us loading icons from
* internal data structures like the omni.ja.
*/
@CheckResult
public IconRequestBuilder privileged(boolean privileged) {
request.privileged = privileged;
return this;
}
/**
* Add an icon descriptor describing the location and properties of an icon. All descriptors
* will be ranked and tried in order of their rank. Executing the request will modify the list
* of icons (filter or add additional descriptors).
*/
@CheckResult
public IconRequestBuilder icon(IconDescriptor descriptor) {
request.icons.add(descriptor);
return this;
}
/**
* Skip the network and do not load an icon from a network connection.
*/
@CheckResult
public IconRequestBuilder skipNetwork() {
request.skipNetwork = true;
return this;
}
/**
* Skip the disk cache and do not load an icon from disk.
*/
@CheckResult
public IconRequestBuilder skipDisk() {
request.skipDisk = true;
return this;
}
/**
* Skip the memory cache and do not return a previously loaded icon.
*/
@CheckResult
public IconRequestBuilder skipMemory() {
request.skipMemory = true;
return this;
}
/**
* The icon will be used as (Android) launcher icon. The loaded icon will be scaled to the
* preferred Android launcher icon size.
*/
public IconRequestBuilder forLauncherIcon() {
request.targetSize = GeckoAppShell.getPreferredIconSize();
return this;
}
/**
* Execute the callback on the background thread. By default the callback is always executed on
* the UI thread in order to add the loaded icon to a view easily.
*/
@CheckResult
public IconRequestBuilder executeCallbackOnBackgroundThread() {
request.backgroundThread = true;
return this;
}
/**
* When executing the request then only prepare executing it but do not actually load an icon.
* This mode is only used for some legacy code that uses the icon URL and therefore needs to
* perform a lookup of the URL but doesn't want to load the icon yet.
*/
public IconRequestBuilder prepareOnly() {
request.prepareOnly = true;
return this;
}
/**
* Return the request built with this builder.
*/
@CheckResult
public IconRequest build() {
if (TextUtils.isEmpty(request.pageUrl)) {
throw new IllegalStateException("Page URL is required");
}
return request;
}
/**
* This is a no-op method.
*
* All builder methods are annotated with @CheckResult to denote that the
* methods return the builder object and that it is typically an error to not call another method
* on it until eventually calling build().
*
* However in some situations code can keep a reference
* to the builder object and call methods only when a specific event occurs. To make this explicit
* and avoid lint errors this method can be called.
*/
public void deferBuild() {
// No op
}
}

View file

@ -0,0 +1,152 @@
/* -*- 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.icons;
import android.support.annotation.NonNull;
import org.mozilla.gecko.icons.loader.ContentProviderLoader;
import org.mozilla.gecko.icons.loader.DataUriLoader;
import org.mozilla.gecko.icons.loader.DiskLoader;
import org.mozilla.gecko.icons.loader.IconDownloader;
import org.mozilla.gecko.icons.loader.IconGenerator;
import org.mozilla.gecko.icons.loader.IconLoader;
import org.mozilla.gecko.icons.loader.JarLoader;
import org.mozilla.gecko.icons.loader.LegacyLoader;
import org.mozilla.gecko.icons.loader.MemoryLoader;
import org.mozilla.gecko.icons.preparation.AboutPagesPreparer;
import org.mozilla.gecko.icons.preparation.AddDefaultIconUrl;
import org.mozilla.gecko.icons.preparation.FilterKnownFailureUrls;
import org.mozilla.gecko.icons.preparation.FilterMimeTypes;
import org.mozilla.gecko.icons.preparation.FilterPrivilegedUrls;
import org.mozilla.gecko.icons.preparation.LookupIconUrl;
import org.mozilla.gecko.icons.preparation.Preparer;
import org.mozilla.gecko.icons.processing.ColorProcessor;
import org.mozilla.gecko.icons.processing.DiskProcessor;
import org.mozilla.gecko.icons.processing.MemoryProcessor;
import org.mozilla.gecko.icons.processing.Processor;
import org.mozilla.gecko.icons.processing.ResizingProcessor;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Executor for icon requests.
*/
/* package-private */ class IconRequestExecutor {
/**
* Loader implementation that generates an icon if none could be loaded.
*/
private static final IconLoader GENERATOR = new IconGenerator();
/**
* Ordered list of prepares that run before any icon is loaded.
*/
private static final List<Preparer> PREPARERS = Arrays.asList(
// First we look into our memory and disk caches if there are some known icon URLs for
// the page URL of the request.
new LookupIconUrl(),
// For all icons with MIME type we filter entries with unknown MIME type that we probably
// cannot decode anyways.
new FilterMimeTypes(),
// If this is not a request that is allowed to load icons from privileged locations (omni.jar)
// then filter such icon URLs.
new FilterPrivilegedUrls(),
// This preparer adds an icon URL for about pages. It's added after the filter for privileged
// URLs. We always want to be able to load those specific icons.
new AboutPagesPreparer(),
// Add the default favicon URL (*/favicon.ico) to the list of icon URLs; with a low priority,
// this icon URL should be tried last.
new AddDefaultIconUrl(),
// Finally we filter all URLs that failed to load recently (4xx / 5xx errors).
new FilterKnownFailureUrls()
);
/**
* Ordered list of loaders. If a loader returns a response object then subsequent loaders are not run.
*/
private static final List<IconLoader> LOADERS = Arrays.asList(
// First we try to load an icon that is already in the memory. That's cheap.
new MemoryLoader(),
// Try to decode the icon if it is a data: URI.
new DataUriLoader(),
// Try to load the icon from the omni.ha if it's a jar:jar URI.
new JarLoader(),
// Try to load the icon from a content provider (if applicable).
new ContentProviderLoader(),
// Try to load the icon from the disk cache.
new DiskLoader(),
// If the icon is not in any of our cashes and can't be decoded then look into the
// database (legacy). Maybe this icon was loaded before the new code was deployed.
new LegacyLoader(),
// Download the icon from the web.
new IconDownloader()
);
/**
* Ordered list of processors that run after an icon has been loaded.
*/
private static final List<Processor> PROCESSORS = Arrays.asList(
// Store the icon (and mapping) in the disk cache if needed
new DiskProcessor(),
// Resize the icon to match the target size (if possible)
new ResizingProcessor(),
// Extract the dominant color from the icon
new ColorProcessor(),
// Store the icon in the memory cache
new MemoryProcessor()
);
private static final ExecutorService EXECUTOR;
static {
final ThreadFactory factory = new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable runnable) {
Thread thread = new Thread(runnable, "GeckoIconTask");
thread.setDaemon(false);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
};
// Single thread executor
EXECUTOR = new ThreadPoolExecutor(
1, /* corePoolSize */
1, /* maximumPoolSize */
0L, /* keepAliveTime */
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
factory);
}
/**
* Submit the request for execution.
*/
/* package-private */ static Future<IconResponse> submit(IconRequest request) {
return EXECUTOR.submit(
new IconTask(request, PREPARERS, LOADERS, PROCESSORS, GENERATOR)
);
}
}

View file

@ -0,0 +1,167 @@
/* -*- 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.icons;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
/**
* Response object containing a successful loaded icon and meta data.
*/
public class IconResponse {
/**
* Create a response for a plain bitmap.
*/
public static IconResponse create(@NonNull Bitmap bitmap) {
return new IconResponse(bitmap);
}
/**
* Create a response for a bitmap that has been loaded from the network by requesting a specific URL.
*/
public static IconResponse createFromNetwork(@NonNull Bitmap bitmap, @NonNull String url) {
final IconResponse response = new IconResponse(bitmap);
response.url = url;
response.fromNetwork = true;
return response;
}
/**
* Create a response for a generated bitmap with a dominant color.
*/
public static IconResponse createGenerated(@NonNull Bitmap bitmap, int color) {
final IconResponse response = new IconResponse(bitmap);
response.color = color;
response.generated = true;
return response;
}
/**
* Create a response for a bitmap that has been loaded from the memory cache.
*/
public static IconResponse createFromMemory(@NonNull Bitmap bitmap, @NonNull String url, int color) {
final IconResponse response = new IconResponse(bitmap);
response.url = url;
response.color = color;
response.fromMemory = true;
return response;
}
/**
* Create a response for a bitmap that has been loaded from the disk cache.
*/
public static IconResponse createFromDisk(@NonNull Bitmap bitmap, @NonNull String url) {
final IconResponse response = new IconResponse(bitmap);
response.url = url;
response.fromDisk = true;
return response;
}
private Bitmap bitmap;
private int color;
private boolean fromNetwork;
private boolean fromMemory;
private boolean fromDisk;
private boolean generated;
private String url;
private IconResponse(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap is null");
}
this.bitmap = bitmap;
this.color = 0;
this.url = null;
this.fromNetwork = false;
this.fromMemory = false;
this.fromDisk = false;
this.generated = false;
}
/**
* Get the icon bitmap. This method will always return a bitmap.
*/
@NonNull
public Bitmap getBitmap() {
return bitmap;
}
/**
* Get the dominant color of the icon. Will return 0 if no color could be extracted.
*/
public int getColor() {
return color;
}
/**
* Does this response contain a dominant color?
*/
public boolean hasColor() {
return color != 0;
}
/**
* Has this icon been loaded from the network?
*/
public boolean isFromNetwork() {
return fromNetwork;
}
/**
* Has this icon been generated?
*/
public boolean isGenerated() {
return generated;
}
/**
* Has this icon been loaded from memory (cache)?
*/
public boolean isFromMemory() {
return fromMemory;
}
/**
* Has this icon been loaded from disk (cache)?
*/
public boolean isFromDisk() {
return fromDisk;
}
/**
* Get the URL this icon has been loaded from.
*/
@Nullable
public String getUrl() {
return url;
}
/**
* Does this response contain an URL from which the icon has been loaded?
*/
public boolean hasUrl() {
return !TextUtils.isEmpty(url);
}
/**
* Update the color of this response. This method is called by processors updating meta data
* after the icon has been loaded.
*/
public void updateColor(int color) {
this.color = color;
}
/**
* Update the bitmap of this response. This method is called by processors that modify the
* loaded icon.
*/
public void updateBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
}

View file

@ -0,0 +1,222 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.icons;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.icons.loader.IconLoader;
import org.mozilla.gecko.icons.preparation.Preparer;
import org.mozilla.gecko.icons.processing.Processor;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Task that will be run by the IconRequestExecutor for every icon request.
*/
/* package-private */ class IconTask implements Callable<IconResponse> {
private static final String LOGTAG = "Gecko/IconTask";
private static final boolean DEBUG = false;
private final List<Preparer> preparers;
private final List<IconLoader> loaders;
private final List<Processor> processors;
private final IconLoader generator;
private final IconRequest request;
/* package-private */ IconTask(
@NonNull IconRequest request,
@NonNull List<Preparer> preparers,
@NonNull List<IconLoader> loaders,
@NonNull List<Processor> processors,
@NonNull IconLoader generator) {
this.request = request;
this.preparers = preparers;
this.loaders = loaders;
this.processors = processors;
this.generator = generator;
}
@Override
public IconResponse call() {
try {
logRequest(request);
prepareRequest(request);
if (request.shouldPrepareOnly()) {
// This request should only be prepared but not load an actual icon.
return null;
}
final IconResponse response = loadIcon(request);
if (response != null) {
processIcon(request, response);
executeCallback(request, response);
logResponse(response);
return response;
}
} catch (InterruptedException e) {
Log.d(LOGTAG, "IconTask was interrupted", e);
// Clear interrupt thread.
Thread.interrupted();
} catch (Throwable e) {
handleException(e);
}
return null;
}
/**
* Check if this thread was interrupted (e.g. this task was cancelled). Throws an InterruptedException
* to stop executing the task in this case.
*/
private void ensureNotInterrupted() throws InterruptedException {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Task has been cancelled");
}
}
private void executeCallback(IconRequest request, final IconResponse response) {
final IconCallback callback = request.getCallback();
if (callback != null) {
if (request.shouldRunOnBackgroundThread()) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
callback.onIconResponse(response);
}
});
} else {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
callback.onIconResponse(response);
}
});
}
}
}
private void prepareRequest(IconRequest request) throws InterruptedException {
for (Preparer preparer : preparers) {
ensureNotInterrupted();
preparer.prepare(request);
logPreparer(request, preparer);
}
}
private IconResponse loadIcon(IconRequest request) throws InterruptedException {
while (request.hasIconDescriptors()) {
for (IconLoader loader : loaders) {
ensureNotInterrupted();
IconResponse response = loader.load(request);
logLoader(request, loader, response);
if (response != null) {
return response;
}
}
request.moveToNextIcon();
}
return generator.load(request);
}
private void processIcon(IconRequest request, IconResponse response) throws InterruptedException {
for (Processor processor : processors) {
ensureNotInterrupted();
processor.process(request, response);
logProcessor(processor);
}
}
private void handleException(final Throwable t) {
if (AppConstants.NIGHTLY_BUILD) {
// We want to be aware of problems: Let's re-throw the exception on the main thread to
// force an app crash. However we only do this in Nightly builds. Every other build
// (especially release builds) should just carry on and log the error.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
throw new RuntimeException("Icon task thread crashed", t);
}
});
} else {
Log.e(LOGTAG, "Icon task crashed", t);
}
}
private boolean shouldLog() {
// Do not log anything if debugging is disabled and never log anything in a non-nightly build.
return DEBUG && AppConstants.NIGHTLY_BUILD;
}
private void logPreparer(IconRequest request, Preparer preparer) {
if (!shouldLog()) {
return;
}
Log.d(LOGTAG, String.format(" PREPARE %s" + " (%s)",
preparer.getClass().getSimpleName(),
request.getIconCount()));
}
private void logLoader(IconRequest request, IconLoader loader, IconResponse response) {
if (!shouldLog()) {
return;
}
Log.d(LOGTAG, String.format(" LOAD [%s] %s : %s",
response != null ? "X" : " ",
loader.getClass().getSimpleName(),
request.getBestIcon().getUrl()));
}
private void logProcessor(Processor processor) {
if (!shouldLog()) {
return;
}
Log.d(LOGTAG, " PROCESS " + processor.getClass().getSimpleName());
}
private void logResponse(IconResponse response) {
if (!shouldLog()) {
return;
}
final Bitmap bitmap = response.getBitmap();
Log.d(LOGTAG, String.format("=> ICON: %sx%s", bitmap.getWidth(), bitmap.getHeight()));
}
private void logRequest(IconRequest request) {
if (!shouldLog()) {
return;
}
Log.d(LOGTAG, String.format("REQUEST (%s) %s",
request.getIconCount(),
request.getPageUrl()));
}
}

View file

@ -0,0 +1,35 @@
/* -*- 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.icons;
import android.content.Context;
import android.support.annotation.CheckResult;
/**
* Entry point for loading icons for websites (just high quality icons, can be favicons or
* touch icons).
*
* The API is loosely inspired by Picasso's builder.
*
* Example:
*
* Icons.with(context)
* .pageUrl(pageURL)
* .skipNetwork()
* .privileged(true)
* .icon(IconDescriptor.createGenericIcon(url))
* .build()
* .execute(callback);
*/
public abstract class Icons {
/**
* Create a new request for loading a website icon.
*/
@CheckResult
public static IconRequestBuilder with(Context context) {
return new IconRequestBuilder(context);
}
}

View file

@ -0,0 +1,140 @@
/* -*- 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.icons;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.util.StringUtils;
import java.util.HashSet;
/**
* Helper methods for icon related tasks.
*/
public class IconsHelper {
private static final String LOGTAG = "Gecko/IconsHelper";
// Mime types of things we are capable of decoding.
private static final HashSet<String> sDecodableMimeTypes = new HashSet<>();
// Mime types of things we are both capable of decoding and are container formats (May contain
// multiple different sizes of image)
private static final HashSet<String> sContainerMimeTypes = new HashSet<>();
static {
// MIME types extracted from http://filext.com - ostensibly all in-use mime types for the
// corresponding formats.
// ICO
sContainerMimeTypes.add("image/vnd.microsoft.icon");
sContainerMimeTypes.add("image/ico");
sContainerMimeTypes.add("image/icon");
sContainerMimeTypes.add("image/x-icon");
sContainerMimeTypes.add("text/ico");
sContainerMimeTypes.add("application/ico");
// Add supported container types to the set of supported types.
sDecodableMimeTypes.addAll(sContainerMimeTypes);
// PNG
sDecodableMimeTypes.add("image/png");
sDecodableMimeTypes.add("application/png");
sDecodableMimeTypes.add("application/x-png");
// GIF
sDecodableMimeTypes.add("image/gif");
// JPEG
sDecodableMimeTypes.add("image/jpeg");
sDecodableMimeTypes.add("image/jpg");
sDecodableMimeTypes.add("image/pipeg");
sDecodableMimeTypes.add("image/vnd.swiftview-jpeg");
sDecodableMimeTypes.add("application/jpg");
sDecodableMimeTypes.add("application/x-jpg");
// BMP
sDecodableMimeTypes.add("application/bmp");
sDecodableMimeTypes.add("application/x-bmp");
sDecodableMimeTypes.add("application/x-win-bitmap");
sDecodableMimeTypes.add("image/bmp");
sDecodableMimeTypes.add("image/x-bmp");
sDecodableMimeTypes.add("image/x-bitmap");
sDecodableMimeTypes.add("image/x-xbitmap");
sDecodableMimeTypes.add("image/x-win-bitmap");
sDecodableMimeTypes.add("image/x-windows-bitmap");
sDecodableMimeTypes.add("image/x-ms-bitmap");
sDecodableMimeTypes.add("image/x-ms-bmp");
sDecodableMimeTypes.add("image/ms-bmp");
}
/**
* Helper method to getIcon the default Favicon URL for a given pageURL. Generally: somewhere.com/favicon.ico
*
* @param pageURL Page URL for which a default Favicon URL is requested
* @return The default Favicon URL or null if no default URL could be guessed.
*/
@Nullable
public static String guessDefaultFaviconURL(String pageURL) {
if (TextUtils.isEmpty(pageURL)) {
return null;
}
// Special-casing for about: pages. The favicon for about:pages which don't provide a link tag
// is bundled in the database, keyed only by page URL, hence the need to return the page URL
// here. If the database ever migrates to stop being silly in this way, this can plausibly
// be removed.
if (AboutPages.isAboutPage(pageURL) || pageURL.startsWith("jar:")) {
return pageURL;
}
if (!StringUtils.isHttpOrHttps(pageURL)) {
// Guessing a default URL only makes sense for http(s) URLs.
return null;
}
try {
// Fall back to trying "someScheme:someDomain.someExtension/favicon.ico".
Uri uri = Uri.parse(pageURL);
if (uri.getAuthority().isEmpty()) {
return null;
}
return uri.buildUpon()
.path("favicon.ico")
.clearQuery()
.fragment("")
.build()
.toString();
} catch (Exception e) {
Log.d(LOGTAG, "Exception getting default favicon URL");
return null;
}
}
/**
* Helper function to determine if the provided mime type is that of a format that can contain
* multiple image types. At time of writing, the only such type is ICO.
* @param mimeType Mime type to check.
* @return true if the given mime type is a container type, false otherwise.
*/
public static boolean isContainerType(@NonNull String mimeType) {
return sContainerMimeTypes.contains(mimeType);
}
/**
* Helper function to determine if we can decode a particular mime type.
*
* @param imgType Mime type to check for decodability.
* @return false if the given mime type is certainly not decodable, true if it might be.
*/
public static boolean canDecodeType(@NonNull String imgType) {
return sDecodableMimeTypes.contains(imgType);
}
}

View file

@ -0,0 +1,197 @@
/* 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.icons.decoders;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Base64;
import android.util.Log;
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 {
private static final String LOG_TAG = "GeckoFaviconDecoder";
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(Context context, byte[] buffer, int offset, int length) {
LoadFaviconResult result;
if (isDecodableByAndroid(buffer, offset)) {
result = new LoadFaviconResult();
result.offset = offset;
result.length = length;
result.isICO = false;
Bitmap decodedImage = BitmapUtils.decodeByteArray(buffer, offset, length);
if (decodedImage == null) {
// What we got wasn't decodable after all. Probably corrupted image, or we got a muffled OOM.
return null;
}
// 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.bitmapsDecoded = new SingleBitmapIterator(decodedImage);
result.faviconBytes = buffer;
return result;
}
// If it's not decodable by Android, it might be an ICO. Let's try.
ICODecoder decoder = new ICODecoder(context, buffer, offset, length);
result = decoder.decode();
if (result == null) {
return null;
}
return result;
}
public static LoadFaviconResult decodeDataURI(Context context, String uri) {
if (uri == null) {
Log.w(LOG_TAG, "Can't decode null data: URI.");
return null;
}
if (!uri.startsWith("data:image/")) {
// Can't decode non-image data: URI.
return null;
}
// Otherwise, let's attack this blindly. Strictly we should be parsing.
int offset = uri.indexOf(',') + 1;
if (offset == 0) {
Log.w(LOG_TAG, "No ',' in data: URI; malformed?");
return null;
}
try {
String base64 = uri.substring(offset);
byte[] raw = Base64.decode(base64, Base64.DEFAULT);
return decodeFavicon(context, raw);
} catch (Exception e) {
Log.w(LOG_TAG, "Couldn't decode data: URI.", e);
return null;
}
}
public static LoadFaviconResult decodeFavicon(Context context, byte[] buffer) {
return decodeFavicon(context, buffer, 0, buffer.length);
}
/**
* Iterator to hold a single bitmap.
*/
static class SingleBitmapIterator implements Iterator<Bitmap> {
private Bitmap bitmap;
public SingleBitmapIterator(Bitmap b) {
bitmap = 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 bitmap;
}
@Override
public boolean hasNext() {
return bitmap != null;
}
@Override
public Bitmap next() {
if (bitmap == null) {
throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
}
Bitmap ret = bitmap;
bitmap = null;
return ret;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
}
}
}

View file

@ -0,0 +1,396 @@
/* 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.icons.decoders;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.VisibleForTesting;
import android.util.SparseArray;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.R;
/**
* 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[] decodand;
// The region of the decodand to decode.
private int offset;
private int len;
IconDirectoryEntry[] iconDirectory;
private boolean isValid;
private boolean hasDecoded;
private int largestFaviconSize;
@RobocopTarget
public ICODecoder(Context context, byte[] decodand, int offset, int len) {
this.decodand = decodand;
this.offset = offset;
this.len = len;
this.largestFaviconSize = context.getResources()
.getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
}
/**
* Decode the Icon Directory for this ICO and store the result in iconDirectory.
*
* @return true if ICO decoding was considered to probably be a success, false if it certainly
* was a failure.
*/
private boolean decodeIconDirectoryAndPossiblyPrune() {
hasDecoded = true;
// Fail if the end of the described range is out of bounds.
if (offset + len > decodand.length) {
return false;
}
// Fail if we don't have enough space for the header.
if (len < 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 (decodand[offset] != 0 ||
decodand[offset + 1] != 0 ||
decodand[offset + 2] != 1 ||
decodand[offset + 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 = (decodand[offset + 4] & 0xFF) |
(decodand[offset + 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 (len < headerAndDirectorySize) {
return false;
}
// Put the pointer on the first byte of the first Icon Directory Entry.
int bufferIndex = offset + 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.
SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<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(decodand, offset, len, bufferIndex);
newEntry.index = i;
if (newEntry.isErroneous) {
continue;
}
if (newEntry.width > largestFaviconSize) {
// 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.width >= minimumMaximum) {
continue;
}
// Remove the previous minimum-maximum.
preferenceArray.delete(minimumMaximum);
minimumMaximum = newEntry.width;
}
IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width);
if (oldEntry == null) {
preferenceArray.put(newEntry.width, newEntry);
continue;
}
if (oldEntry.compareTo(newEntry) < 0) {
preferenceArray.put(newEntry.width, newEntry);
}
}
final int count = preferenceArray.size();
// Abort if no entries are desired (Perhaps all are corrupt?)
if (count == 0) {
return false;
}
// Allocate space for the icon directory entries in the decoded directory.
iconDirectory = new IconDirectoryEntry[count];
// The size of the data in the buffer that we find useful.
int retainedSpace = ICO_HEADER_LENGTH_BYTES;
for (int i = 0; i < count; i++) {
IconDirectoryEntry e = preferenceArray.valueAt(i);
retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize;
iconDirectory[i] = e;
}
isValid = true;
// Set the number of images field in the buffer to reflect the number of retained entries.
decodand[offset + 4] = (byte) iconDirectory.length;
decodand[offset + 5] = (byte) (iconDirectory.length >>> 8);
if ((len - 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(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES);
int headerPtr = ICO_HEADER_LENGTH_BYTES;
int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
int ind = 0;
for (IconDirectoryEntry entry : iconDirectory) {
// Copy this entry.
System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
// Copy its payload.
System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize);
// 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.payloadOffset = payloadPtr;
entry.index = ind;
payloadPtr += entry.payloadSize;
headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
ind++;
}
decodand = buf;
offset = 0;
len = 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 = iconDirectory[index];
if (iconDirEntry.payloadIsPNG) {
// PNG payload. Simply extract it and decode it.
return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize);
}
// 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.payloadSize];
// 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(decodand, offset + 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(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize);
// Update the offset field of the ICONDIRENTRY to make the new ICO valid.
decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = singlePayloadOffset;
decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (singlePayloadOffset >>> 8);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (singlePayloadOffset >>> 16);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (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 (hasDecoded && !isValid) {
return null;
}
// If we've not been decoded before, but now fail to make any sense of the ICO, abort.
if (!hasDecoded) {
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.bitmapsDecoded = bitmaps;
result.faviconBytes = decodand;
result.offset = offset;
result.length = len;
result.isICO = true;
return result;
}
@VisibleForTesting
@RobocopTarget
public IconDirectoryEntry[] getIconDirectory() {
return iconDirectory;
}
@VisibleForTesting
@RobocopTarget
public int getLargestFaviconSize() {
return largestFaviconSize;
}
/**
* Inner class to iterate over the elements in the ICO represented by the enclosing instance.
*/
private class ICOIterator implements Iterator<Bitmap> {
private int mIndex;
@Override
public boolean hasNext() {
return mIndex < iconDirectory.length;
}
@Override
public Bitmap next() {
if (mIndex > iconDirectory.length) {
throw new NoSuchElementException("No more elements in this ICO.");
}
return decodeBitmapAtIndex(mIndex++);
}
@Override
public void remove() {
if (iconDirectory[mIndex] == null) {
throw new IllegalStateException("Remove already called for element " + mIndex);
}
iconDirectory[mIndex] = null;
}
}
}

View file

@ -0,0 +1,212 @@
/* 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.icons.decoders;
import android.support.annotation.VisibleForTesting;
import org.mozilla.gecko.annotation.RobocopTarget;
/**
* Representation of an ICO file ICONDIRENTRY structure.
*/
public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
public static int maxBPP;
int width;
int height;
int paletteSize;
int bitsPerPixel;
int payloadSize;
int payloadOffset;
boolean payloadIsPNG;
// Tracks the index in the Icon Directory of this entry. Useful only for pruning.
int index;
boolean isErroneous;
@RobocopTarget
public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
this.width = width;
this.height = height;
this.paletteSize = paletteSize;
this.bitsPerPixel = bitsPerPixel;
this.payloadSize = payloadSize;
this.payloadOffset = payloadOffset;
this.payloadIsPNG = 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.isErroneous = 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 + (index * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
}
@Override
public int compareTo(IconDirectoryEntry another) {
if (width > another.width) {
return 1;
}
if (width < another.width) {
return -1;
}
// Where both images exceed the max BPP, take the smaller of the two BPP values.
if (bitsPerPixel >= maxBPP && another.bitsPerPixel >= maxBPP) {
if (bitsPerPixel < another.bitsPerPixel) {
return 1;
}
if (bitsPerPixel > another.bitsPerPixel) {
return -1;
}
}
// Otherwise, take the larger of the BPP values.
if (bitsPerPixel > another.bitsPerPixel) {
return 1;
}
if (bitsPerPixel < another.bitsPerPixel) {
return -1;
}
// Prefer large palettes.
if (paletteSize > another.paletteSize) {
return 1;
}
if (paletteSize < another.paletteSize) {
return -1;
}
// Prefer smaller payloads.
if (payloadSize < another.payloadSize) {
return 1;
}
if (payloadSize > another.payloadSize) {
return -1;
}
// If all else fails, prefer PNGs over BMPs. They tend to be smaller.
if (payloadIsPNG && !another.payloadIsPNG) {
return 1;
}
if (!payloadIsPNG && another.payloadIsPNG) {
return -1;
}
return 0;
}
public static void setMaxBPP(int maxBPP) {
IconDirectoryEntry.maxBPP = maxBPP;
}
@VisibleForTesting
@RobocopTarget
public int getWidth() {
return width;
}
@Override
public String toString() {
return "IconDirectoryEntry{" +
"\nwidth=" + width +
", \nheight=" + height +
", \npaletteSize=" + paletteSize +
", \nbitsPerPixel=" + bitsPerPixel +
", \npayloadSize=" + payloadSize +
", \npayloadOffset=" + payloadOffset +
", \npayloadIsPNG=" + payloadIsPNG +
", \nindex=" + index +
'}';
}
}

View file

@ -0,0 +1,133 @@
/* 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.icons.decoders;
import android.graphics.Bitmap;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.SparseArray;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* 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[] faviconBytes;
int offset;
int length;
boolean isICO;
Iterator<Bitmap> bitmapsDecoded;
public Iterator<Bitmap> getBitmaps() {
return bitmapsDecoded;
}
/**
* Return a representation of this result suitable for storing in the database.
*
* @return A byte array containing the bytes from which this result was decoded,
* or null if re-encoding failed.
*/
public byte[] getBytesForDatabaseStorage() {
// Begin by normalising the buffer.
if (offset != 0 || length != faviconBytes.length) {
final byte[] normalised = new byte[length];
System.arraycopy(faviconBytes, offset, normalised, 0, length);
offset = 0;
faviconBytes = normalised;
}
// 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.).
if (isICO) {
return faviconBytes;
}
// For results containing a single image, we re-encode the
// result as a PNG in an effort to save space.
final Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) bitmapsDecoded).peek();
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
return stream.toByteArray();
}
} catch (OutOfMemoryError e) {
Log.w(LOGTAG, "Out of memory re-compressing favicon.");
}
Log.w(LOGTAG, "Favicon re-compression failed.");
return null;
}
@Nullable
public Bitmap getBestBitmap(int targetWidthAndHeight) {
final SparseArray<Bitmap> iconMap = new SparseArray<>();
final List<Integer> sizes = new ArrayList<>();
while (bitmapsDecoded.hasNext()) {
final Bitmap b = bitmapsDecoded.next();
// It's possible to receive null, most likely due to OOM or a zero-sized image,
// from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
if (b != null) {
iconMap.put(b.getWidth(), b);
sizes.add(b.getWidth());
}
}
int bestSize = selectBestSizeFromList(sizes, targetWidthAndHeight);
if (bestSize == -1) {
// No icons found: this could occur if we weren't able to process any of the
// supplied icons.
return null;
}
return iconMap.get(bestSize);
}
/**
* Select the closest icon size from a list of icon sizes.
* We just find the first icon that is larger than the preferred size if available, or otherwise select the
* largest icon (if all icons are smaller than the preferred size).
*
* @return The closest icon size, or -1 if no sizes are supplied.
*/
public static int selectBestSizeFromList(final List<Integer> sizes, final int preferredSize) {
if (sizes.isEmpty()) {
// This isn't ideal, however current code assumes this as an error value for now.
return -1;
}
Collections.sort(sizes);
for (int size : sizes) {
if (size >= preferredSize) {
return size;
}
}
// If all icons are smaller than the preferred size then we don't have an icon
// selected yet, therefore just take the largest (last) icon.
return sizes.get(sizes.size() - 1);
}
}

View file

@ -0,0 +1,96 @@
/* -*- 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.icons.loader;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.text.TextUtils;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Loader for loading icons from a content provider. This loader was primarily written to load icons
* from the partner bookmarks provider. However it can load icons from arbitrary content providers
* as long as they return a cursor with a "favicon" or "touchicon" column (blob).
*/
public class ContentProviderLoader implements IconLoader {
@Override
public IconResponse load(IconRequest request) {
if (request.shouldSkipDisk()) {
// If we should not load data from disk then we do not load from content providers either.
return null;
}
final String iconUrl = request.getBestIcon().getUrl();
final Context context = request.getContext();
final int targetSize = request.getTargetSize();
if (TextUtils.isEmpty(iconUrl) || !iconUrl.startsWith("content://")) {
return null;
}
Cursor cursor = context.getContentResolver().query(
Uri.parse(iconUrl),
new String[] {
PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON,
PartnerBookmarksProviderProxy.PartnerContract.FAVICON,
},
null,
null,
null
);
if (cursor == null) {
return null;
}
try {
if (!cursor.moveToFirst()) {
return null;
}
// Try the touch icon first. It has a higher resolution usually.
Bitmap icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetSize);
if (icon != null) {
return IconResponse.create(icon);
}
icon = decodeFromCursor(request.getContext(), cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetSize);
if (icon != null) {
return IconResponse.create(icon);
}
} finally {
cursor.close();
}
return null;
}
private Bitmap decodeFromCursor(Context context, Cursor cursor, String column, int targetWidthAndHeight) {
final int index = cursor.getColumnIndex(column);
if (index == -1) {
return null;
}
if (cursor.isNull(index)) {
return null;
}
final byte[] data = cursor.getBlob(index);
LoadFaviconResult result = FaviconDecoder.decodeFavicon(context, data, 0, data.length);
if (result == null) {
return null;
}
return result.getBestBitmap(targetWidthAndHeight);
}
}

View file

@ -0,0 +1,36 @@
/* -*- 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.icons.loader;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Loader for loading icons from a data URI. This loader will try to decode any data with an
* "image/*" MIME type.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
*/
public class DataUriLoader implements IconLoader {
@Override
public IconResponse load(IconRequest request) {
final String iconUrl = request.getBestIcon().getUrl();
if (!iconUrl.startsWith("data:image/")) {
return null;
}
LoadFaviconResult loadFaviconResult = FaviconDecoder.decodeDataURI(request.getContext(), iconUrl);
if (loadFaviconResult == null) {
return null;
}
return IconResponse.create(
loadFaviconResult.getBestBitmap(request.getTargetSize()));
}
}

View file

@ -0,0 +1,27 @@
/* -*- 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.icons.loader;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.storage.DiskStorage;
/**
* Loader implementation for loading icons from the disk cache (Implemented by DiskStorage).
*/
public class DiskLoader implements IconLoader {
@Override
public IconResponse load(IconRequest request) {
if (request.shouldSkipDisk()) {
return null;
}
final DiskStorage storage = DiskStorage.get(request.getContext());
final String iconUrl = request.getBestIcon().getUrl();
return storage.getIcon(iconUrl);
}
}

View file

@ -0,0 +1,219 @@
/* -*- 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.icons.loader;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.storage.FailureCache;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.ProxySelector;
import org.mozilla.gecko.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
/**
* This loader implementation downloads icons from http(s) URLs.
*/
public class IconDownloader implements IconLoader {
private static final String LOGTAG = "Gecko/Downloader";
/**
* The maximum number of http redirects (3xx) until we give up.
*/
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_BYTES = 25000;
@Override
public IconResponse load(IconRequest request) {
if (request.shouldSkipNetwork()) {
return null;
}
final String iconUrl = request.getBestIcon().getUrl();
if (!StringUtils.isHttpOrHttps(iconUrl)) {
return null;
}
try {
final LoadFaviconResult result = downloadAndDecodeImage(request.getContext(), iconUrl);
if (result == null) {
return null;
}
final Bitmap bitmap = result.getBestBitmap(request.getTargetSize());
if (bitmap == null) {
return null;
}
return IconResponse.createFromNetwork(bitmap, iconUrl);
} catch (Exception e) {
Log.e(LOGTAG, "Error reading favicon", e);
} catch (OutOfMemoryError e) {
Log.e(LOGTAG, "Insufficient memory to process favicon");
}
return null;
}
/**
* 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 was 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.
*/
@VisibleForTesting
LoadFaviconResult downloadAndDecodeImage(Context context, String targetFaviconURL) throws IOException, URISyntaxException {
// Try the URL we were given.
final HttpURLConnection connection = tryDownload(targetFaviconURL);
if (connection == null) {
return null;
}
InputStream stream = null;
// Decode the image from the fetched response.
try {
stream = connection.getInputStream();
return decodeImageFromResponse(context, stream, connection.getHeaderFieldInt("Content-Length", -1));
} finally {
// Close the stream and free related resources.
IOUtils.safeStreamClose(stream);
connection.disconnect();
}
}
/**
* 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 HttpURLConnection tryDownload(String faviconURI) throws URISyntaxException, IOException {
final HashSet<String> visitedLinkSet = new HashSet<>();
visitedLinkSet.add(faviconURI);
return tryDownloadRecurse(faviconURI, visitedLinkSet);
}
/**
* Try to download from the favicon URL and recursively follow redirects.
*/
private HttpURLConnection tryDownloadRecurse(String faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
return null;
}
final HttpURLConnection connection = connectTo(faviconURI);
// Was the response a failure?
final int status = connection.getResponseCode();
// Handle HTTP status codes requesting a redirect.
if (status >= 300 && status < 400) {
final String newURI = connection.getHeaderField("Location");
// Handle mad web servers.
try {
if (newURI == null || newURI.equals(faviconURI)) {
return null;
}
if (visited.contains(newURI)) {
// Already been redirected here - abort.
return null;
}
visited.add(newURI);
} finally {
connection.disconnect();
}
return tryDownloadRecurse(newURI, visited);
}
if (status >= 400) {
// Client or Server error. Let's not retry loading from this URL again for some time.
FailureCache.get().rememberFailure(faviconURI);
connection.disconnect();
return null;
}
return connection;
}
@VisibleForTesting
HttpURLConnection connectTo(String faviconURI) throws URISyntaxException, IOException {
final HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(
new URI(faviconURI));
connection.setRequestProperty("User-Agent", GeckoAppShell.getGeckoInterface().getDefaultUAString());
// We implemented or own way of following redirects back when this code was using HttpClient.
// Nowadays we should let HttpUrlConnection do the work - assuming that it doesn't follow
// redirects in loops forever.
connection.setInstanceFollowRedirects(false);
connection.connect();
return connection;
}
/**
* Copies the favicon stream to a buffer and decodes downloaded content into bitmaps using the
* FaviconDecoder.
*
* @param stream to decode
* @param contentLength as reported by the server (or -1)
* @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or
* null if no or corrupt data were 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.
*/
private LoadFaviconResult decodeImageFromResponse(Context context, InputStream stream, int contentLength) throws IOException {
// This may not be provided, but if it is, it's useful.
final int bufferSize;
if (contentLength > 0) {
// The size was reported and sane, so let's use that.
// Integer overflow should not be a problem for Favicon sizes...
bufferSize = contentLength + 1;
} else {
// No declared size, so guess and reallocate later if it turns out to be too small.
bufferSize = DEFAULT_FAVICON_BUFFER_SIZE_BYTES;
}
// Read the InputStream into a byte[].
final IOUtils.ConsumedInputStream result = IOUtils.readFully(stream, bufferSize);
if (result == null) {
return null;
}
// Having downloaded the image, decode it.
return FaviconDecoder.decodeFavicon(context, result.getData(), 0, result.consumedLength);
}
}

View file

@ -0,0 +1,168 @@
/* -*- 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.icons.loader;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.TypedValue;
import org.mozilla.gecko.R;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* This loader will generate an icon in case no icon could be loaded. In order to do so this needs
* to be the last loader that will be tried.
*/
public class IconGenerator implements IconLoader {
// Mozilla's Visual Design Colour Palette
// http://firefoxux.github.io/StyleGuide/#/visualDesign/colours
private static final int[] COLORS = {
0xFFc33c32,
0xFFf25820,
0xFFff9216,
0xFFffcb00,
0xFF57bd35,
0xFF01bdad,
0xFF0996f8,
0xFF02538b,
0xFF1f386e,
0xFF7a2f7a,
0xFFea385e,
};
// List of common prefixes of host names. Those prefixes will be striped before a prepresentative
// character for an URL is determined.
private static final String[] COMMON_PREFIXES = {
"www.",
"m.",
"mobile.",
};
private static final int TEXT_SIZE_DP = 12;
@Override
public IconResponse load(IconRequest request) {
if (request.getIconCount() > 1) {
// There are still other icons to try. We will only generate an icon if there's only one
// icon left and all previous loaders have failed (assuming this is the last one).
return null;
}
return generate(request.getContext(), request.getPageUrl());
}
/**
* Generate default favicon for the given page URL.
*/
@VisibleForTesting static IconResponse generate(Context context, String pageURL) {
final Resources resources = context.getResources();
final int widthAndHeight = resources.getDimensionPixelSize(R.dimen.favicon_bg);
final int roundedCorners = resources.getDimensionPixelOffset(R.dimen.favicon_corner_radius);
final Bitmap favicon = Bitmap.createBitmap(widthAndHeight, widthAndHeight, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(favicon);
final int color = pickColor(pageURL);
final Paint paint = new Paint();
paint.setColor(color);
canvas.drawRoundRect(new RectF(0, 0, widthAndHeight, widthAndHeight), roundedCorners, roundedCorners, paint);
paint.setColor(Color.WHITE);
final String character = getRepresentativeCharacter(pageURL);
final float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DP, context.getResources().getDisplayMetrics());
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(textSize);
paint.setAntiAlias(true);
canvas.drawText(character,
canvas.getWidth() / 2,
(int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)),
paint);
return IconResponse.createGenerated(favicon, color);
}
/**
* Get a representative character for the given URL.
*
* For example this method will return "f" for "http://m.facebook.com/foobar".
*/
@VisibleForTesting static String getRepresentativeCharacter(String url) {
if (TextUtils.isEmpty(url)) {
return "?";
}
final String snippet = getRepresentativeSnippet(url);
for (int i = 0; i < snippet.length(); i++) {
char c = snippet.charAt(i);
if (Character.isLetterOrDigit(c)) {
return String.valueOf(Character.toUpperCase(c));
}
}
// Nothing found..
return "?";
}
/**
* Return a color for this URL. Colors will be based on the host. URLs with the same host will
* return the same color.
*/
@VisibleForTesting static int pickColor(String url) {
if (TextUtils.isEmpty(url)) {
return COLORS[0];
}
final String snippet = getRepresentativeSnippet(url);
final int color = Math.abs(snippet.hashCode() % COLORS.length);
return COLORS[color];
}
/**
* Get the representative part of the URL. Usually this is the host (without common prefixes).
*/
private static String getRepresentativeSnippet(@NonNull String url) {
Uri uri = Uri.parse(url);
// Use the host if available
String snippet = uri.getHost();
if (TextUtils.isEmpty(snippet)) {
// If the uri does not have a host (e.g. file:// uris) then use the path
snippet = uri.getPath();
}
if (TextUtils.isEmpty(snippet)) {
// If we still have no snippet then just return the question mark
return "?";
}
// Strip common prefixes that we do not want to use to determine the representative character
for (String prefix : COMMON_PREFIXES) {
if (snippet.startsWith(prefix)) {
snippet = snippet.substring(prefix.length());
}
}
return snippet;
}
}

View file

@ -0,0 +1,23 @@
/* -*- 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.icons.loader;
import android.support.annotation.Nullable;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Generic interface for classes that can load icons.
*/
public interface IconLoader {
/**
* Loads the icon for this request or returns null if this loader can't load an icon for this
* request or just failed this time.
*/
@Nullable
IconResponse load(IconRequest request);
}

View file

@ -0,0 +1,45 @@
/* -*- 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.icons.loader;
import android.content.Context;
import android.util.Log;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.util.GeckoJarReader;
/**
* Loader implementation for loading icons from the omni.ja (jar:jar: URLs).
*
* https://developer.mozilla.org/en-US/docs/Mozilla/About_omni.ja_(formerly_omni.jar)
*/
public class JarLoader implements IconLoader {
private static final String LOGTAG = "Gecko/JarLoader";
@Override
public IconResponse load(IconRequest request) {
if (request.shouldSkipDisk()) {
return null;
}
final String iconUrl = request.getBestIcon().getUrl();
if (!iconUrl.startsWith("jar:jar:")) {
return null;
}
try {
final Context context = request.getContext();
return IconResponse.create(
GeckoJarReader.getBitmap(context, context.getResources(), iconUrl));
} catch (Exception e) {
// Just about anything could happen here.
Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
return null;
}
}
}

View file

@ -0,0 +1,74 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.icons.loader;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* This legacy loader loads icons from the abandoned database storage. This loader should only exist
* for a couple of releases and be removed afterwards.
*
* When updating to an app version with the new loaders our initial storage won't have any data so
* we need to continue loading from the database storage until the new storage has a good set of data.
*/
public class LegacyLoader implements IconLoader {
@Override
public IconResponse load(IconRequest request) {
if (!request.shouldSkipNetwork()) {
// If we are allowed to load from the network for this request then just ommit the legacy
// loader and fetch a fresh new icon.
return null;
}
if (request.shouldSkipDisk()) {
return null;
}
if (request.getIconCount() > 1) {
// There are still other icon URLs to try. Let's try to load from the legacy loader only
// if there's one icon left and the other loads have failed. We will ignore the icon URL
// anyways and try to receive the legacy icon URL from the database.
return null;
}
final Bitmap bitmap = loadBitmapFromDatabase(request);
if (bitmap == null) {
return null;
}
return IconResponse.create(bitmap);
}
/* package-private */ Bitmap loadBitmapFromDatabase(IconRequest request) {
final Context context = request.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final BrowserDB db = BrowserDB.from(context);
// We ask the database for the favicon URL and ignore the icon URL in the request object:
// As we are not updating the database anymore the icon might be stored under a different URL.
final String legacyFaviconUrl = db.getFaviconURLFromPageURL(contentResolver, request.getPageUrl());
if (legacyFaviconUrl == null) {
// No URL -> Nothing to load.
return null;
}
final LoadFaviconResult result = db.getFaviconForUrl(context, context.getContentResolver(), legacyFaviconUrl);
if (result == null) {
return null;
}
return result.getBestBitmap(request.getTargetSize());
}
}

View file

@ -0,0 +1,31 @@
/* -*- 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.icons.loader;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.storage.MemoryStorage;
/**
* Loader implementation for loading icons from an in-memory cached (Implemented by MemoryStorage).
*/
public class MemoryLoader implements IconLoader {
private final MemoryStorage storage;
public MemoryLoader() {
storage = MemoryStorage.get();
}
@Override
public IconResponse load(IconRequest request) {
if (request.shouldSkipMemory()) {
return null;
}
final String iconUrl = request.getBestIcon().getUrl();
return storage.getIcon(iconUrl);
}
}

View file

@ -0,0 +1,39 @@
/* -*- 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.icons.preparation;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.util.GeckoJarReader;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Preparer implementation for adding the omni.ja URL for internal about: pages.
*/
public class AboutPagesPreparer implements Preparer {
private Set<String> aboutUrls;
public AboutPagesPreparer() {
aboutUrls = new HashSet<>();
Collections.addAll(aboutUrls, AboutPages.DEFAULT_ICON_PAGES);
}
@Override
public void prepare(IconRequest request) {
if (aboutUrls.contains(request.getPageUrl())) {
final String iconUrl = GeckoJarReader.getJarURL(request.getContext(), "chrome/chrome/content/branding/favicon64.png");
request.modify()
.icon(IconDescriptor.createLookupIcon(iconUrl))
.deferBuild();
}
}
}

View file

@ -0,0 +1,39 @@
/* -*- 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.icons.preparation;
import android.text.TextUtils;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.util.StringUtils;
/**
* Preparer to add the "default/guessed" favicon URL (domain/favicon.ico) to the list of URLs to
* try loading the favicon from.
*
* The default URL will be added with a very low priority so that we will only try to load from this
* URL if all other options failed.
*/
public class AddDefaultIconUrl implements Preparer {
@Override
public void prepare(IconRequest request) {
if (!StringUtils.isHttpOrHttps(request.getPageUrl())) {
return;
}
final String defaultFaviconUrl = IconsHelper.guessDefaultFaviconURL(request.getPageUrl());
if (TextUtils.isEmpty(defaultFaviconUrl)) {
// We couldn't generate a default favicon URL for this URL. Nothing to do here.
return;
}
request.modify()
.icon(IconDescriptor.createGenericIcon(defaultFaviconUrl))
.deferBuild();
}
}

View file

@ -0,0 +1,29 @@
/* -*- 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.icons.preparation;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.storage.FailureCache;
import java.util.Iterator;
public class FilterKnownFailureUrls implements Preparer {
@Override
public void prepare(IconRequest request) {
final FailureCache failureCache = FailureCache.get();
final Iterator<IconDescriptor> iterator = request.getIconIterator();
while (iterator.hasNext()) {
final IconDescriptor descriptor = iterator.next();
if (failureCache.isKnownFailure(descriptor.getUrl())) {
// Loading from this URL has failed in the past. Do not try again.
iterator.remove();
}
}
}
}

View file

@ -0,0 +1,39 @@
/* -*- 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.icons.preparation;
import android.text.TextUtils;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconsHelper;
import java.util.Iterator;
/**
* Preparer implementation to filter unknown MIME types to avoid loading images that we cannot decode.
*/
public class FilterMimeTypes implements Preparer {
@Override
public void prepare(IconRequest request) {
final Iterator<IconDescriptor> iterator = request.getIconIterator();
while (iterator.hasNext()) {
final IconDescriptor descriptor = iterator.next();
final String mimeType = descriptor.getMimeType();
if (TextUtils.isEmpty(mimeType)) {
// We do not have a MIME type for this icon, so we cannot know in advance if we are able
// to decode it. Let's just continue.
continue;
}
if (!IconsHelper.canDecodeType(mimeType)) {
iterator.remove();
}
}
}
}

View file

@ -0,0 +1,30 @@
package org.mozilla.gecko.icons.preparation;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.util.StringUtils;
import java.util.Iterator;
/**
* Filter non http/https URLs if the request is not from privileged code.
*/
public class FilterPrivilegedUrls implements Preparer {
@Override
public void prepare(IconRequest request) {
if (request.isPrivileged()) {
// This request is privileged. No need to filter anything.
return;
}
final Iterator<IconDescriptor> iterator = request.getIconIterator();
while (iterator.hasNext()) {
IconDescriptor descriptor = iterator.next();
if (!StringUtils.isHttpOrHttps(descriptor.getUrl())) {
iterator.remove();
}
}
}
}

View file

@ -0,0 +1,56 @@
/* -*- 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.icons.preparation;
import org.mozilla.gecko.icons.IconDescriptor;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.storage.DiskStorage;
import org.mozilla.gecko.icons.storage.MemoryStorage;
/**
* Preparer implementation to lookup the icon URL for the page URL in the request. This class tries
* to locate the icon URL by looking through previously stored mappings on disk and in memory.
*/
public class LookupIconUrl implements Preparer {
@Override
public void prepare(IconRequest request) {
if (lookupFromMemory(request)) {
return;
}
lookupFromDisk(request);
}
private boolean lookupFromMemory(IconRequest request) {
final String iconUrl = MemoryStorage.get()
.getMapping(request.getPageUrl());
if (iconUrl != null) {
request.modify()
.icon(IconDescriptor.createLookupIcon(iconUrl))
.deferBuild();
return true;
}
return false;
}
private boolean lookupFromDisk(IconRequest request) {
final String iconUrl = DiskStorage.get(request.getContext())
.getMapping(request.getPageUrl());
if (iconUrl != null) {
request.modify()
.icon(IconDescriptor.createLookupIcon(iconUrl))
.deferBuild();
return true;
}
return false;
}
}

View file

@ -0,0 +1,19 @@
/* -*- 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.icons.preparation;
import org.mozilla.gecko.icons.IconRequest;
/**
* Generic interface for a class "preparing" a request before we try to load icons. A class
* implementing this interface can modify the request (e.g. filter or add icon URLs).
*/
public interface Preparer {
/**
* Inspects or modifies the request before any icon is loaded.
*/
void prepare(IconRequest request);
}

View file

@ -0,0 +1,61 @@
/* -*- 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.icons.processing;
import android.support.v7.graphics.Palette;
import android.util.Log;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.util.HardwareUtils;
/**
* Processor implementation to extract the dominant color from the icon and attach it to the icon
* response object.
*/
public class ColorProcessor implements Processor {
private static final String LOGTAG = "GeckoColorProcessor";
private static final int DEFAULT_COLOR = 0; // 0 == No color
@Override
public void process(IconRequest request, IconResponse response) {
if (response.hasColor()) {
return;
}
if (HardwareUtils.isX86System()) {
// (Bug 1318667) We are running into crashes when using the palette library with
// specific icons on x86 devices. They take down the whole VM and are not recoverable.
// Unfortunately our release icon is triggering this crash. Until we can switch to a
// newer version of the support library where this does not happen, we are using our
// own slower implementation.
extractColorUsingCustomImplementation(response);
} else {
extractColorUsingPaletteSupportLibrary(response);
}
}
private void extractColorUsingPaletteSupportLibrary(final IconResponse response) {
try {
final Palette palette = Palette.from(response.getBitmap()).generate();
response.updateColor(palette.getVibrantColor(DEFAULT_COLOR));
} catch (ArrayIndexOutOfBoundsException e) {
// We saw the palette library fail with an ArrayIndexOutOfBoundsException intermittently
// in automation. In this case lets just swallow the exception and move on without a
// color. This is a valid condition and callers should handle this gracefully (Bug 1318560).
Log.e(LOGTAG, "Palette generation failed with ArrayIndexOutOfBoundsException", e);
response.updateColor(DEFAULT_COLOR);
}
}
private void extractColorUsingCustomImplementation(final IconResponse response) {
final int dominantColor = BitmapUtils.getDominantColor(response.getBitmap());
response.updateColor(dominantColor);
}
}

View file

@ -0,0 +1,36 @@
package org.mozilla.gecko.icons.processing;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.storage.DiskStorage;
import org.mozilla.gecko.util.StringUtils;
public class DiskProcessor implements Processor {
@Override
public void process(IconRequest request, IconResponse response) {
if (request.shouldSkipDisk()) {
return;
}
if (!response.hasUrl() || !StringUtils.isHttpOrHttps(response.getUrl())) {
// If the response does not contain an URL from which the icon was loaded or if this is
// not a http(s) URL then we cannot store this or do not need to (because it's already
// stored somewhere else, like for URLs pointing inside the omni.ja).
return;
}
final DiskStorage storage = DiskStorage.get(request.getContext());
if (response.isFromNetwork()) {
// The icon has been loaded from the network. Store it on the disk now.
storage.putIcon(response);
}
if (response.isFromMemory() || response.isFromDisk() || response.isFromNetwork()) {
// Remember mapping between page URL and storage URL. Even when this icon has been loaded
// from memory or disk this does not mean that we stored this mapping already: We could
// have loaded this icon for a different page URL previously.
storage.putMapping(request, response.getUrl());
}
}
}

View file

@ -0,0 +1,38 @@
/* -*- 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.icons.processing;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.icons.storage.MemoryStorage;
public class MemoryProcessor implements Processor {
private final MemoryStorage storage;
public MemoryProcessor() {
storage = MemoryStorage.get();
}
@Override
public void process(IconRequest request, IconResponse response) {
if (request.shouldSkipMemory() || request.getIconCount() == 0 || response.isGenerated()) {
// Do not cache this icon in memory if we should skip the memory cache or if this icon
// has been generated. We can re-generate it if needed.
return;
}
final String iconUrl = request.getBestIcon().getUrl();
if (iconUrl.startsWith("data:image/")) {
// The image data is encoded in the URL. It doesn't make sense to store the URL and the
// bitmap in cache.
return;
}
storage.putMapping(request, iconUrl);
storage.putIcon(iconUrl, response);
}
}

View file

@ -0,0 +1,21 @@
/* -*- 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.icons.processing;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Generic interface for a class that processes a response object after an icon has been loaded and
* decoded. A class implementing this interface can attach additional data to the response or modify
* the bitmap (e.g. resizing).
*/
public interface Processor {
/**
* Process a response object containing an icon loaded for this request.
*/
void process(IconRequest request, IconResponse response);
}

View file

@ -0,0 +1,68 @@
/* -*- 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.icons.processing;
import android.graphics.Bitmap;
import android.support.annotation.VisibleForTesting;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Processor implementation for resizing the loaded icon based on the target size.
*/
public class ResizingProcessor implements Processor {
@Override
public void process(IconRequest request, IconResponse response) {
if (response.isFromMemory()) {
// This bitmap has been loaded from memory, so it has already gone through the resizing
// process. We do not want to resize the image every time we hit the memory cache.
return;
}
final Bitmap originalBitmap = response.getBitmap();
final int size = originalBitmap.getWidth();
final int targetSize = request.getTargetSize();
if (size == targetSize) {
// The bitmap has exactly the size we are looking for.
return;
}
final Bitmap resizedBitmap;
if (size > targetSize) {
resizedBitmap = resize(originalBitmap, targetSize);
} else {
// Our largest primary is smaller than the desired size. Upscale by a maximum of 2x.
// 'largestSize' now reflects the maximum size we can upscale to.
final int largestSize = size * 2;
if (largestSize > targetSize) {
// Perfect! We can upscale by less than 2x and reach the needed size. Do it.
resizedBitmap = resize(originalBitmap, targetSize);
} else {
// We don't have enough information to make the target size look non terrible. Best effort:
resizedBitmap = resize(originalBitmap, largestSize);
}
}
response.updateBitmap(resizedBitmap);
originalBitmap.recycle();
}
@VisibleForTesting Bitmap resize(Bitmap bitmap, int targetSize) {
try {
return Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true);
} catch (OutOfMemoryError error) {
// There's not enough memory to create a resized copy of the bitmap in memory. Let's just
// use what we have.
return bitmap;
}
}
}

View file

@ -0,0 +1,293 @@
/* -*- 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.icons.storage;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;
import android.util.Log;
import com.jakewharton.disklrucache.DiskLruCache;
import org.mozilla.gecko.background.nativecode.NativeCrypto;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
/**
* Least Recently Used (LRU) disk cache for icons and the mappings from page URLs to icon URLs.
*/
public class DiskStorage {
private static final String LOGTAG = "Gecko/DiskStorage";
/**
* Maximum size (in bytes) of the cache. This cache is located in the cache directory of the
* application and can be cleared by the user.
*/
private static final int DISK_CACHE_SIZE = 50 * 1024 * 1024;
/**
* Version of the cache. Updating the version will invalidate all existing items.
*/
private static final int CACHE_VERSION = 1;
private static final String KEY_PREFIX_ICON = "icon:";
private static final String KEY_PREFIX_MAPPING = "mapping:";
private static DiskStorage instance;
public static DiskStorage get(Context context) {
if (instance == null) {
instance = new DiskStorage(context);
}
return instance;
}
private Context context;
private DiskLruCache cache;
private DiskStorage(Context context) {
this.context = context.getApplicationContext();
}
@CheckResult
private synchronized DiskLruCache ensureCacheIsReady() throws IOException {
if (cache == null || cache.isClosed()) {
cache = DiskLruCache.open(
new File(context.getCacheDir(), "icons"),
CACHE_VERSION,
1,
DISK_CACHE_SIZE);
}
return cache;
}
/**
* Store a mapping from page URL to icon URL in the cache.
*/
public void putMapping(IconRequest request, String iconUrl) {
putMapping(request.getPageUrl(), iconUrl);
}
/**
* Store a mapping from page URL to icon URL in the cache.
*/
public void putMapping(String pageUrl, String iconUrl) {
DiskLruCache.Editor editor = null;
try {
final DiskLruCache cache = ensureCacheIsReady();
final String key = createKey(KEY_PREFIX_MAPPING, pageUrl);
if (key == null) {
return;
}
editor = cache.edit(key);
if (editor == null) {
return;
}
editor.set(0, iconUrl);
editor.commit();
} catch (IOException e) {
Log.w(LOGTAG, "IOException while accessing disk cache", e);
abortSilently(editor);
}
}
/**
* Store an icon in the cache (uses the icon URL as key).
*/
public void putIcon(IconResponse response) {
putIcon(response.getUrl(), response.getBitmap());
}
/**
* Store an icon in the cache (uses the icon URL as key).
*/
public void putIcon(String iconUrl, Bitmap bitmap) {
OutputStream outputStream = null;
DiskLruCache.Editor editor = null;
try {
final DiskLruCache cache = ensureCacheIsReady();
final String key = createKey(KEY_PREFIX_ICON, iconUrl);
if (key == null) {
return;
}
editor = cache.edit(key);
if (editor == null) {
return;
}
outputStream = editor.newOutputStream(0);
boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100 /* quality; ignored. PNG is lossless */, outputStream);
outputStream.close();
if (success) {
editor.commit();
} else {
editor.abort();
}
} catch (IOException e) {
Log.w(LOGTAG, "IOException while accessing disk cache", e);
abortSilently(editor);
} finally {
IOUtils.safeStreamClose(outputStream);
}
}
/**
* Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL.
*/
@Nullable
public IconResponse getIcon(String iconUrl) {
InputStream inputStream = null;
try {
final DiskLruCache cache = ensureCacheIsReady();
final String key = createKey(KEY_PREFIX_ICON, iconUrl);
if (key == null) {
return null;
}
if (cache.isClosed()) {
throw new RuntimeException("CLOSED");
}
final DiskLruCache.Snapshot snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
inputStream = snapshot.getInputStream(0);
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap == null) {
return null;
}
return IconResponse.createFromDisk(bitmap, iconUrl);
} catch (IOException e) {
Log.w(LOGTAG, "IOException while accessing disk cache", e);
} finally {
IOUtils.safeStreamClose(inputStream);
}
return null;
}
/**
* Get the icon URL for this page URL. Returns null if no mapping is in the cache.
*/
@Nullable
public String getMapping(String pageUrl) {
try {
final DiskLruCache cache = ensureCacheIsReady();
final String key = createKey(KEY_PREFIX_MAPPING, pageUrl);
if (key == null) {
return null;
}
DiskLruCache.Snapshot snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
return snapshot.getString(0);
} catch (IOException e) {
Log.w(LOGTAG, "IOException while accessing disk cache", e);
}
return null;
}
/**
* Remove all entries from this cache.
*/
public void evictAll() {
try {
final DiskLruCache cache = ensureCacheIsReady();
cache.delete();
} catch (IOException e) {
Log.w(LOGTAG, "IOException while accessing disk cache", e);
}
}
/**
* Create a key for this URL using the given prefix.
*
* The disk cache requires valid file names to be used as key. Therefore we hash the created key
* (SHA-256).
*/
@Nullable
private String createKey(String prefix, String url) {
try {
// We use our own crypto implementation to avoid the penalty of loading the java crypto
// framework.
byte[] ctx = NativeCrypto.sha256init();
if (ctx == null) {
return null;
}
byte[] data = prefix.getBytes("UTF-8");
NativeCrypto.sha256update(ctx, data, data.length);
data = url.getBytes("UTF-8");
NativeCrypto.sha256update(ctx, data, data.length);
return Utils.byte2Hex(NativeCrypto.sha256finalize(ctx));
} catch (NoClassDefFoundError | ExceptionInInitializerError error) {
// We could not load libmozglue.so. Let's use Java's MessageDigest as fallback. We do
// this primarily for our unit tests that can't load native libraries. On an device
// we will have a lot of other problems if we can't load libmozglue.so
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(prefix.getBytes("UTF-8"));
md.update(url.getBytes("UTF-8"));
return Utils.byte2Hex(md.digest());
} catch (Exception e) {
// Just give up. And let everyone know.
throw new RuntimeException(e);
}
} catch (UnsupportedEncodingException e) {
throw new AssertionError("Should not happen: Device does not understand UTF-8");
}
}
private void abortSilently(DiskLruCache.Editor editor) {
if (editor != null) {
try {
editor.abort();
} catch (IOException e) {
// Ignore
}
}
}
}

View file

@ -0,0 +1,70 @@
/* -*- 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.icons.storage;
import android.os.SystemClock;
import android.support.annotation.VisibleForTesting;
import android.util.LruCache;
/**
* In-memory cache to remember URLs from which loading icons has failed recently.
*/
public class FailureCache {
/**
* Retry loading failed icons after 4 hours.
*/
private static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 60 * 4;
private static final int MAX_ENTRIES = 25;
private static FailureCache instance;
public static synchronized FailureCache get() {
if (instance == null) {
instance = new FailureCache();
}
return instance;
}
private final LruCache<String, Long> cache;
private FailureCache() {
cache = new LruCache<>(MAX_ENTRIES);
}
/**
* Remember this icon URL after loading from it (over the network) has failed.
*/
public void rememberFailure(String iconUrl) {
cache.put(iconUrl, SystemClock.elapsedRealtime());
}
/**
* Has loading from this URL failed previously and recently?
*/
public boolean isKnownFailure(String iconUrl) {
synchronized (cache) {
final Long failedAt = cache.get(iconUrl);
if (failedAt == null) {
return false;
}
if (failedAt + FAILURE_RETRY_MILLISECONDS < SystemClock.elapsedRealtime()) {
// The wait time has passed and we can retry loading from this URL.
cache.remove(iconUrl);
return false;
}
}
return true;
}
@VisibleForTesting
public void evictAll() {
cache.evictAll();
}
}

View file

@ -0,0 +1,112 @@
/* -*- 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.icons.storage;
import android.graphics.Bitmap;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.LruCache;
import org.mozilla.gecko.icons.IconRequest;
import org.mozilla.gecko.icons.IconResponse;
/**
* Least Recently Used (LRU) memory cache for icons and the mappings from page URLs to icon URLs.
*/
public class MemoryStorage {
/**
* Maximum number of items in the cache for mapping page URLs to icon URLs.
*/
private static final int MAPPING_CACHE_SIZE = 500;
private static MemoryStorage instance;
public static synchronized MemoryStorage get() {
if (instance == null) {
instance = new MemoryStorage();
}
return instance;
}
/**
* Class representing an cached icon. We store the original bitmap and the color in cache only.
*/
private static class CacheEntry {
private final Bitmap bitmap;
private final int color;
private CacheEntry(Bitmap bitmap, int color) {
this.bitmap = bitmap;
this.color = color;
}
}
private final LruCache<String, CacheEntry> iconCache; // Guarded by 'this'
private final LruCache<String, String> mappingCache; // Guarded by 'this'
private MemoryStorage() {
iconCache = new LruCache<String, CacheEntry>(calculateCacheSize()) {
@Override
protected int sizeOf(String key, CacheEntry value) {
return value.bitmap.getByteCount() / 1024;
}
};
mappingCache = new LruCache<>(MAPPING_CACHE_SIZE);
}
private int calculateCacheSize() {
// Use a maximum of 1/8 of the available memory for storing cached icons.
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
return maxMemory / 8;
}
/**
* Store a mapping from page URL to icon URL in the cache.
*/
public synchronized void putMapping(IconRequest request, String iconUrl) {
mappingCache.put(request.getPageUrl(), iconUrl);
}
/**
* Get the icon URL for this page URL. Returns null if no mapping is in the cache.
*/
@Nullable
public synchronized String getMapping(String pageUrl) {
return mappingCache.get(pageUrl);
}
/**
* Store an icon in the cache (uses the icon URL as key).
*/
public synchronized void putIcon(String url, IconResponse response) {
final CacheEntry entry = new CacheEntry(response.getBitmap(), response.getColor());
iconCache.put(url, entry);
}
/**
* Get an icon for the icon URL from the cache. Returns null if no icon is cached for this URL.
*/
@Nullable
public synchronized IconResponse getIcon(String iconUrl) {
final CacheEntry entry = iconCache.get(iconUrl);
if (entry == null) {
return null;
}
return IconResponse.createFromMemory(entry.bitmap, iconUrl, entry.color);
}
/**
* Remove all entries from this cache.
*/
public synchronized void evictAll() {
iconCache.evictAll();
mappingCache.evictAll();
}
}