mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-08 00:38:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
222
mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
Normal file
222
mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
35
mobile/android/base/java/org/mozilla/gecko/icons/Icons.java
Normal file
35
mobile/android/base/java/org/mozilla/gecko/icons/Icons.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue